Repository: alibaba/formily Branch: formily_next Commit: d9a46442a575 Files: 1323 Total size: 4.4 MB Directory structure: gitextract_50e4s068/ ├── .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 ├── LICENSE.md ├── README.md ├── README.zh-cn.md ├── commitlint.config.js ├── devtools/ │ ├── .eslintrc │ └── chrome-extension/ │ ├── .npmignore │ ├── LICENSE.md │ ├── config/ │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── package.json │ ├── src/ │ │ ├── app/ │ │ │ ├── components/ │ │ │ │ ├── FieldTree.tsx │ │ │ │ ├── LeftPanel.tsx │ │ │ │ ├── RightPanel.tsx │ │ │ │ ├── SearchBox.tsx │ │ │ │ ├── Tabs.tsx │ │ │ │ └── filter.ts │ │ │ ├── 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/ │ │ │ ├── VerifyCode.tsx │ │ │ ├── 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 │ │ ├── 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 ├── package.json ├── packages/ │ ├── .eslintrc │ ├── antd/ │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── __tests__/ │ │ │ ├── moment.spec.ts │ │ │ └── sideEffects.spec.ts │ │ ├── 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 │ │ │ │ ├── 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 │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── package.json │ │ ├── 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/ │ │ │ │ ├── PasswordStrength.tsx │ │ │ │ ├── index.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 │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.tsx │ │ ├── template.ejs │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── core/ │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── docs/ │ │ │ ├── api/ │ │ │ │ ├── entry/ │ │ │ │ │ ├── ActionResponse.less │ │ │ │ │ ├── ActionResponse.tsx │ │ │ │ │ ├── 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 │ │ │ │ │ ├── createForm.md │ │ │ │ │ └── createForm.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 │ │ ├── package.json │ │ ├── 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 │ │ │ │ ├── LifeCycle.ts │ │ │ │ ├── ObjectField.ts │ │ │ │ ├── Query.ts │ │ │ │ ├── VoidField.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── shared/ │ │ │ │ ├── checkers.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── effective.ts │ │ │ │ ├── externals.ts │ │ │ │ └── internals.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── element/ │ │ ├── .npmignore │ │ ├── README.md │ │ ├── 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 │ │ │ ├── README.md │ │ │ ├── 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 │ │ ├── package.json │ │ ├── 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 │ │ ├── README.md │ │ ├── package.json │ │ ├── rollup.config.js │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── observer.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── json-schema/ │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── package.json │ │ ├── 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/ │ │ │ │ ├── SPECIFICATION_1_0.ts │ │ │ │ └── index.ts │ │ │ ├── schema.ts │ │ │ ├── shared.ts │ │ │ ├── transformer.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── next/ │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LESENCE.md │ │ ├── README.md │ │ ├── __tests__/ │ │ │ ├── moment.spec.ts │ │ │ └── sideEffects.spec.ts │ │ ├── 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 │ │ │ │ ├── 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 │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── package.json │ │ ├── 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/ │ │ │ │ ├── PasswordStrength.tsx │ │ │ │ ├── index.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 │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── benchmark.ts │ │ ├── package.json │ │ ├── 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 │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── 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/ │ │ │ │ ├── Schema.md │ │ │ │ ├── Schema.zh-CN.md │ │ │ │ ├── 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 │ │ │ ├── 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 │ │ ├── package.json │ │ ├── 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 │ │ │ │ ├── ObjectField.tsx │ │ │ │ ├── ReactiveField.tsx │ │ │ │ ├── RecordScope.tsx │ │ │ │ ├── RecordsScope.tsx │ │ │ │ ├── RecursionField.tsx │ │ │ │ ├── SchemaField.tsx │ │ │ │ ├── VoidField.tsx │ │ │ │ └── index.ts │ │ │ ├── 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 │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── 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 │ │ ├── package.json │ │ ├── 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 │ │ ├── README.md │ │ ├── package.json │ │ ├── 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 │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── MySlowList.js │ │ │ └── index.js │ │ ├── template.ejs │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── reactive-vue/ │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── package.json │ │ ├── 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 │ │ ├── README.md │ │ ├── package.json │ │ ├── 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 │ │ ├── README.md │ │ ├── package.json │ │ ├── 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 │ ├── README.md │ ├── 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 │ │ ├── README.md │ │ ├── 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/ │ │ │ ├── README.md │ │ │ ├── architecture.md │ │ │ └── concept.md │ │ └── questions/ │ │ └── README.md │ ├── package.json │ ├── 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 │ │ │ ├── ObjectField.ts │ │ │ ├── ReactiveField.ts │ │ │ ├── RecursionField.ts │ │ │ ├── SchemaField.ts │ │ │ ├── VoidField.ts │ │ │ └── index.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 ├── scripts/ │ ├── build-style/ │ │ ├── buildAllStyles.ts │ │ ├── copy.ts │ │ ├── helper.ts │ │ └── index.ts │ └── rollup.base.js ├── tsconfig.build.json ├── tsconfig.jest.json └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .all-contributorsrc ================================================ { "projectName": "formily", "projectOwner": "alibaba", "repoType": "github", "repoHost": "https://github.com", "files": [ "README.md" ], "imageSize": 100, "commit": false, "contributors": [ { "login": "janryWang", "name": "Janry", "avatar_url": "https://avatars0.githubusercontent.com/u/4060976?v=4", "profile": "https://github.com/janryWang", "contributions": [ "design" ] }, { "login": "cnt1992", "name": "SkyCai", "avatar_url": "https://avatars1.githubusercontent.com/u/3118988?v=4", "profile": "http://cnt1992.github.io", "contributions": [ "design" ] }, { "login": "yujiangshui", "name": "Harry Yu", "avatar_url": "https://avatars3.githubusercontent.com/u/2942913?v=4", "profile": "https://www.linkedin.com/in/harry-yu-0a931a69/", "contributions": [ "doc", "code" ] }, { "login": "zsirfs", "name": "zsir", "avatar_url": "https://avatars2.githubusercontent.com/u/22249411?v=4", "profile": "https://www.luoyangfu.com", "contributions": [ "code" ] }, { "login": "monkindey", "name": "Kiho · Cham", "avatar_url": "https://avatars0.githubusercontent.com/u/6913898?v=4", "profile": "http://www.monkindey.xyz/", "contributions": [ "code", "doc" ] }, { "login": "whj1995", "name": "Hongjiang Wu", "avatar_url": "https://avatars2.githubusercontent.com/u/22634735?v=4", "profile": "http://whj1995.xyz", "contributions": [ "doc" ] }, { "login": "anyuxuan", "name": "合木", "avatar_url": "https://avatars3.githubusercontent.com/u/24931869?v=4", "profile": "https://github.com/anyuxuan", "contributions": [ "code" ] }, { "login": "Azath0th", "name": "Chen YuBen", "avatar_url": "https://avatars2.githubusercontent.com/u/18497361?v=4", "profile": "https://github.com/Azath0th", "contributions": [ "code" ] }, { "login": "HarrisFeng", "name": "Harris Feng", "avatar_url": "https://avatars1.githubusercontent.com/u/7928957?v=4", "profile": "https://github.com/HarrisFeng", "contributions": [ "code" ] } ], "contributorsPerLine": 7 } ================================================ FILE: .codecov.yml ================================================ coverage: status: project: default: threshold: 0.1% patch: default: threshold: 0.1% target: 95% ================================================ FILE: .editorconfig ================================================ # EditorConfig is awesome: http://EditorConfig.org # top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 [*.gradle] indent_size = 4 ================================================ FILE: .eslintignore ================================================ node_modules lib dist build coverage expected website gh-pages weex build.ts packages/vue packages/element esm doc-site public package ================================================ FILE: .eslintrc ================================================ { "env": { "node": true }, "extends": [ "plugin:react/recommended", "plugin:@typescript-eslint/recommended", "prettier/@typescript-eslint" ], "globals": { "sleep": true, "prettyFormat": true }, "parserOptions": { "ecmaVersion": 10, "sourceType": "module", "ecmaFeatures": { "jsx": true } }, "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint", "react", "prettier", "markdown"], "settings": { "react": { "version": "detect" } }, "rules": { "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/ban-ts-comment": "off", "react/no-unescaped-entities": "off", "react/prop-types": "off" }, "overrides": [ { "files": ["**/*.md"], "processor": "markdown/markdown" }, { "files": ["**/*.md/*.{jsx,tsx}"], "rules": { "@typescript-eslint/no-unused-vars": "error", "no-unused-vars": "error", "no-console": "off", "react/display-name": "off", "react/prop-types": "off" } }, { "files": ["**/*.md/*.{js,ts}"], "rules": { "@typescript-eslint/no-unused-vars": "off", "no-unused-vars": "off", "no-console": "off", "react/display-name": "off", "react/prop-types": "off" } } ] } ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing Guide Hi! I’m really excited that you are interested in contributing to Formily. Before submitting your contribution though, please make sure to take a moment and read through the following guidelines. - [Contributing Guide](#contributing-guide) - [Issue Reporting Guidelines](#issue-reporting-guidelines) - [Pull Request Guidelines](#pull-request-guidelines) - [Git Commit Specific](#git-commit-specific) ## Issue Reporting Guidelines - The issue list of this repo is **exclusively** for bug reports and feature requests. Non-conforming issues will be closed immediately. - For simple beginner questions, you can get quick answers from - For more complicated questions, you can use Google or StackOverflow. Make sure to provide enough information when asking your questions - this makes it easier for others to help you! - Try to search for your issue, it may have already been answered or even fixed in the development branch. - Check if the issue is reproducible with the latest stable version of Formily. If you are using a pre-release, please indicate the specific version you are using. - It is **required** that you clearly describe the steps necessary to reproduce the issue you are running into. Issues with no clear repro steps will not be triaged. If an issue labeled "need repro" receives no further input from the issue author for more than 5 days, it will be closed. - For bugs that involves build setups, you can create a reproduction repository with steps in the README. - If your issue is resolved but still open, don’t hesitate to close it. In case you found a solution by yourself, it could be helpful to explain how you fixed it. ## Pull Request Guidelines - Only code that's ready for release should be committed to the master branch. All development should be done in dedicated branches. - Checkout a **new** topic branch from master branch, and merge back against master branch. - Work in the `src` folder and **DO NOT** checkin `dist` in the commits. - Make sure `npm test` passes. - If adding new feature: - Add accompanying test case. - Provide convincing reason to add this feature. Ideally you should open a suggestion issue first and have it greenlighted before working on it. - If fixing a bug: - If you are resolving a special issue, add `(fix #xxxx[,#xxx])` (#xxxx is the issue id) in your PR title for a better release log, e.g. `update entities encoding/decoding (fix #3899)`. - Provide detailed description of the bug in the PR. Live demo preferred. - Add appropriate test coverage if applicable. ## Git Commit Specific - Your commits message must follow our [git commit specific](https://github.com/alibaba/formily/blob/master/.github/GIT_COMMIT_SPECIFIC.md). - We will check your commit message, if it does not conform to the specification, the commit will be automatically refused, make sure you have read the specification above. - You could use `git cz` with a CLI interface to replace `git commit` command, it will help you to build a proper commit-message, see [commitizen](https://github.com/commitizen/cz-cli). - It's OK to have multiple small commits as you work on your branch - we will let GitHub automatically squash it before merging. ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: formily # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: Create new issue url: https://formilyjs.org/guide/issue-helper about: The issue which is not created via https://formilyjs.org/guide/issue-helper will be closed immediately. - name: ✨ Question Answer / Idea url: https://github.com/alibaba/formily/discussions/new about: All questions can be solved here. At the same time you can provide all your ideas here. - name: 📖 View documentation url: https://formilyjs.org about: Official Formily documentation ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ _Before_ submitting a pull request, please make sure the following is done... - [ ] Ensure the pull request title and commit message follow the [Commit Specific](https://formilyjs.org/guide/contribution#pr-specification) in **English**. - [ ] Fork the repo and create your branch from `master` or `formily_next`. - [ ] If you've added code that should be tested, add tests! - [ ] If you've changed APIs, update the documentation. - [ ] Ensure the test suite passes (`npm test`). - [ ] Make sure your code lints (`npm run lint`) - we've done our best to make sure these rules match our internal linting guidelines. **Please do not delete the above content** --- ## What have you changed? ================================================ FILE: .github/workflows/check-pr-title.yml ================================================ name: Check PR title on: pull_request_target: types: - opened - reopened - edited - synchronize jobs: lint: runs-on: ubuntu-latest steps: - uses: aslafy-z/conventional-pr-title-action@v2.4.0 with: preset: conventional-changelog-angular@^5.0.6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/ci.yml ================================================ name: Node CI on: push: branches: - formily_next pull_request: branches: - formily_next jobs: build: runs-on: ubuntu-latest if: contains(github.event.head_commit.message, 'chore(versions)') == false steps: - uses: actions/checkout@v1 - name: Use Node.js uses: actions/setup-node@v1 with: node-version: 16 - run: yarn -v - run: yarn --ignore-engines - name: ESlint uses: reviewdog/action-eslint@v1 with: reporter: github-check eslint_flags: '.' - run: yarn build - run: yarn test:prod env: CI: true HEADLESS: false PROGRESS: none NODE_ENV: test NODE_OPTIONS: --max_old_space_size=4096 ================================================ FILE: .github/workflows/commitlint.yml ================================================ # This is a basic workflow to help you get started with Actions name: Check Commit spec # Controls when the action will run. on: # Triggers the workflow on push or pull request events but only for the formily_next branch push: branches: [formily_next] pull_request: branches: [formily_next] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" commitlint: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 with: fetch-depth: 0= - uses: wagoid/commitlint-github-action@v3 ================================================ FILE: .github/workflows/issue-open-check.yml ================================================ name: Issue Open Check on: issues: types: [opened] jobs: check-issue: runs-on: ubuntu-latest steps: - uses: actions-cool/check-user-permission@v1.0.0 id: checkUser with: require: 'write' - name: check invalid if: (contains(github.event.issue.body, 'formily-issue-helper') == false) && (steps.checkUser.outputs.result == 'false') uses: actions-cool/issues-helper@v1.2 with: actions: 'create-comment,add-labels,close-issue' issue-number: ${{ github.event.issue.number }} labels: 'Invalid' body: | Hello @${{ github.event.issue.user.login }}, your issue has been closed because it does not conform to our issue requirements. Please use the [Issue Helper](https://formilyjs.org/guide/issue-helper) to create an issue, thank you! 你好 @${{ github.event.issue.user.login }},为了能够进行高效沟通,我们对 issue 有一定的格式要求,你的 issue 因为不符合要求而被自动关闭。你可以通过 [issue 助手](https://formilyjs.org/guide/issue-helper) 来创建 issue 以方便我们定位错误。谢谢配合! ================================================ FILE: .github/workflows/package-size.yml ================================================ name: Compressed Size on: [pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 16 - uses: preactjs/compressed-size-action@v2 with: repo-token: '${{ secrets.GITHUB_TOKEN }}' ================================================ FILE: .github/workflows/pr-welcome.yml ================================================ name: PR Welcome on: pull_request_target: types: [opened] jobs: welcome: runs-on: ubuntu-latest steps: - uses: actions-cool/pr-welcome@v1.1.2 with: pr-emoji: '+1, heart' ================================================ FILE: .gitignore ================================================ *~ *.swp .DS_Store .tea npm-debug.log lerna-debug.log npm-debug.log* package-lock.json lib/ esm/ temp_esm/ dist/ type-artefacts/ build/ coverage/ node_modules/ examples/test .idea/ TODO.md tsconfig.tsbuildinfo package/ package.zip .umi .umi-production .cjsescache doc-site .lerna-changelog .history .lint-report.log ================================================ FILE: .prettierrc.js ================================================ module.exports = { semi: false, tabWidth: 2, singleQuote: true, } ================================================ FILE: .umirc.js ================================================ export default { mode: 'site', logo: '//img.alicdn.com/imgextra/i2/O1CN01Kq3OHU1fph6LGqjIz_!!6000000004056-55-tps-1141-150.svg', title: 'Formily', hash: true, favicon: '//img.alicdn.com/imgextra/i3/O1CN01XtT3Tv1Wd1b5hNVKy_!!6000000002810-55-tps-360-360.svg', outputPath: './doc-site', locales: [ ['en-US', 'English'], ['zh-CN', '中文'], ], navs: { 'en-US': [ { title: 'Guide', path: '/guide', }, { title: 'Basic Core Library', children: [ { title: '@formily/reactive', path: 'https://reactive.formilyjs.org', }, { title: '@formily/core', path: 'https://core.formilyjs.org', }, { title: '@formily/react', path: 'https://react.formilyjs.org', }, { title: '@formily/vue', path: 'https://vue.formilyjs.org', }, ], }, { title: 'Component Ecology', children: [ { title: '@formily/antd', path: 'https://antd.formilyjs.org', }, { title: '@formily/antd-v5', path: 'https://antd5.formilyjs.org', }, { title: '@formily/antd-mobile', path: 'https://antd-mobile.formilyjs.org', }, { title: '@formily/next', path: 'https://fusion.formilyjs.org', }, { title: '@formily/element', path: 'https://element.formilyjs.org', }, { title: '@formily/element-plus', path: 'https://element-plus.formilyjs.org', }, { title: '@formily/antdv', path: 'https://antdv.formilyjs.org', }, { title: '@formily/antdv-x3', path: 'https://antdv-x3.formilyjs.org', }, { title: '@formily/vant', path: 'https://vant.formilyjs.org', }, { title: '@formily/semi', path: 'https://semi.formilyjs.org', }, { title: '@formily/tdesign-react', path: 'https://tdesign-react.formilyjs.org/', }, { title: 'aliyun teamix', path: 'https://formily.dg.aliyun-inc.com/', }, { title: 'antd-formily-boost', path: 'https://github.com/fishedee/antd-formily-boost', }, ], }, { title: 'Tools', children: [ { title: 'Formily Designer', path: 'https://designable-antd.formilyjs.org/', }, { title: 'Designable', path: 'https://github.com/alibaba/designable', }, { title: 'Chrome Extension', path: 'https://chrome.google.com/webstore/detail/formily-devtools/kkocalmbfnplecdmbadaapgapdioecfm?hl=zh-CN', }, ], }, { title: 'Community', children: [ { title: 'Forum', path: 'https://github.com/alibaba/formily/discussions', }, { title: 'Zhihu', path: 'https://www.zhihu.com/column/uform' }, ], }, { title: 'Document@1.x', path: 'https://v1.formilyjs.org', }, { title: 'GITHUB', path: 'https://github.com/alibaba/formily', }, ], 'zh-CN': [ { title: '指南', path: '/zh-CN/guide', }, { title: '基础核心库', children: [ { title: '@formily/reactive', path: 'https://reactive.formilyjs.org/zh-CN', }, { title: '@formily/core', path: 'https://core.formilyjs.org/zh-CN', }, { title: '@formily/react', path: 'https://react.formilyjs.org/zh-CN', }, { title: '@formily/vue', path: 'https://vue.formilyjs.org', }, ], }, { title: '组件生态', children: [ { title: '@formily/antd', path: 'https://antd.formilyjs.org/zh-CN', }, { title: '@formily/antd-v5', path: 'https://antd5.formilyjs.org/zh-CN', }, { title: '@formily/antd-mobile', path: 'https://antd-mobile.formilyjs.org/zh-CN', }, { title: '@formily/next', path: 'https://fusion.formilyjs.org/zh-CN', }, { title: '@formily/element', path: 'https://element.formilyjs.org', }, { title: '@formily/element-plus', path: 'https://element-plus.formilyjs.org', }, { title: '@formily/antdv', path: 'https://antdv.formilyjs.org', }, { title: '@formily/vant', path: 'https://vant.formilyjs.org', }, { title: '@formily/semi', path: 'https://semi.formilyjs.org', }, { title: '@formily/tdesign-react', path: 'https://tdesign-react.formilyjs.org', }, { title: 'aliyun teamix', path: 'https://formily.dg.aliyun-inc.com', }, { title: 'antd-formily-boost', path: 'https://github.com/fishedee/antd-formily-boost', }, ], }, { title: '工具', children: [ { title: 'Formily 设计器', path: 'https://designable-antd.formilyjs.org/', }, { title: '通用搭建引擎', path: 'https://github.com/alibaba/designable', }, { title: 'Chrome扩展', path: 'https://chrome.google.com/webstore/detail/formily-devtools/kkocalmbfnplecdmbadaapgapdioecfm?hl=zh-CN', }, ], }, { title: '社区', children: [ { title: '论坛', path: 'https://github.com/alibaba/formily/discussions', }, { title: '知乎专栏', path: 'https://www.zhihu.com/column/uform' }, ], }, { title: '1.x文档', path: 'https://v1.formilyjs.org', }, { title: 'GITHUB', path: 'https://github.com/alibaba/formily', }, ], }, headScripts: [ ` function loadAd(){ var header = document.querySelector('.__dumi-default-layout-content .markdown h1') if(header && !header.querySelector('#_carbonads_js')){ var script = document.createElement('script') script.src = '//cdn.carbonads.com/carbon.js?serve=CEAICK3M&placement=formilyjsorg' script.id = '_carbonads_js' script.classList.add('head-ad') header.appendChild(script) } } var request = null var observer = new MutationObserver(function(){ cancelIdleCallback(request) request = requestIdleCallback(loadAd) }) document.addEventListener('DOMContentLoaded',function(){ loadAd() observer.observe( document.body, { childList:true, subtree:true } ) }) `, ], links: [ { rel: 'stylesheet', href: 'https://esm.sh/antd@4.x/dist/antd.css', }, ], styles: [ `.__dumi-default-navbar-logo{ height: 60px !important; width: 150px !important; padding-left:0 !important; color: transparent !important; } .__dumi-default-navbar{ padding: 0 28px !important; } .__dumi-default-layout-hero{ background-image: url(//img.alicdn.com/imgextra/i4/O1CN01ZcvS4e26XMsdsCkf9_!!6000000007671-2-tps-6001-4001.png); background-size: cover; background-repeat: no-repeat; padding: 120px 0 !important; } .__dumi-default-layout-hero h1{ color:#45124e !important; font-size:80px !important; padding-bottom: 30px !important; } .__dumi-default-dark-switch { display:none } nav a{ text-decoration: none !important; } #carbonads * { margin: initial; padding: initial; } #carbonads { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Helvetica, Arial, sans-serif; } #carbonads { display: flex; max-width: 330px; background-color: hsl(0, 0%, 98%); box-shadow: 0 1px 4px 1px hsla(0, 0%, 0%, 0.1); z-index: 100; float:right; } #carbonads a { color: inherit; text-decoration: none; } #carbonads a:hover { color: inherit; } #carbonads span { position: relative; display: block; overflow: hidden; } #carbonads .carbon-wrap { display: flex; } #carbonads .carbon-img { display: block; margin: 0; line-height: 1; } #carbonads .carbon-img img { display: block; } #carbonads .carbon-text { font-size: 13px; padding: 10px; margin-bottom: 16px; line-height: 1.5; text-align: left; } #carbonads .carbon-poweredby { display: block; padding: 6px 8px; background: #f1f1f2; text-align: center; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; font-size: 8px; line-height: 1; border-top-left-radius: 3px; position: absolute; bottom: 0; right: 0; } `, ], menus: { '/guide': [ { title: 'Introduction', path: '/guide', }, { title: 'How to learn Formily', path: '/guide/learn-formily', }, { title: 'Quick start', path: '/guide/quick-start', }, { title: 'V2 Upgrade Guide', path: '/guide/upgrade', }, { title: 'Contribution Guide', path: '/guide/contribution', }, { title: 'Form Builder Guide', path: '/guide/form-builder', }, { title: 'Issue Helper', path: '/guide/issue-helper', }, { title: 'Scenes', children: [ { title: 'Login&Signup', path: '/guide/scenes/login-register', }, { title: 'Query List', path: '/guide/scenes/query-list', }, { title: 'Edit&Details', path: '/guide/scenes/edit-detail', }, { title: 'Dialog&Drawer', path: '/guide/scenes/dialog-drawer', }, { title: 'Step Form', path: '/guide/scenes/step-form', }, { title: 'Tab Form', path: '/guide/scenes/tab-form', }, { title: 'More Scenes', path: '/guide/scenes/more', }, ], }, { title: 'Advanced Guide', children: [ { title: 'Form Validation', path: '/guide/advanced/validate', }, { title: 'Form Layout', path: '/guide/advanced/layout', }, { title: 'Asynchronous Data Sources', path: '/guide/advanced/async', }, { title: 'Form Controlled', path: '/guide/advanced/controlled', }, { title: 'Linkage Logic', path: '/guide/advanced/linkages', }, { title: 'Calculator', path: '/guide/advanced/calculator', }, { title: 'Custom Components', path: '/guide/advanced/custom', }, { title: 'Front-end and back-end data compatibility solution', path: '/guide/advanced/destructor', }, { title: 'Manage Business Logic', path: '/guide/advanced/business-logic', }, { title: 'Pack on demand', path: '/guide/advanced/build', }, ], }, ], '/zh-CN/guide': [ { title: '介绍', path: '/zh-CN/guide', }, { title: '如何学习Formily', path: '/zh-CN/guide/learn-formily', }, { title: '快速开始', path: '/zh-CN/guide/quick-start', }, { title: 'V2升级指南', path: '/zh-CN/guide/upgrade', }, { title: '贡献指南', path: '/zh-CN/guide/contribution', }, { title: '表单设计器开发指南', path: '/zh-CN/guide/form-builder', }, { title: '问题反馈', path: '/zh-CN/guide/issue-helper', }, { title: '场景案例', children: [ { title: '登录注册', path: '/zh-CN/guide/scenes/login-register', }, { title: '查询列表', path: '/zh-CN/guide/scenes/query-list', }, { title: '编辑详情', path: '/zh-CN/guide/scenes/edit-detail', }, { title: '弹窗与抽屉', path: '/zh-CN/guide/scenes/dialog-drawer', }, { title: '分步表单', path: '/zh-CN/guide/scenes/step-form', }, { title: '选项卡/手风琴表单', path: '/zh-CN/guide/scenes/tab-form', }, { title: '更多场景', path: '/zh-CN/guide/scenes/more', }, ], }, { title: '进阶指南', children: [ { title: '实现表单校验', path: '/zh-CN/guide/advanced/validate', }, { title: '实现表单布局', path: '/zh-CN/guide/advanced/layout', }, { title: '实现异步数据源', path: '/zh-CN/guide/advanced/async', }, { title: '实现表单受控', path: '/zh-CN/guide/advanced/controlled', }, { title: '实现联动逻辑', path: '/zh-CN/guide/advanced/linkages', }, { title: '实现联动计算器', path: '/zh-CN/guide/advanced/calculator', }, { title: '实现自定义组件', path: '/zh-CN/guide/advanced/custom', }, { title: '前后端数据差异兼容方案', path: '/zh-CN/guide/advanced/destructor', }, { title: '管理业务逻辑', path: '/zh-CN/guide/advanced/business-logic', }, { title: '按需打包', path: '/zh-CN/guide/advanced/build', }, ], }, ], }, } ================================================ FILE: .vscode/cspell.json ================================================ { "version": "0.1", "language": "en", "ignoreWords": [ "autorun", "mutators", "Formily", "formily", "untrack", "untracker", "untracked", "Untracking", "Unmount", "octokit", "repos", "alibaba", "Lifecycles", "antd", "Antd", "alifd", "Mixins", "builtins", "cascader", "Cascader", "middlewares" ] } ================================================ FILE: .yarnrc ================================================ registry "https://registry.yarnpkg.com" ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## v2.3.6(2025-05-15) ### No Change Log ## v2.3.5(2025-05-15) ### No Change Log ## v2.3.4(2025-05-14) ### :tada: Enhancements 1. [feat(core): 支持用户配置 validate 在哪些 pattern & display 下生效](https://github.com/alibaba/formily/commit/cea638cd) :point_right: ( [liuwei](https://github.com/liuwei) ) ### :bug: Bug Fixes 1. [fix(formily grid): add requestAnimationFrame to smooth grid digest (#4281)](https://github.com/alibaba/formily/commit/70475f77) :point_right: ( [CHEN GUODONG](https://github.com/CHEN GUODONG) ) 1. [fix: fix doc cdn link](https://github.com/alibaba/formily/commit/cabecfea) :point_right: ( [janrywang](https://github.com/janrywang) ) ### :blush: Other Changes 1. [chore: upgrade devtools](https://github.com/alibaba/formily/commit/29bbcf5d) :point_right: ( [janrywang](https://github.com/janrywang) ) ## v2.3.3(2025-03-31) ### :tada: Enhancements 1. [feat: slot (#4259)](https://github.com/alibaba/formily/commit/123d536b) :point_right: ( [NiceTooo](https://github.com/NiceTooo) ) 1. [feat(core): 支持用户配置 validate 在哪些 pattern & display 下生效 (#4211)](https://github.com/alibaba/formily/commit/39fdb681) :point_right: ( [liuwei](https://github.com/liuwei) ) ### :bug: Bug Fixes 1. [fix(antd): fix antd/next render error at React 19 (#4262)](https://github.com/alibaba/formily/commit/a0f3169a) :point_right: ( [ChaoGPT](https://github.com/ChaoGPT) ) 1. [fix: array-table/main.scss mixed-decls Deprecation warning on sass@1.77.7 + (#4195)](https://github.com/alibaba/formily/commit/d60f12db) :point_right: ( [Godpu](https://github.com/Godpu) ) ## v2.3.2(2024-07-18) ### :tada: Enhancements 1. [feat(shared): support BigNumber (#4182)](https://github.com/alibaba/formily/commit/b46b9b72) :point_right: ( [飝猫](https://github.com/飝猫) ) 1. [feat(vue): add default value for createSchemaField (#4123)](https://github.com/alibaba/formily/commit/0eeeb0c8) :point_right: ( [Din](https://github.com/Din) ) 1. [feat(antd): add form-item tooltip props support (#4144)](https://github.com/alibaba/formily/commit/b4524135) :point_right: ( [阿四](https://github.com/阿四) ) 1. [feat(json-schema): x-compile-omitted supports x-validator (#4072)](https://github.com/alibaba/formily/commit/4dc50bce) :point_right: ( [hyl](https://github.com/hyl) ) 1. [feat: improve checkers generics (#4075)](https://github.com/alibaba/formily/commit/d11080a4) :point_right: ( [幽閒](https://github.com/幽閒) ) ### :bug: Bug Fixes 1. [fix(chrome devtool): graph has symbol value, but devtool dont show (#4113)](https://github.com/alibaba/formily/commit/37d437d6) :point_right: ( [zhangxiaofan](https://github.com/zhangxiaofan) ) 1. [fix: wrong RadioGroup optionType value (#4083)](https://github.com/alibaba/formily/commit/9bb53573) :point_right: ( [shenshen](https://github.com/shenshen) ) 1. [fix(element): fix form-item extra bugs (#4125)](https://github.com/alibaba/formily/commit/6d55283a) :point_right: ( [James Smith](https://github.com/James Smith) ) 1. [fix(vue): fix vue-demi's dependencies version (#4085)](https://github.com/alibaba/formily/commit/cecf56d0) :point_right: ( [严浩](https://github.com/严浩) ) ### :memo: Documents Changes 1. [docs: fix deps (#4096)](https://github.com/alibaba/formily/commit/0932a11b) :point_right: ( [tkgkn](https://github.com/tkgkn) ) 1. [docs: update validate.md (#4156)](https://github.com/alibaba/formily/commit/9001580e) :point_right: ( [唯心](https://github.com/唯心) ) 1. [docs(react): fix typo (#4065)](https://github.com/alibaba/formily/commit/3fa68e69) :point_right: ( [stefango](https://github.com/stefango) ) ### :hammer_and_wrench: Update Workflow Scripts 1. [build(deps): bump axios from 0.18.1 to 1.7.2 (#4185)](https://github.com/alibaba/formily/commit/7d43e923) :point_right: ( [dependabot[bot]](https://github.com/dependabot[bot]) ) ### :blush: Other Changes 1. [chore: update ci.yml](https://github.com/alibaba/formily/commit/bf292edf) :point_right: ( [Janry](https://github.com/Janry) ) ## v2.3.1(2023-12-18) ### :tada: Enhancements 1. [feat: export getLocaleByPath (#4006)](https://github.com/alibaba/formily/commit/15d51bc9) :point_right: ( [uxuip](https://github.com/uxuip) ) 1. [feat: props recursion test case added (#4001)](https://github.com/alibaba/formily/commit/d8716ea9) :point_right: ( [Nice](https://github.com/Nice) ) 1. [feat(antd): support disable default behavior of built-in operations in ArrayBase (#3998)](https://github.com/alibaba/formily/commit/c90b1df1) :point_right: ( [whincwu](https://github.com/whincwu) ) ### :bug: Bug Fixes 1. [fix: fix vue2 array reactive bug (#4042)](https://github.com/alibaba/formily/commit/c94da3fe) :point_right: ( [yiyunwan](https://github.com/yiyunwan) ) 1. [fix: fix docs throw error](https://github.com/alibaba/formily/commit/588e5e52) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix: #3986 (#4003)](https://github.com/alibaba/formily/commit/39d64318) :point_right: ( [Lumdzeehol](https://github.com/Lumdzeehol) ) ### :hammer_and_wrench: Update Workflow Scripts 1. [build(deps-dev): bump axios from 0.23.0 to 1.6.0 (#4023)](https://github.com/alibaba/formily/commit/d6f827c1) :point_right: ( [dependabot[bot]](https://github.com/dependabot[bot]) ) 1. [build(deps): bump browserify-sign from 4.2.1 to 4.2.2 (#4010)](https://github.com/alibaba/formily/commit/e38de2a3) :point_right: ( [dependabot[bot]](https://github.com/dependabot[bot]) ) ### :blush: Other Changes 1. [chore: dumi updated for node 18+ (#4018)](https://github.com/alibaba/formily/commit/48c9968b) :point_right: ( [bob](https://github.com/bob) ) ## v2.3.0(2023-10-20) ### :tada: Enhancements 1. [feat: recursion field props recursion (#3966)](https://github.com/alibaba/formily/commit/72d533f6) :point_right: ( [Nice](https://github.com/Nice) ) ## v2.2.30(2023-10-18) ### :tada: Enhancements 1. [feat: Support labelFor props in (#3974)](https://github.com/alibaba/formily/commit/eeac65c2) :point_right: ( [小四](https://github.com/小四) ) ### :bug: Bug Fixes 1. [fix: remove unexpect label tag (#3996)](https://github.com/alibaba/formily/commit/e8707e9e) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(react): fix field unmounted but can not update right model (#3994)](https://github.com/alibaba/formily/commit/5207021f) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): @types/node pollution (#3944)](https://github.com/alibaba/formily/commit/bf0ab1c2) :point_right: ( [Dmitrii Kartashev](https://github.com/Dmitrii Kartashev) ) 1. [fix: close dialog should remove dom (#3963)](https://github.com/alibaba/formily/commit/97e7544d) :point_right: ( [Summer](https://github.com/Summer) ) 1. [fix: cancel button props (#3964)](https://github.com/alibaba/formily/commit/ac76b62f) :point_right: ( [Summer](https://github.com/Summer) ) ### :memo: Documents Changes 1. [docs: fix TS signature for dependencies field of SchemaReactions (#3938)](https://github.com/alibaba/formily/commit/9598d971) :point_right: ( [Andy](https://github.com/Andy) ) ### :rocket: Improve Performance 1. [perf(path): simplified code (#3933)](https://github.com/alibaba/formily/commit/8861ef58) :point_right: ( [huangcheng](https://github.com/huangcheng) ) ### :hammer_and_wrench: Update Workflow Scripts 1. [build(deps): bump @babel/traverse from 7.17.10 to 7.23.2 (#3993)](https://github.com/alibaba/formily/commit/421073e7) :point_right: ( [dependabot[bot]](https://github.com/dependabot[bot]) ) 1. [build(rollup): add externals with vue-demi (#3975)](https://github.com/alibaba/formily/commit/373dfed3) :point_right: ( [ttsimon](https://github.com/ttsimon) ) 1. [build(deps-dev): bump postcss from 7.0.39 to 8.4.31 (#3984)](https://github.com/alibaba/formily/commit/d51b4f28) :point_right: ( [dependabot[bot]](https://github.com/dependabot[bot]) ) ### :construction: Add/Update Test Cases 1. [test(core): test case for query field error issue and pr (#3995)](https://github.com/alibaba/formily/commit/31d2c8e3) :point_right: ( [ChaoGPT](https://github.com/ChaoGPT) ) ### :blush: Other Changes 1. [chore(grid): add support for peer dependencies of typescript@5.x (#3988)](https://github.com/alibaba/formily/commit/2fc23dd9) :point_right: ( [Sam Liu](https://github.com/Sam Liu) ) ## v2.2.29(2023-08-08) ### :bug: Bug Fixes 1. [fix(antd): componentProps lose responsiveness (#3917)](https://github.com/alibaba/formily/commit/6f8ec7dd) :point_right: ( [huangcheng](https://github.com/huangcheng) ) 1. [fix: money format regex (#3913)](https://github.com/alibaba/formily/commit/d37bce83) :point_right: ( [huangcheng](https://github.com/huangcheng) ) 1. [fix: reduce judgment (#3916)](https://github.com/alibaba/formily/commit/da52e7c1) :point_right: ( [huangcheng](https://github.com/huangcheng) ) 1. [fix: 🐛 antd array-table sortable infinite loop and cursor style (#3911)](https://github.com/alibaba/formily/commit/f254b399) :point_right: ( [ChaoGPT](https://github.com/ChaoGPT) ) ### :hammer_and_wrench: Update Workflow Scripts 1. [build(deps): bump word-wrap from 1.2.3 to 1.2.4 (#3908)](https://github.com/alibaba/formily/commit/bc90d7b2) :point_right: ( [dependabot[bot]](https://github.com/dependabot[bot]) ) ### :construction: Add/Update Test Cases 1. [test: remove unneed code (#3921)](https://github.com/alibaba/formily/commit/8508358d) :point_right: ( [huangcheng](https://github.com/huangcheng) ) 1. [test: add more test cases (#3922)](https://github.com/alibaba/formily/commit/f4223e8d) :point_right: ( [huangcheng](https://github.com/huangcheng) ) ## v2.2.27(2023-07-11) ### :tada: Enhancements 1. [feat(antd): support for hidden pagination of component array table (#3875)](https://github.com/alibaba/formily/commit/1e62b24e) :point_right: ( [xudihui](https://github.com/xudihui) ) 1. [feat: 🎸 antd sortable impl by dnd, replace react-sort-hoc (#3855)](https://github.com/alibaba/formily/commit/b3e270fc) :point_right: ( [ChaoGPT](https://github.com/ChaoGPT) ) ### :bug: Bug Fixes 1. [fix(vue): decorator event props not work in vue2 (#3884)](https://github.com/alibaba/formily/commit/8528067b) :point_right: ( [MeetzhDing](https://github.com/MeetzhDing) ) 1. [fix: ts type (#3888)](https://github.com/alibaba/formily/commit/2e59dc52) :point_right: ( [huangcheng](https://github.com/huangcheng) ) ### :memo: Documents Changes 1. [docs(schema): supplementary scope variable (#3869)](https://github.com/alibaba/formily/commit/061ad213) :point_right: ( [huangcheng](https://github.com/huangcheng) ) ### :rocket: Improve Performance 1. [perf: ⚡️ core array move method optimize, move to shared (#3863)](https://github.com/alibaba/formily/commit/3349815f) :point_right: ( [ChaoGPT](https://github.com/ChaoGPT) ) 1. [perf(schema): parse pattern only when needed (#3871)](https://github.com/alibaba/formily/commit/7f6fed07) :point_right: ( [huangcheng](https://github.com/huangcheng) ) ### :hammer_and_wrench: Update Workflow Scripts 1. [build(deps-dev): bump semver from 7.3.7 to 7.5.2 (#3868)](https://github.com/alibaba/formily/commit/f5128343) :point_right: ( [dependabot[bot]](https://github.com/dependabot[bot]) ) ### :construction: Add/Update Test Cases 1. [test: add message scope (#3886)](https://github.com/alibaba/formily/commit/0ac09f91) :point_right: ( [huangcheng](https://github.com/huangcheng) ) 1. [test: remove duplicate use cases (#3882)](https://github.com/alibaba/formily/commit/b9ab5097) :point_right: ( [huangcheng](https://github.com/huangcheng) ) 1. [test: rename (#3885)](https://github.com/alibaba/formily/commit/c8661b1c) :point_right: ( [huangcheng](https://github.com/huangcheng) ) ### :blush: Other Changes 1. [style: simplify get value (#3887)](https://github.com/alibaba/formily/commit/287fdadc) :point_right: ( [huangcheng](https://github.com/huangcheng) ) ## v2.2.26(2023-06-21) ### :bug: Bug Fixes 1. [fix(core): onInput not ignore when currentTarget is undefined (#3862)](https://github.com/alibaba/formily/commit/1e490616) :point_right: ( [frehkxu](https://github.com/frehkxu) ) ### :construction: Add/Update Test Cases 1. [test(core): improve array test case (#3861)](https://github.com/alibaba/formily/commit/44f08106) :point_right: ( [{ Chao }](https://github.com/{ Chao }) ) ## v2.2.25(2023-06-16) ### :tada: Enhancements 1. [feat(json-schema): add IScopeContext easy to expand scope types (#3821)](https://github.com/alibaba/formily/commit/cc6a5fdf) :point_right: ( [yiyunwan](https://github.com/yiyunwan) ) ### :bug: Bug Fixes 1. [fix(core): onInput ignore HTMLInputEvent propagation (#3856)](https://github.com/alibaba/formily/commit/b3edf2d1) :point_right: ( [frehkxu](https://github.com/frehkxu) ) 1. [fix(doc): fix url langue (#3844)](https://github.com/alibaba/formily/commit/77d7e586) :point_right: ( [微笑](https://github.com/微笑) ) 1. [fix: fix array items sortable (#3836)](https://github.com/alibaba/formily/commit/7477e86a) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(json-schema): use `string & {}` instead of string to keep Literal Type for ISchema (#3835)](https://github.com/alibaba/formily/commit/798fde79) :point_right: ( [yiyunwan](https://github.com/yiyunwan) ) 1. [fix: fix react typings (#3831)](https://github.com/alibaba/formily/commit/2c41e3ef) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: change mapProps consistent with react (#3819)](https://github.com/alibaba/formily/commit/6a3fe6b1) :point_right: ( [yiyunwan](https://github.com/yiyunwan) ) ### :memo: Documents Changes 1. [doc: fix typo (#3826)](https://github.com/alibaba/formily/commit/84740029) :point_right: ( [godf](https://github.com/godf) ) ## v2.2.24(2023-05-15) ### :bug: Bug Fixes 1. [fix: require react dom #3704 (#3818)](https://github.com/alibaba/formily/commit/c3d028d6) :point_right: ( [gwsbhqt](https://github.com/gwsbhqt) ) ### :blush: Other Changes 1. [chore: update json-schema's peerDependencies (#3817)](https://github.com/alibaba/formily/commit/4050621f) :point_right: ( [严浩](https://github.com/严浩) ) ## v2.2.23(2023-05-05) ### :bug: Bug Fixes 1. [fix: fix requiredMark hidden (#3796)](https://github.com/alibaba/formily/commit/0d187111) :point_right: ( [Alex](https://github.com/Alex) ) 1. [fix(antd/next): fix array-collapse onAdd function nullable issue (#3795)](https://github.com/alibaba/formily/commit/39fac2b5) :point_right: ( [戣蓦](https://github.com/戣蓦) ) ## v2.2.22(2023-04-12) ### :bug: Bug Fixes 1. [fix(antd): add helperContainer to antd array-items (#3780)](https://github.com/alibaba/formily/commit/757b466d) :point_right: ( [linxianxi](https://github.com/linxianxi) ) 1. [fix: field hidden with null value (#3783)](https://github.com/alibaba/formily/commit/f8c2040f) :point_right: ( [gwsbhqt](https://github.com/gwsbhqt) ) 1. [fix(core): add types to form submit (#3775)](https://github.com/alibaba/formily/commit/b458efdb) :point_right: ( [Dmitrii Kartashev](https://github.com/Dmitrii Kartashev) ) ## v2.2.21(2023-03-21) ### :tada: Enhancements 1. [feat(antd): support ReactNode of ArrayCollapse header](https://github.com/alibaba/formily/commit/1450f60d) :point_right: ( [coder_curry](https://github.com/coder_curry) ) 1. [feat(antd): support array-base operator title display (#3646)](https://github.com/alibaba/formily/commit/3ba48209) :point_right: ( [Jehu](https://github.com/Jehu) ) ### :bug: Bug Fixes 1. [fix(core): fix patchFieldStates update problem (#3763)](https://github.com/alibaba/formily/commit/eca5a7e5) :point_right: ( [zeqing](https://github.com/zeqing) ) ## v2.2.20(2023-02-28) ### :tada: Enhancements 1. [feat(antd): FormItem adds more attribute configuration (#3727)](https://github.com/alibaba/formily/commit/71be0a57) :point_right: ( [Alex](https://github.com/Alex) ) ### :bug: Bug Fixes 1. [fix(antd): fix locale import path](https://github.com/alibaba/formily/commit/06a64935) :point_right: ( [janrywang](https://github.com/janrywang) ) ### :memo: Documents Changes 1. [docs: add antd5 links (#3728)](https://github.com/alibaba/formily/commit/7e34079d) :point_right: ( [yiyunwan](https://github.com/yiyunwan) ) ### :blush: Other Changes 1. [chore(antd/next): fix getObjectParent issue in arrayTable component (#3741)](https://github.com/alibaba/formily/commit/21cff368) :point_right: ( [zeqing](https://github.com/zeqing) ) 1. [chore: fixed build ci node version as 16 (#3732)](https://github.com/alibaba/formily/commit/cfca08d5) :point_right: ( [gwsbhqt](https://github.com/gwsbhqt) ) ## v2.2.19(2023-02-17) ### :tada: Enhancements 1. [feat(core): support record api (#3711)](https://github.com/alibaba/formily/commit/d4bb96c4) :point_right: ( [Janry](https://github.com/Janry) ) ### :bug: Bug Fixes 1. [fix(antd/next): remove RecordScope (#3726)](https://github.com/alibaba/formily/commit/29c347a0) :point_right: ( [zeqing](https://github.com/zeqing) ) ### :memo: Documents Changes 1. [docs(linkage): change the controlled is display when initialized & dynamic controlled field (#3717)](https://github.com/alibaba/formily/commit/75d36d29) :point_right: ( [xbsheng](https://github.com/xbsheng) ) 1. [docs(core): change the default value of the hidden parameter of createForm function (#3707)](https://github.com/alibaba/formily/commit/5f95cdf1) :point_right: ( [xbsheng](https://github.com/xbsheng) ) ## v2.2.18(2023-02-07) ### :bug: Bug Fixes 1. [fix(next): fix ArrayCards and ArrayTable props (#3701)](https://github.com/alibaba/formily/commit/0367c51b) :point_right: ( [常泽清](https://github.com/常泽清) ) 1. [fix(element): fix opened name writing error (#3695)](https://github.com/alibaba/formily/commit/c616bf15) :point_right: ( [LiangZhiLin](https://github.com/LiangZhiLin) ) ### :hammer_and_wrench: Update Workflow Scripts 1. [build(deps): bump ua-parser-js from 0.7.31 to 0.7.33 (#3682)](https://github.com/alibaba/formily/commit/9fe0520b) :point_right: ( [dependabot[bot]](https://github.com/dependabot[bot]) ) ## v2.2.17(2023-01-18) ### :bug: Bug Fixes 1. [fix(vue): fix view may not update when states change. (#3680)](https://github.com/alibaba/formily/commit/b221d3e0) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [fix: fix a compatible problem when using ios10.x (#3677)](https://github.com/alibaba/formily/commit/2fce092c) :point_right: ( [ZSQCola](https://github.com/ZSQCola) ) 1. [fix(reactive-react): fix reactive useForceUpdate uncounted strategy (#3668)](https://github.com/alibaba/formily/commit/0bf551eb) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(element): fix Checkbox.Group's change event failure (#3667)](https://github.com/alibaba/formily/commit/2bfa40c1) :point_right: ( [LiangZhiLin](https://github.com/LiangZhiLin) ) ### :construction: Add/Update Test Cases 1. [test: adding test case (#3652)](https://github.com/alibaba/formily/commit/f54ccfbc) :point_right: ( [Lumdzeehol](https://github.com/Lumdzeehol) ) ## v2.2.16(2022-12-29) ### :bug: Bug Fixes 1. [fix(vue): fix default slot invalid bug when not pass decorator (#3638)](https://github.com/alibaba/formily/commit/29b799c3) :point_right: ( [frehkxu](https://github.com/frehkxu) ) ### :rose: Improve code quality 1. [refactor(core): revert initial values check logic (#3642)](https://github.com/alibaba/formily/commit/42be1937) :point_right: ( [Janry](https://github.com/Janry) ) ## v2.2.15(2022-12-16) ### :bug: Bug Fixes 1. [fix(antd): fix array tabs waring (#3629)](https://github.com/alibaba/formily/commit/a7e80893) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(vue): keep origin SlotProps pass in ReactiveField (#3623)](https://github.com/alibaba/formily/commit/200af68e) :point_right: ( [frehkxu](https://github.com/frehkxu) ) 1. [fix(vue): fix view may not update when states change. (#3619)](https://github.com/alibaba/formily/commit/82ca678d) :point_right: ( [月落音阑](https://github.com/月落音阑) ) ### :hammer_and_wrench: Update Workflow Scripts 1. [build(deps): bump decode-uri-component from 0.2.0 to 0.2.2 (#3607)](https://github.com/alibaba/formily/commit/e075e4e3) :point_right: ( [dependabot[bot]](https://github.com/dependabot[bot]) ) ## v2.2.14(2022-12-05) ### :bug: Bug Fixes 1. [fix(react): fix react-dom deps (#3591)](https://github.com/alibaba/formily/commit/75b1e8b9) :point_right: ( [常泽清](https://github.com/常泽清) ) 1. [fix(core): fix initValues when values is empty Array or Object (#3583)](https://github.com/alibaba/formily/commit/c538d0d2) :point_right: ( [yiyunwan](https://github.com/yiyunwan) ) 1. [fix(antd/next): fix checkbox and radio can not trigger user onChange (#3585)](https://github.com/alibaba/formily/commit/e6454be2) :point_right: ( [Janry](https://github.com/Janry) ) ### :construction: Add/Update Test Cases 1. [test: improve antd coverage (#3586)](https://github.com/alibaba/formily/commit/8602fe2b) :point_right: ( [Lumdzeehol](https://github.com/Lumdzeehol) ) ### :blush: Other Changes 1. [chore: update benchmark template static js url](https://github.com/alibaba/formily/commit/836d1ce3) :point_right: ( [janrywang](https://github.com/janrywang) ) ## v2.2.13(2022-11-28) ### :bug: Bug Fixes 1. [fix(core): take result is possible to be undefined (#3562)](https://github.com/alibaba/formily/commit/cda62b90) :point_right: ( [戣蓦](https://github.com/戣蓦) ) 1. [fix(path): fix typo of readIgnoreString (#3545)](https://github.com/alibaba/formily/commit/34964f26) :point_right: ( [huangcheng](https://github.com/huangcheng) ) ### :memo: Documents Changes 1. [doc: typo in architecture.zh-CN.md (#3569)](https://github.com/alibaba/formily/commit/d1ee69b2) :point_right: ( [yeehone](https://github.com/yeehone) ) 1. [docs(core): correct properties spelling (#3555)](https://github.com/alibaba/formily/commit/b0206023) :point_right: ( [Lumdzeehol](https://github.com/Lumdzeehol) ) 1. [docs(reactive): correct typos (#3532)](https://github.com/alibaba/formily/commit/41d1720b) :point_right: ( [liuwei1025](https://github.com/liuwei1025) ) ### :rocket: Improve Performance 1. [perf: improve performance of ArrayTable (#3574)](https://github.com/alibaba/formily/commit/0c0c3b06) :point_right: ( [Janry](https://github.com/Janry) ) 1. [perf: parentLTok should not after dbStartTok (#3534)](https://github.com/alibaba/formily/commit/48fd1842) :point_right: ( [huangcheng](https://github.com/huangcheng) ) ### :blush: Other Changes 1. [chore: update antd.css version](https://github.com/alibaba/formily/commit/324986c2) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [style: rename to camelCase (#3533)](https://github.com/alibaba/formily/commit/a7c4627d) :point_right: ( [huangcheng](https://github.com/huangcheng) ) ## v2.2.12(2022-11-11) ### :bug: Bug Fixes 1. [fix(core): fix setValues/setInitialValues will change ref (#3529)](https://github.com/alibaba/formily/commit/886144fa) :point_right: ( [Janry](https://github.com/Janry) ) ### :memo: Documents Changes 1. [docs: add formily-antd-mobile doc link (#3527)](https://github.com/alibaba/formily/commit/c658cb91) :point_right: ( [Dark](https://github.com/Dark) ) ## v2.2.11(2022-11-07) ### :bug: Bug Fixes 1. [fix(element): remove Space gap when child is hidden and attrs pass children (#3526)](https://github.com/alibaba/formily/commit/8bcd51fe) :point_right: ( [frehkxu](https://github.com/frehkxu) ) 1. [fix(reactive-react): fix reactive track failed in suspense mode (#3525)](https://github.com/alibaba/formily/commit/5ab10b48) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): fix field destructor name will cause stack overflow (#3524)](https://github.com/alibaba/formily/commit/7306677b) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: ts error in test (#3516)](https://github.com/alibaba/formily/commit/f35e5dfa) :point_right: ( [huangcheng](https://github.com/huangcheng) ) 1. [fix: callback will not be executed until it is a function (#3511)](https://github.com/alibaba/formily/commit/0c969140) :point_right: ( [huangcheng](https://github.com/huangcheng) ) 1. [fix(element): fix vue resolve (#3496)](https://github.com/alibaba/formily/commit/f347a7c0) :point_right: ( [Muyao](https://github.com/Muyao) ) ### :rocket: Improve Performance 1. [perf(path): judge lastToken when needed (#3522)](https://github.com/alibaba/formily/commit/0ef61df0) :point_right: ( [huangcheng](https://github.com/huangcheng) ) 1. [perf: lowerCase when necessary (#3492)](https://github.com/alibaba/formily/commit/4379ad0b) :point_right: ( [huangcheng](https://github.com/huangcheng) ) ### :construction: Add/Update Test Cases 1. [test: add setTimeout default value (#3514)](https://github.com/alibaba/formily/commit/618307b9) :point_right: ( [huangcheng](https://github.com/huangcheng) ) ### :blush: Other Changes 1. [chore(core): improve allowAssignDefaultValue (#3523)](https://github.com/alibaba/formily/commit/666b867a) :point_right: ( [huangcheng](https://github.com/huangcheng) ) 1. [style: simplify code (#3506)](https://github.com/alibaba/formily/commit/d6a894f0) :point_right: ( [huangcheng](https://github.com/huangcheng) ) ## v2.2.10(2022-10-26) ### :bug: Bug Fixes 1. [fix(react): fix throw react-dom error in react-native (#3491)](https://github.com/alibaba/formily/commit/00141fe7) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): fix value filtered from none-hidden #3477 (#3481)](https://github.com/alibaba/formily/commit/617717cb) :point_right: ( [Lumdzeehol](https://github.com/Lumdzeehol) ) ### :memo: Documents Changes 1. [docs: update readme](https://github.com/alibaba/formily/commit/e1539bbf) :point_right: ( [janrywang](https://github.com/janrywang) ) ### :blush: Other Changes 1. [chore(docs): remove linkage of adjacent in initiative scene (#3488)](https://github.com/alibaba/formily/commit/3033416c) :point_right: ( [kesiyuan](https://github.com/kesiyuan) ) ## v2.2.9(2022-10-19) ### :bug: Bug Fixes 1. [fix(core): fix initial value is filtered when the field is hidden (#3471)](https://github.com/alibaba/formily/commit/47ee4786) :point_right: ( [Janry](https://github.com/Janry) ) ## v2.2.8(2022-10-18) ### :tada: Enhancements 1. [feat(core): support auto clean field value with visible false (#3452)](https://github.com/alibaba/formily/commit/2c9b332f) :point_right: ( [Janry](https://github.com/Janry) ) ### :bug: Bug Fixes 1. [fix(element): remove useless code in demo's guide (#3463)](https://github.com/alibaba/formily/commit/3a3db058) :point_right: ( [guaqiu](https://github.com/guaqiu) ) 1. [fix(antd): fix ArrayTable WrapperComp deps missing (#3457)](https://github.com/alibaba/formily/commit/a382a18e) :point_right: ( [Lumdzeehol](https://github.com/Lumdzeehol) ) ### :hammer_and_wrench: Update Workflow Scripts 1. [build: fix duplicate packaging with @formily/json-schema (#3467)](https://github.com/alibaba/formily/commit/6d768b0a) :point_right: ( [Grapedge](https://github.com/Grapedge) ) ### :blush: Other Changes 1. [chore(docs): improve the translation and example of the login and registration case (#3455)](https://github.com/alibaba/formily/commit/95c295b5) :point_right: ( [WD](https://github.com/WD) ) 1. [chore(devtools): change dependencies version (#3448)](https://github.com/alibaba/formily/commit/9809637b) :point_right: ( [fuzi](https://github.com/fuzi) ) ## v2.2.7(2022-10-11) ### :bug: Bug Fixes 1. [fix(next): fix cascader preview text can not shown data (#3447)](https://github.com/alibaba/formily/commit/67125bd6) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(docs): fix links in chinese documentation (#3438)](https://github.com/alibaba/formily/commit/382cd177) :point_right: ( [WD](https://github.com/WD) ) 1. [fix(core): indexes need exclude incomplete number (#3437)](https://github.com/alibaba/formily/commit/d328bb3a) :point_right: ( [frehkxu](https://github.com/frehkxu) ) ## v2.2.6(2022-09-30) ### :bug: Bug Fixes 1. [fix(next/antd): chore formatMomentValue (#3432)](https://github.com/alibaba/formily/commit/ed386f4d) :point_right: ( [danyue](https://github.com/danyue) ) 1. [fix(core): fix clearFormGraph unexpect behaviors with action annotation (#3431)](https://github.com/alibaba/formily/commit/e077e6c9) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): fix void field child field reactions not work in some cases (#3415)](https://github.com/alibaba/formily/commit/f05cb6b3) :point_right: ( [coolbob](https://github.com/coolbob) ) 1. [fix(next/antd): fix SelectTable Template literals invalid && fix FormItem classname error (#3413)](https://github.com/alibaba/formily/commit/b3d3eb7b) :point_right: ( [Lyca](https://github.com/Lyca) ) ### :rocket: Improve Performance 1. [perf(reactive): improve reactive performance (#3430)](https://github.com/alibaba/formily/commit/5196f452) :point_right: ( [Janry](https://github.com/Janry) ) ## v2.2.5(2022-09-20) ### :bug: Bug Fixes 1. [fix(next/antd): fix moment timestamp (#3395)](https://github.com/alibaba/formily/commit/2054c10f) :point_right: ( [danyue](https://github.com/danyue) ) ### :memo: Documents Changes 1. [docs(antd): correct the usage type of the password component (#3406)](https://github.com/alibaba/formily/commit/03719d12) :point_right: ( [WD](https://github.com/WD) ) ## v2.2.4(2022-09-12) ### :tada: Enhancements 1. [feat(core): support field inject/invoke actions api (#3389)](https://github.com/alibaba/formily/commit/07593760) :point_right: ( [Janry](https://github.com/Janry) ) ### :bug: Bug Fixes 1. [fix(antd): fix ArrayTabs warning after antd4.23.0 (#3387)](https://github.com/alibaba/formily/commit/f6347cc4) :point_right: ( [Lumdzeehol](https://github.com/Lumdzeehol) ) 1. [fix(antd/next): onChange does not work when no formTab instance is passed (#3388)](https://github.com/alibaba/formily/commit/e4ba3ea1) :point_right: ( [Dark](https://github.com/Dark) ) 1. [fix(antd/next): fix array base use record null error (#3380)](https://github.com/alibaba/formily/commit/053e0f0c) :point_right: ( [{ Chao }](https://github.com/{ Chao }) ) ## v2.2.3(2022-09-07) ### :tada: Enhancements 1. [feat(docs): add antdv-x3 doc link (#3361)](https://github.com/alibaba/formily/commit/af1484e3) :point_right: ( [zhouxinyong](https://github.com/zhouxinyong) ) ### :bug: Bug Fixes 1. [fix(vue): fix useFormEffects not reactive when form change (#3371)](https://github.com/alibaba/formily/commit/b8b4c510) :point_right: ( [frehkxu](https://github.com/frehkxu) ) 1. [fix(element): update type of IFormDialog (#3360)](https://github.com/alibaba/formily/commit/9233d5ec) :point_right: ( [椿楸冥灵](https://github.com/椿楸冥灵) ) ## v2.2.2(2022-08-30) ### :bug: Bug Fixes 1. [fix(antd/next): fix array base record ref data is not newest in expression (#3358)](https://github.com/alibaba/formily/commit/35cd1431) :point_right: ( [Janry](https://github.com/Janry) ) ## v2.2.1(2022-08-22) ### :tada: Enhancements 1. [feat(element): compat vue2.7 (#3350)](https://github.com/alibaba/formily/commit/da94164e) :point_right: ( [Muyao](https://github.com/Muyao) ) ### :bug: Bug Fixes 1. [fix(devtools): Does not render correctly when title is an object (#3340)](https://github.com/alibaba/formily/commit/4fb83052) :point_right: ( [Dark](https://github.com/Dark) ) ## v2.2.0(2022-08-11) ### :tada: Enhancements 1. [feat(core): lock setValue/setInitialValue behavior to untrack (#3331)](https://github.com/alibaba/formily/commit/ff1d403a) :point_right: ( [Janry](https://github.com/Janry) ) ## v2.1.13(2022-08-11) ### :tada: Enhancements 1. [feat(vue): support vue2.7](https://github.com/alibaba/formily/commit/6af972d1) :point_right: ( [MisicDemone](https://github.com/MisicDemone) ) ### :bug: Bug Fixes 1. [fix(next): fix time format moment (#3330)](https://github.com/alibaba/formily/commit/8b7bbd28) :point_right: ( [danyue](https://github.com/danyue) ) 1. [fix(core): fix form initialValues not work after array field removed elements (#3324)](https://github.com/alibaba/formily/commit/f7e1b7d8) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(antd/next/element): fix ArrayCards-like component errors with inline component (#3321)](https://github.com/alibaba/formily/commit/aa8ed99e) :point_right: ( [Lumdzeehol](https://github.com/Lumdzeehol) ) 1. [fix(antd/next): fix array base not work with pure jsx (#3317)](https://github.com/alibaba/formily/commit/acd6533d) :point_right: ( [Janry](https://github.com/Janry) ) ### :memo: Documents Changes 1. [docs(antd): add close command demo (#3312)](https://github.com/alibaba/formily/commit/e718f2b2) :point_right: ( [moon](https://github.com/moon) ) ## v2.1.12(2022-08-04) ### :bug: Bug Fixes 1. [fix(path): fix getIn unexpect value with null (#3305)](https://github.com/alibaba/formily/commit/140aa524) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(react): update type of IRecursionFieldProps (#3291)](https://github.com/alibaba/formily/commit/42fcc28e) :point_right: ( [Elinia](https://github.com/Elinia) ) 1. [fix(vue): fix reactions not work correctly in schema field (#3287)](https://github.com/alibaba/formily/commit/551ad0f2) :point_right: ( [月落音阑](https://github.com/月落音阑) ) ### :blush: Other Changes 1. [chore(deps): bump terser from 4.8.0 to 4.8.1 (#3290)](https://github.com/alibaba/formily/commit/1314087a) :point_right: ( [dependabot[bot]](https://github.com/dependabot[bot]) ) ## v2.1.11(2022-07-19) ### :tada: Enhancements 1. [feat(antd/next): improve copy action ui (#3263)](https://github.com/alibaba/formily/commit/fd7b5f53) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(antd/next): add NumberPicker PreviewText (#3237)](https://github.com/alibaba/formily/commit/bfef03a6) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(antd): drawer support extra (#3213)](https://github.com/alibaba/formily/commit/ef218b9c) :point_right: ( [shaaaaaaaa](https://github.com/shaaaaaaaa) ) 1. [feat(element): add element style import description (#3188)](https://github.com/alibaba/formily/commit/4bca1108) :point_right: ( [KKandLL-Forever](https://github.com/KKandLL-Forever) ) 1. [feat(react): adjust component recognition priority (#3180)](https://github.com/alibaba/formily/commit/bf4e035c) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(json-schema): support x-compile-omitted attribute (#3145)](https://github.com/alibaba/formily/commit/c8485c0e) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(react): support dynamic scope (#3143)](https://github.com/alibaba/formily/commit/92945b0b) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(antd/next): react-sticky-box upgraded to 1.x (#3125)](https://github.com/alibaba/formily/commit/78479704) :point_right: ( [蜘蛛侠](https://github.com/蜘蛛侠) ) 1. [feat(core): support disable forceClear to clearFormGraph (#3122)](https://github.com/alibaba/formily/commit/d24168bb) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(core): improve readPretty restrict (#3105)](https://github.com/alibaba/formily/commit/67a555c3) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(docs): add element-plus doc link (#3086)](https://github.com/alibaba/formily/commit/4f13df32) :point_right: ( [Stephen Woo](https://github.com/Stephen Woo) ) 1. [feat(next): add TimPicker2 component (#3082)](https://github.com/alibaba/formily/commit/657fc298) :point_right: ( [yiye](https://github.com/yiye) ) 1. [feat(next/antd): fix SelectTable optionAsValue and add disabled props in ArrayBase icon button (#3072)](https://github.com/alibaba/formily/commit/43d0faa9) :point_right: ( [Lyca](https://github.com/Lyca) ) 1. [feat(antd/next): add style generator (#3053)](https://github.com/alibaba/formily/commit/fddd591a) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(next/antd): fix selected bug3 by search in SelectTable (#2927)](https://github.com/alibaba/formily/commit/bc943de3) :point_right: ( [Lyca](https://github.com/Lyca) ) 1. [feat(vue): improve performance of mapProps (#2909)](https://github.com/alibaba/formily/commit/5ca0456a) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [feat(vue): support x-slot (#2892)](https://github.com/alibaba/formily/commit/9e268aa8) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [feat(vue): improve expression scope (#2875)](https://github.com/alibaba/formily/commit/22a3d2bf) :point_right: ( [frehkxu](https://github.com/frehkxu) ) 1. [feat(antd/next): use full text matcha for SelectTable nd remove filterOptionProp](https://github.com/alibaba/formily/commit/127e0c7f) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(next): support checkStrictly props in SelectTable (#2824)](https://github.com/alibaba/formily/commit/feba6375) :point_right: ( [Lyca](https://github.com/Lyca) ) 1. [feat(antd/next): support 16427form scope with Form](https://github.com/alibaba/formily/commit/09a597f7) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(core): support index/indexes properties (#2769)](https://github.com/alibaba/formily/commit/36143ef0) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(next/antd/vue): add useResponsiveFormLayout fault tolerance and FormItem useOverflow update (#2707)](https://github.com/alibaba/formily/commit/98a544ac) :point_right: ( [Lyca](https://github.com/Lyca) ) 1. [feat(devtools): support select node to bind $vm with console (#2682)](https://github.com/alibaba/formily/commit/80ef0792) :point_right: ( [fuzi](https://github.com/fuzi) ) 1. [feat(reactive-vue): add observer option scheduler (#2672)](https://github.com/alibaba/formily/commit/ca55e484) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [feat(antd-component): provide `getPopupContainer` prop for `FormItem` when use popover feedback (#2619)](https://github.com/alibaba/formily/commit/69ff01cb) :point_right: ( [小翼](https://github.com/小翼) ) 1. [feat(element): support createFormGrid api (#2510)](https://github.com/alibaba/formily/commit/cadd63b3) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [feat(core): add setData & setContent of field models (#2478)](https://github.com/alibaba/formily/commit/f6d31032) :point_right: ( [DivX.Hu](https://github.com/DivX.Hu) ) 1. [feat(vue): add injectionCleaner to FormProvider (#2449)](https://github.com/alibaba/formily/commit/56c36468) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [feat(grid): support onDigest](https://github.com/alibaba/formily/commit/3c857a24) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(element): add ArrayTable/ArrayCollapse/ArrayTabs event (#2365)](https://github.com/alibaba/formily/commit/d54cdb8b) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [feat(next/antd): support breakpoints for FormLayout (#2336)](https://github.com/alibaba/formily/commit/c894adc8) :point_right: ( [Lyca](https://github.com/Lyca) ) 1. [feat(element): support useRecord for ArrayBase (#2313)](https://github.com/alibaba/formily/commit/74594663) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [feat(next): fix FormDialog footerActions/okProps/cancelProps (#2312)](https://github.com/alibaba/formily/commit/e2fe6734) :point_right: ( [Lyca](https://github.com/Lyca) ) 1. [feat(json-schema): support extend schema property (#2284)](https://github.com/alibaba/formily/commit/67ca5e58) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(react): fix schema x-component-props children invalid (#2160)](https://github.com/alibaba/formily/commit/7dc9d9ff) :point_right: ( [Lyca](https://github.com/Lyca) ) 1. [feat(reactive): support skip toJS with markRaw](https://github.com/alibaba/formily/commit/5d245511) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(element): radio/checkbox add optionType prop (#2114)](https://github.com/alibaba/formily/commit/54072a67) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [feat(next/antd): add tooltipIcon props to FormLayout & FormItem (#2085)](https://github.com/alibaba/formily/commit/1a817918) :point_right: ( [Lyca](https://github.com/Lyca) ) 1. [feat(designable): add icons for drag source](https://github.com/alibaba/formily/commit/8c14fa6e) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(designable): support componentsIcon/componentsSourceIcon](https://github.com/alibaba/formily/commit/5255e0da) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(core): support field destroy method (#1895)](https://github.com/alibaba/formily/commit/52457e10) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(antd/next): improve FormDialog/FormDrawer typings and api (#1886)](https://github.com/alibaba/formily/commit/e3d7d264) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(setters): add ValidatorSetter (#1885)](https://github.com/alibaba/formily/commit/4e2203e7) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(element): update array-table component & doc (#1862)](https://github.com/alibaba/formily/commit/f98129a9) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [feat(shared): add middleware function (#1858)](https://github.com/alibaba/formily/commit/e54525da) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(packages): add react 18 test cases (#1834)](https://github.com/alibaba/formily/commit/aa792203) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(reactive): support autorun.memo/autorun.effect (#1819)](https://github.com/alibaba/formily/commit/e43dda6a) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(project): support bundle dts (#1796)](https://github.com/alibaba/formily/commit/5f8c1879) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(form-dialog): add form dialog and form drawer oncancel return value (#1791)](https://github.com/alibaba/formily/commit/f08de0dc) :point_right: ( [张威](https://github.com/张威) ) 1. [feat(gitignore): support ignore .history directory (#1792)](https://github.com/alibaba/formily/commit/0035e61c) :point_right: ( [张威](https://github.com/张威) ) 1. [feat(antd): transfer compat label/value](https://github.com/alibaba/formily/commit/2be3a10d) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(core): skip validate when parent.visible is equal hidden/none (#1712)](https://github.com/alibaba/formily/commit/0076ef7d) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(designable-antd): support markup schema view](https://github.com/alibaba/formily/commit/2acb1033) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(vue): add components prop for schema-field (#1686)](https://github.com/alibaba/formily/commit/e9dec48f) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [feat(antd): improve Submit API (#1640)](https://github.com/alibaba/formily/commit/6b33ec9c) :point_right: ( [后浪](https://github.com/后浪) ) 1. [feat(reactive-react): support Observer Component like vue slot](https://github.com/alibaba/formily/commit/a49ee263) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(antd/next): add Form and Submit components submitFailed callback events (#1597)](https://github.com/alibaba/formily/commit/2517f807) :point_right: ( [后浪](https://github.com/后浪) ) 1. [feat(antd/next): add tree-shaking support for antd/next (#1544)](https://github.com/alibaba/formily/commit/6835f6d2) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [feat(core): support more types for dataSource](https://github.com/alibaba/formily/commit/6715555e) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(next): support form drawer get context from fusion (#1511)](https://github.com/alibaba/formily/commit/7fce306c) :point_right: ( [王大白](https://github.com/王大白) ) 1. [feat(next): add fusion multiple lang of validator (#1504)](https://github.com/alibaba/formily/commit/2ca07e7a) :point_right: ( [王大白](https://github.com/王大白) ) 1. [feat(antd): support defaultOpenPanelCount for ArrayCollapse (#1505)](https://github.com/alibaba/formily/commit/e9e3f74e) :point_right: ( [Lind](https://github.com/Lind) ) 1. [feat(next): add stopPropagation to array-base events](https://github.com/alibaba/formily/commit/276a5fbb) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(core): remove property of form values with undefined value (#1495)](https://github.com/alibaba/formily/commit/296eae47) :point_right: ( [小黄黄](https://github.com/小黄黄) ) 1. [feat(core): support value change trigger validate](https://github.com/alibaba/formily/commit/0473017a) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(core): add extra strategy for merge form value (#1448)](https://github.com/alibaba/formily/commit/0b5606d1) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [feat(vue): improve typings and docs(#1433)](https://github.com/alibaba/formily/commit/fc5d6650) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [feat(.md): Form => FormLayout (#1427)](https://github.com/alibaba/formily/commit/2501e72f) :point_right: ( [Lyca](https://github.com/Lyca) ) 1. [feat(next): improve styles](https://github.com/alibaba/formily/commit/bce90958) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat: url regexp support /?a=1 and ?a=1 (#1374)](https://github.com/alibaba/formily/commit/4fed6246) :point_right: ( [No.96](https://github.com/No.96) ) 1. [feat(shared): remove isValidElement types dependency](https://github.com/alibaba/formily/commit/b649228f) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(antd/next): export FormGrid props interface (#1327)](https://github.com/alibaba/formily/commit/733f7c26) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [feat(json-schema): add registerPolyfills/enablePolyfills api](https://github.com/alibaba/formily/commit/fd5eac5f) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(json-schema): add error when x-component can not found](https://github.com/alibaba/formily/commit/8bc884b3) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(json-schema): support alias style for x-reactions.dependencies](https://github.com/alibaba/formily/commit/b84a6244) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(form-item): support string format for labelWidth/wrapperWidth](https://github.com/alibaba/formily/commit/228e259c) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(effects): normoalize onFieldInit](https://github.com/alibaba/formily/commit/98922c8a) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat: add build style (#1201)](https://github.com/alibaba/formily/commit/3ceedb11) :point_right: ( [atzcl](https://github.com/atzcl) ) 1. [feat(project): rename fullfill=>fulfill](https://github.com/alibaba/formily/commit/0b794f11) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(reactive): recover batch.scope](https://github.com/alibaba/formily/commit/aeeb9f94) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(antd/next): update extract css name](https://github.com/alibaba/formily/commit/ea3d5194) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat: applicable less and scss to vite (#1187)](https://github.com/alibaba/formily/commit/fb011768) :point_right: ( [atzcl](https://github.com/atzcl) ) 1. [feat: add logic-diagram to Next and AntD (TBD) (#1158)](https://github.com/alibaba/formily/commit/5626d97d) :point_right: ( [soulwu](https://github.com/soulwu) ) 1. [feat: update antd message style](https://github.com/alibaba/formily/commit/b6d87da6) :point_right: ( [quirkyshop](https://github.com/quirkyshop) ) 1. [feat(react): update mapProps](https://github.com/alibaba/formily/commit/7940cab8) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat: move param-case to shared (#1152)](https://github.com/alibaba/formily/commit/6106257b) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [feat: add feedback layout](https://github.com/alibaba/formily/commit/df56cded) :point_right: ( [quirkyshop](https://github.com/quirkyshop) ) 1. [feat: update 'feedbackText'](https://github.com/alibaba/formily/commit/9e71f0c9) :point_right: ( [quirkyshop](https://github.com/quirkyshop) ) 1. [feat: add formitem demo](https://github.com/alibaba/formily/commit/5a263e68) :point_right: ( [guishu.zc](https://github.com/guishu.zc) ) 1. [feat(next): add FormGrid](https://github.com/alibaba/formily/commit/1805d8da) :point_right: ( [ZirkleTsing](https://github.com/ZirkleTsing) ) 1. [feat(vue): add vue3 compatibly (#1138)](https://github.com/alibaba/formily/commit/ac3783df) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [feat(react): connect add hoistNonReactStatics](https://github.com/alibaba/formily/commit/9b68f1ef) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(core): add more effects](https://github.com/alibaba/formily/commit/5b42226d) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat: update alignment (#1060)](https://github.com/alibaba/formily/commit/fadb3f7d) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [feat(core): support enableUnmountRemoveNode/disableUnmountRemoveNode API](https://github.com/alibaba/formily/commit/8f99e5b3) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat: add registerPreviewTextComponent (#1041)](https://github.com/alibaba/formily/commit/4b0f9768) :point_right: ( [soulwu](https://github.com/soulwu) ) 1. [feat: Add ja validation language (#1028) (#1029)](https://github.com/alibaba/formily/commit/6b65fbb9) :point_right: ( [Yaodong Li](https://github.com/Yaodong Li) ) 1. [feat(layout/docs): update docs and fix layout (#1003)](https://github.com/alibaba/formily/commit/16be58cc) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [feat(schema): add nested form polyfill (#972)](https://github.com/alibaba/formily/commit/6deb86d9) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [feat(components): add FormMegaLayout className (#935)](https://github.com/alibaba/formily/commit/7a2ad9e2) :point_right: ( [changfuguo](https://github.com/changfuguo) ) 1. [feat: add span to array-card dot for custom style (#922)](https://github.com/alibaba/formily/commit/4b2833d5) :point_right: ( [slientcloud](https://github.com/slientcloud) ) 1. [feat(layout): support responsive gri layout for older browsers (#916)](https://github.com/alibaba/formily/commit/f87e70dc) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [feat: add ie compat mode of grid(ie) (#912)](https://github.com/alibaba/formily/commit/b7313976) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [feat(layout): add ts type desc of MegaLayout and fix array-inc doc (#905)](https://github.com/alibaba/formily/commit/f37a0934) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [feat(layout): add inset mode for mega layout (#900)](https://github.com/alibaba/formily/commit/6f173317) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [feat: update snapshot and layout test for nested grid (#894)](https://github.com/alibaba/formily/commit/72619eca) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [feat: compile expression for array-table column title (#868)](https://github.com/alibaba/formily/commit/48fbcf0f) :point_right: ( [soulwu](https://github.com/soulwu) ) 1. [feat(docs): add antd TimePicker.RangePicker demo. (#811)](https://github.com/alibaba/formily/commit/fab22309) :point_right: ( [ShiCheng](https://github.com/ShiCheng) ) 1. [feat(antd-components): add default export (#810)](https://github.com/alibaba/formily/commit/0b4e64da) :point_right: ( [kenve](https://github.com/kenve) ) 1. [feat: add formily-meet documents (#797)](https://github.com/alibaba/formily/commit/03bbd0b7) :point_right: ( [DarK-AleX-alibaba](https://github.com/DarK-AleX-alibaba) ) 1. [feat(core): remove initializeLazySyncState](https://github.com/alibaba/formily/commit/70094beb) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat(schema-renderer): support relative target path (#779)](https://github.com/alibaba/formily/commit/f5fe4061) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(docs): add service worker cache (#745)](https://github.com/alibaba/formily/commit/a5879b72) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat: add recursive-render doc and fix some bugs (#736)](https://github.com/alibaba/formily/commit/d7199d82) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(hooks): add onSubmit hook and docs (#727)](https://github.com/alibaba/formily/commit/b99be566) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [feat(core): support pass FormPathPattern to createMutators, and fix some typings (#728)](https://github.com/alibaba/formily/commit/c0798c6d) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(core): change visible behavior to fix array list delete auto assign value not work (#725)](https://github.com/alibaba/formily/commit/366047e6) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(prject): access unified log module (#723)](https://github.com/alibaba/formily/commit/750ef0af) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(shared): support BigData (#708)](https://github.com/alibaba/formily/commit/7343b960) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat: add wiki (#705)](https://github.com/alibaba/formily/commit/9b2126c9) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat: url type support rtmp (#686)](https://github.com/alibaba/formily/commit/084cbc03) :point_right: ( [Desen Meng](https://github.com/Desen Meng) ) 1. [feat: add components and hooks (#670)](https://github.com/alibaba/formily/commit/ef9bc68e) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [feat(@uform/devtools): update lerna config (#635)](https://github.com/alibaba/formily/commit/7ca92451) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(@uform/core): reset add clearInitialValue (#627)](https://github.com/alibaba/formily/commit/02e715ce) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat: update formitem props (#614)](https://github.com/alibaba/formily/commit/1ff5f8bc) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [feat: use react-drag-listview instead of ReactDnD and support antd draggable table (#609)](https://github.com/alibaba/formily/commit/88ce573b) :point_right: ( [soulwu](https://github.com/soulwu) ) 1. [feat(@uform/core)support visible cache values and intialValues sync action (#588)](https://github.com/alibaba/formily/commit/7bceed76) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat: support change fieldKey](https://github.com/alibaba/formily/commit/ffc8f6a7) :point_right: ( [ziyi.hzy](https://github.com/ziyi.hzy) ) 1. [feat: add dragable to @uform/next table field (#561)](https://github.com/alibaba/formily/commit/4c947306) :point_right: ( [soulwu](https://github.com/soulwu) ) 1. [featfix(@uform/react-schema-renderer/antd/next) doc and depreacate x-render (#557)](https://github.com/alibaba/formily/commit/2bd1503b) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [feat: FieldEditor UI 优化](https://github.com/alibaba/formily/commit/071058e4) :point_right: ( [秋逢](https://github.com/秋逢) ) 1. [feat: update unitest and document (#476)](https://github.com/alibaba/formily/commit/8c49ca9a) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [feat: json to basic schema (#450)](https://github.com/alibaba/formily/commit/785a760d) :point_right: ( [大康](https://github.com/大康) ) 1. [feat: 表达式 value](https://github.com/alibaba/formily/commit/73c90914) :point_right: ( [秋逢](https://github.com/秋逢) ) 1. [feat: fix bug](https://github.com/alibaba/formily/commit/bfd76328) :point_right: ( [ascoders](https://github.com/ascoders) ) 1. [feat(@uform/next): update next features (#439)](https://github.com/alibaba/formily/commit/15b6b43e) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(@uform/react): actions support clearErrors (#434)](https://github.com/alibaba/formily/commit/551d74c1) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat: 规则](https://github.com/alibaba/formily/commit/fa6215dc) :point_right: ( [秋逢](https://github.com/秋逢) ) 1. [feat(@uform/react): remove raf and fix unittest (#422)](https://github.com/alibaba/formily/commit/670fadbe) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [feat(@uform/core): support pass visible/display of register method (#421)](https://github.com/alibaba/formily/commit/908882a2) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat: support useFormEffects (#403)](https://github.com/alibaba/formily/commit/dff959c8) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat: 临时交互对焦](https://github.com/alibaba/formily/commit/bed060ff) :point_right: ( [秋逢](https://github.com/秋逢) ) 1. [feat: add docs and some test cases (#395)](https://github.com/alibaba/formily/commit/ecff8eff) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat: add react/actions tests](https://github.com/alibaba/formily/commit/21ee40b1) :point_right: ( [anyuxuan](https://github.com/anyuxuan) ) 1. [feat: 添加 next components schema](https://github.com/alibaba/formily/commit/1b184a6c) :point_right: ( [秋逢](https://github.com/秋逢) ) 1. [feat: add silent option (#377)](https://github.com/alibaba/formily/commit/43771809) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [feat(shared): add unit test (#374)](https://github.com/alibaba/formily/commit/9cd72725) :point_right: ( [s0ngyee](https://github.com/s0ngyee) ) 1. [feat(docs): support deconstruction (#179)](https://github.com/alibaba/formily/commit/b114c9e7) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(@uform/core): Improve noValidate reset logic](https://github.com/alibaba/formily/commit/efaa75a8) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [feat: update the api docs using typescript (#149)](https://github.com/alibaba/formily/commit/5a9ea5a2) :point_right: ( [Kevin Tan](https://github.com/Kevin Tan) ) 1. [feat: make scheduler optional (#141)](https://github.com/alibaba/formily/commit/ed52e4a7) :point_right: ( [Kevin Tan](https://github.com/Kevin Tan) ) 1. [feat(@uform/antd/next): Optimize the description of the word count calculation rules and docs #117](https://github.com/alibaba/formily/commit/65c449e0) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(@uform/antd): support form layout properties #116](https://github.com/alibaba/formily/commit/e9cc882d) :point_right: ( [Janry](https://github.com/Janry) ) 1. [feat(refactor): perfect test suites and add builder demo in docs (#100)](https://github.com/alibaba/formily/commit/ada8ba9f) :point_right: ( [SkyCai](https://github.com/SkyCai) ) 1. [feat(@uform/types): update validator type description](https://github.com/alibaba/formily/commit/6763583a) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [feat(@uform/utils): support ts, but build scripts is not work](https://github.com/alibaba/formily/commit/8e452149) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [feat(@uform/next): add disabled when loading](https://github.com/alibaba/formily/commit/1b1d70db) :point_right: ( [monkindey](https://github.com/monkindey) ) 1. [feat(@uform/react): Optimize package size and fixing onFieldChange initialization trigger twice](https://github.com/alibaba/formily/commit/a98c247b) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [feat(packages): some the antd component and the react component](https://github.com/alibaba/formily/commit/c663abc0) :point_right: ( [zsirfs](https://github.com/zsirfs) ) 1. [feat(fix): add builder-next package and fix builder bugs. fix(docs): update playground link and fix some bugs](https://github.com/alibaba/formily/commit/71e6af8a) :point_right: ( [cnt1992](https://github.com/cnt1992) ) 1. [feat(@uform/next/antd): support mapTextComponent and mapStyledProps](https://github.com/alibaba/formily/commit/b0f7134d) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [feat(utils): export path destruct string parse methods.](https://github.com/alibaba/formily/commit/1bded6c3) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [feat(fix): fix style](https://github.com/alibaba/formily/commit/7841970d) :point_right: ( [janryWang](https://github.com/janryWang) ) ### :bug: Bug Fixes 1. [fix(react): fix useAttach not work with react18 strict mode (#3284)](https://github.com/alibaba/formily/commit/9df806b5) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: doc (#3280)](https://github.com/alibaba/formily/commit/8569c0fe) :point_right: ( [Creabine](https://github.com/Creabine) ) 1. [fix(antd): use Select fieldNames (#3275)](https://github.com/alibaba/formily/commit/edf7a9f9) :point_right: ( [yiyunwan](https://github.com/yiyunwan) ) 1. [fix(antd/next): fix array components lose reactive (#3266)](https://github.com/alibaba/formily/commit/9107e86c) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(antd/next): fix Cascader in form readPretty mode display error #3250 (#3253)](https://github.com/alibaba/formily/commit/d5719100) :point_right: ( [风](https://github.com/风) ) 1. [fix(core): fix memo leak of onFieldReact/onFieldChange (#3231)](https://github.com/alibaba/formily/commit/d1c44513) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(antd/next): react 18 createRoot warning (#3226)](https://github.com/alibaba/formily/commit/cc1f5d48) :point_right: ( [csc-bo](https://github.com/csc-bo) ) 1. [fix(reactive-react): fix useLayoutEffect warning in server render (#3228)](https://github.com/alibaba/formily/commit/8c9ab06b) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: vue3 slots.default do not always exist (#3192)](https://github.com/alibaba/formily/commit/91d64889) :point_right: ( [qq1037305420](https://github.com/qq1037305420) ) 1. [fix(core): fix set initialValue no cache value when display none (#3182)](https://github.com/alibaba/formily/commit/66ffeb6c) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(react): fix field wrong mounted state (#3181)](https://github.com/alibaba/formily/commit/d705f56d) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: typo (#3169)](https://github.com/alibaba/formily/commit/0cb920d2) :point_right: ( [Adel](https://github.com/Adel) ) 1. [fix(antd): fix array table lose focus (#3160)](https://github.com/alibaba/formily/commit/8ee5ba51) :point_right: ( [Grapedge](https://github.com/Grapedge) ) 1. [fix(next): fix space item empty style (#3149)](https://github.com/alibaba/formily/commit/18700a90) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: add Component Ecology: semi for zhCN (#3146)](https://github.com/alibaba/formily/commit/4eee3574) :point_right: ( [programmerwy](https://github.com/programmerwy) ) 1. [fix(antd): remove radio button border right color compat codes (#3144)](https://github.com/alibaba/formily/commit/abefbeac) :point_right: ( [蜘蛛侠](https://github.com/蜘蛛侠) ) 1. [fix(core): fix onInput should not filter value with target (#3140)](https://github.com/alibaba/formily/commit/e1a2a65e) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: fix ArrayTable skipping validation of new page (#3117)](https://github.com/alibaba/formily/commit/99f669a8) :point_right: ( [maurice](https://github.com/maurice) ) 1. [fix: compat FormItem styles for chrome88 (#3121)](https://github.com/alibaba/formily/commit/9eb73067) :point_right: ( [陈为响](https://github.com/陈为响) ) 1. [fix(core): fix field destroyed still can be assigned value (#3115)](https://github.com/alibaba/formily/commit/5dd9acce) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): fix errors filter (#3113)](https://github.com/alibaba/formily/commit/7d731af5) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(next): fix ArrayCollapse extra (#3106)](https://github.com/alibaba/formily/commit/39e26a83) :point_right: ( [oldchicken](https://github.com/oldchicken) ) 1. [fix(next/antd): fix form-collapse pass defaultActiveKey (#3107)](https://github.com/alibaba/formily/commit/793cae98) :point_right: ( [Jehu](https://github.com/Jehu) ) 1. [fix(react): update react peerDependencies (#3096)](https://github.com/alibaba/formily/commit/6e939d82) :point_right: ( [蜘蛛侠](https://github.com/蜘蛛侠) ) 1. [fix: fix rollup.base.js externals antd name (#3084)](https://github.com/alibaba/formily/commit/a4029fbe) :point_right: ( [ickeep](https://github.com/ickeep) ) 1. [fix(next): fix FormDrawer demo error (#3080)](https://github.com/alibaba/formily/commit/79d94774) :point_right: ( [Lyca](https://github.com/Lyca) ) 1. [fix(reactive-vue): stop tracking if watcher is destroyed #3074 (#3075)](https://github.com/alibaba/formily/commit/c2b7ba91) :point_right: ( [lcch](https://github.com/lcch) ) 1. [fix(core): fix field validateFirst not working (#3071)](https://github.com/alibaba/formily/commit/82af5e16) :point_right: ( [ryuurock](https://github.com/ryuurock) ) 1. [fix(vue): fix render loop cause by functional component in mapProps (#3070)](https://github.com/alibaba/formily/commit/0d15fe96) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [fix(vue): fix unexpected dep collection during $mount() #3015 (#3065)](https://github.com/alibaba/formily/commit/b2128aeb) :point_right: ( [lcch](https://github.com/lcch) ) 1. [fix(antd): fix the problem that when optionAsValue, the value is lost when searched or paged (#3064)](https://github.com/alibaba/formily/commit/43fbf031) :point_right: ( [Ray](https://github.com/Ray) ) 1. [fix(path): fix range all match is not expect (#3067)](https://github.com/alibaba/formily/commit/04e753f5) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): fix array child reactions invalid with remove (#3063)](https://github.com/alibaba/formily/commit/34e9420b) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(next): fix date time picker value format (#3044)](https://github.com/alibaba/formily/commit/cee97228) :point_right: ( [Eric Zhang](https://github.com/Eric Zhang) ) 1. [fix(antd/next): form step setCurrent bug (#3039)](https://github.com/alibaba/formily/commit/7aba4847) :point_right: ( [戣蓦](https://github.com/戣蓦) ) 1. [fix(antd/next): valueType should not be required attribute since it has default value (#3036)](https://github.com/alibaba/formily/commit/7b8669ba) :point_right: ( [戣蓦](https://github.com/戣蓦) ) 1. [fix(antd/next): fix form tab type check issue (#3025)](https://github.com/alibaba/formily/commit/f0511355) :point_right: ( [戣蓦](https://github.com/戣蓦) ) 1. [fix(antd): fix error, can't read 'length' of undefined (#3020) (#3021)](https://github.com/alibaba/formily/commit/10503b83) :point_right: ( [melodyYang](https://github.com/melodyYang) ) 1. [fix(antd/next): fix cascader preview text exception errors (#3000)](https://github.com/alibaba/formily/commit/a48252b6) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(antd/next): fix array string field addition logic (#2998)](https://github.com/alibaba/formily/commit/888dc47e) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(next/antd): fix array base type (#2994)](https://github.com/alibaba/formily/commit/b202c0d5) :point_right: ( [Jehu](https://github.com/Jehu) ) 1. [fix(reactive-vue): fix the exception of multiple update nodes in vue3 case (#2991)](https://github.com/alibaba/formily/commit/90486ecb) :point_right: ( [e_the](https://github.com/e_the) ) 1. [fix: fix destroy can not remove value/initialValues and FormStep reactive strategy (#2988)](https://github.com/alibaba/formily/commit/4d18c9e7) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(antd): StepPane & createFormStep should not be optional property (#2979)](https://github.com/alibaba/formily/commit/1c6970c5) :point_right: ( [戣蓦](https://github.com/戣蓦) ) 1. [fix(antd): fix ConfigProvider.ConfigContext error in antd@4.6.3- (#2956)](https://github.com/alibaba/formily/commit/3bdfe2f2) :point_right: ( [Lyca](https://github.com/Lyca) ) 1. [fix(antd/next): fix null in dataSource error in SelectTable (#2952)](https://github.com/alibaba/formily/commit/2d428941) :point_right: ( [Lyca](https://github.com/Lyca) ) 1. [fix(next/antd): fix single value invalid in PreviewText.Cascader (#2940)](https://github.com/alibaba/formily/commit/33a54e7a) :point_right: ( [Lyca](https://github.com/Lyca) ) 1. [fix(grid): fix grid mutation observer infinite loop (#2925)](https://github.com/alibaba/formily/commit/72534b43) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(validator): fix unexpect validate with empty format (#2926)](https://github.com/alibaba/formily/commit/7da26285) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(vue): fix error when designable is true (#2908)](https://github.com/alibaba/formily/commit/398fac96) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [fix(core): fix empty string or number can not rewrite default value (#2906)](https://github.com/alibaba/formily/commit/b6c3e311) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(reactive-vue): fix vue3 render dependency collection broken (#2904)](https://github.com/alibaba/formily/commit/b226760e) :point_right: ( [e_the](https://github.com/e_the) ) 1. [fix(element-components): fix formitem feedback msg (#2899)](https://github.com/alibaba/formily/commit/8d201778) :point_right: ( [skyfore](https://github.com/skyfore) ) 1. [fix(antd/next): remove host element after unmount in portal (#2900)](https://github.com/alibaba/formily/commit/a2af5c94) :point_right: ( [zhouxinyong](https://github.com/zhouxinyong) ) 1. [fix(antd/next): fix ArrayItems sortItem style (#2893)](https://github.com/alibaba/formily/commit/1ef47b0a) :point_right: ( [zhouxinyong](https://github.com/zhouxinyong) ) 1. [fix(antd/next): disable label/wrapper col when vertical layout](https://github.com/alibaba/formily/commit/119fd389) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(vue): fix FormConsumer not update correctly (#2888)](https://github.com/alibaba/formily/commit/4e39c082) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [fix(antd): fix use treeData props for PreviewText.TreeSelect (#2867)](https://github.com/alibaba/formily/commit/edcc9544) :point_right: ( [Dark](https://github.com/Dark) ) 1. [fix(core): fix relative match can not skip void field (#2850)](https://github.com/alibaba/formily/commit/e7c99843) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(shared): fix merge empty object is not work (#2841)](https://github.com/alibaba/formily/commit/28a58530) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(vue): add component name in connect (#2810)](https://github.com/alibaba/formily/commit/5a695c06) :point_right: ( [zhouxinyong](https://github.com/zhouxinyong) ) 1. [fix(core): fix reset can not clear value in array list (#2775)](https://github.com/alibaba/formily/commit/064e13aa) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(antd): fix ArrayTabs auto switch activeKey (#2774)](https://github.com/alibaba/formily/commit/72e0bdbd) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: correct indian rupee regexp (#2714)](https://github.com/alibaba/formily/commit/b2269019) :point_right: ( [catch on me](https://github.com/catch on me) ) 1. [fix(element): fix ArrayTable style error (#2760)](https://github.com/alibaba/formily/commit/3b24f7f7) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [fix(antd/next): fix Editable component can not set default editable](https://github.com/alibaba/formily/commit/88915bc5) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(antd/next): fix tool methods and provide simple unit tests (#2694)](https://github.com/alibaba/formily/commit/475d10e9) :point_right: ( [小翼](https://github.com/小翼) ) 1. [fix(vue): fix postinstall error (#2684)](https://github.com/alibaba/formily/commit/d4b9133f) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [fix(core): fix void array items node need skip (#2683)](https://github.com/alibaba/formily/commit/a67ab3a4) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(path): update README.md (#2677)](https://github.com/alibaba/formily/commit/589e74bf) :point_right: ( [AlexStacker](https://github.com/AlexStacker) ) 1. [fix(element): fix usePlaceholder not update error (#2646)](https://github.com/alibaba/formily/commit/550d0a6a) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [fix(grid): add resize-observer-polyfill (#2630)](https://github.com/alibaba/formily/commit/8c234a8a) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(vue): fix format vue3 h function props (#2609)](https://github.com/alibaba/formily/commit/e2dfc0bc) :point_right: ( [zhaowei-plus](https://github.com/zhaowei-plus) ) 1. [fix(antd/next): fix FormItem.label can not shown in void field](https://github.com/alibaba/formily/commit/f2bd220c) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(json-schema): fix reactions isolate effect (#2590)](https://github.com/alibaba/formily/commit/f04deb13) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(react): fix doc link (#2584)](https://github.com/alibaba/formily/commit/4faa406d) :point_right: ( [燃冰](https://github.com/燃冰) ) 1. [fix(next): fix missing ExclamationCircleOutlined Icon (#2564)](https://github.com/alibaba/formily/commit/33d8d278) :point_right: ( [Lyca](https://github.com/Lyca) ) 1. [fix(reactive): fix unexpect effect in reactions (#2563)](https://github.com/alibaba/formily/commit/8f8db67a) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(vue): fix void field children is not undefined (#2551)](https://github.com/alibaba/formily/commit/f5a1d1bb) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(next): fix Space align is not work (#2531)](https://github.com/alibaba/formily/commit/3f4afef1) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(element): add optinal chain to FormItem useOverflow hook (#2519)](https://github.com/alibaba/formily/commit/da189834) :point_right: ( [qq1037305420](https://github.com/qq1037305420) ) 1. [fix(next): fix the antd-icons is not removed cleanly](https://github.com/alibaba/formily/commit/4e7a4626) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(core): fix required validate with wrong order (#2508)](https://github.com/alibaba/formily/commit/f0ac9918) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): fix validator will trigger multi times with duplicate triggerTypes (#2495)](https://github.com/alibaba/formily/commit/88d6f83b) :point_right: ( [nexx](https://github.com/nexx) ) 1. [fix(grid): fix calc origin columns (#2468)](https://github.com/alibaba/formily/commit/1a9e37b4) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: fix validator notify error message of Antd Upload Item (#2433)](https://github.com/alibaba/formily/commit/8e4a6a98) :point_right: ( [jazzjia](https://github.com/jazzjia) ) 1. [fix(grid): fix build by removing build:global (#2417)](https://github.com/alibaba/formily/commit/0d78006d) :point_right: ( [Deng Ruoqi](https://github.com/Deng Ruoqi) ) 1. [fix(grid): fix grid calculate failed when container was hidden (#2400)](https://github.com/alibaba/formily/commit/18a09d42) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): fix reset should clear field caches (#2401)](https://github.com/alibaba/formily/commit/6b1162ad) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(reactive): fix computed value can not get real value (#2389)](https://github.com/alibaba/formily/commit/eb34b2de) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): fix default is not work when name is length (#2387)](https://github.com/alibaba/formily/commit/0adf07ab) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: fix decorator attrs is not passed down correctly (#2369)](https://github.com/alibaba/formily/commit/fee4af03) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [fix(vue): view should updated when schema changed (#2354)](https://github.com/alibaba/formily/commit/4b3d092d) :point_right: ( [Amorites](https://github.com/Amorites) ) 1. [fix(core): make exchangeArrayState be right when move (#2357)](https://github.com/alibaba/formily/commit/a2189465) :point_right: ( [折木](https://github.com/折木) ) 1. [fix(react): fix incorrect dts in useFieldSchema (#2350)](https://github.com/alibaba/formily/commit/e8781032) :point_right: ( [Jingkun Hua](https://github.com/Jingkun Hua) ) 1. [fix(core): fix initialValues merge with no fields (#2339)](https://github.com/alibaba/formily/commit/9c2ebc36) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(schema): fix setValidateRule will throw error when use void field (#2281)](https://github.com/alibaba/formily/commit/d752b221) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): fix validate lifecycle wrong trigger in skip digest (#2279)](https://github.com/alibaba/formily/commit/1ac87fb4) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(validator): getValidatorLocale Maximum call stack size exceeded (#2273)](https://github.com/alibaba/formily/commit/200253e0) :point_right: ( [Suel](https://github.com/Suel) ) 1. [fix(reactive): fix batch api can not throw error (#2268)](https://github.com/alibaba/formily/commit/07227ad2) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(vue): fix the problem that the component class name will be overwritten rather than merged (#2260)](https://github.com/alibaba/formily/commit/73053737) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [fix(element): fix form props pass bug (#2253)](https://github.com/alibaba/formily/commit/71859771) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [fix(vue): fix vue2 scopedSlot and slot pass problem (#2221)](https://github.com/alibaba/formily/commit/2489182c) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [fix(antd/next): fix props.prefix is not work for FormGrid/FormLayout (#2151)](https://github.com/alibaba/formily/commit/bcdac582) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): fix array unshift with incomplete elements (#2150)](https://github.com/alibaba/formily/commit/64633714) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(path): fix path match destructor (#2148)](https://github.com/alibaba/formily/commit/f621d989) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(next/antd/vue): fix Switch type & add classname to ArrayItems.Index (#2093)](https://github.com/alibaba/formily/commit/9f875692) :point_right: ( [Lyca](https://github.com/Lyca) ) 1. [fix(designable): fix Uncaught SyntaxError (#1997) (#2089)](https://github.com/alibaba/formily/commit/b56b5b28) :point_right: ( [youshao](https://github.com/youshao) ) 1. [fix(core): fix add effects memo leak in form umount (#2050)](https://github.com/alibaba/formily/commit/f753ba12) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(element): remove Formily namepsace usecase](https://github.com/alibaba/formily/commit/0cc90672) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(vue): fix 'x-content' render named slot not work (#2046)](https://github.com/alibaba/formily/commit/71fb9814) :point_right: ( [jiezi19971225](https://github.com/jiezi19971225) ) 1. [fix(designable-antd): fix locales](https://github.com/alibaba/formily/commit/27be2651) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(designable): fix can not drag object to array cards in initialization](https://github.com/alibaba/formily/commit/99b46a3e) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(next): fix usePrefixCls tag undefined (#2042)](https://github.com/alibaba/formily/commit/9af2dda7) :point_right: ( [hellohy](https://github.com/hellohy) ) 1. [fix(next): fix fullness icon width (#2020)](https://github.com/alibaba/formily/commit/8c4651fb) :point_right: ( [Lyca](https://github.com/Lyca) ) 1. [fix(antd): fix form size=large bug (#1998) (#2008)](https://github.com/alibaba/formily/commit/3edd6e89) :point_right: ( [Grapedge](https://github.com/Grapedge) ) 1. [fix(designable-next): fix style and support history (#2007)](https://github.com/alibaba/formily/commit/7e9c9cbd) :point_right: ( [Grapedge](https://github.com/Grapedge) ) 1. [fix(next): fix range and transfer styles in FormItem](https://github.com/alibaba/formily/commit/cd9c2159) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(next): fix config provider prefix bug (#2000) (#2002)](https://github.com/alibaba/formily/commit/32746f77) :point_right: ( [Grapedge](https://github.com/Grapedge) ) 1. [fix(vue): prop "scope" of SchemaField not work with x-reactions (#1976)](https://github.com/alibaba/formily/commit/05e14cea) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [fix(next): fix mapStatus takeState](https://github.com/alibaba/formily/commit/576c9b56) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(antd): fix dark label color](https://github.com/alibaba/formily/commit/c1e5b0f4) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(shared): fix applyMiddleware can not catch error (#1952)](https://github.com/alibaba/formily/commit/22f0379a) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(next): fix FormTab activeKey bug (#1945)](https://github.com/alibaba/formily/commit/29024475) :point_right: ( [Grapedge](https://github.com/Grapedge) ) 1. [fix(next): add the default language when the language is undefined (#1939)](https://github.com/alibaba/formily/commit/c74e7f91) :point_right: ( [Grapedge](https://github.com/Grapedge) ) 1. [fix(next/designable-antd): fix Select bug && designable-antd spelling error (#1934)](https://github.com/alibaba/formily/commit/739e8c18) :point_right: ( [Grapedge](https://github.com/Grapedge) ) 1. [fix(next): fix size style in FormItem/main.scss && set default fullness true (#1908)](https://github.com/alibaba/formily/commit/c0e2c126) :point_right: ( [Lyca](https://github.com/Lyca) ) 1. [fix(element): fix protal destroy (#1898)](https://github.com/alibaba/formily/commit/1036440c) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [fix(designable-antd): remove switch optionType: 'button' (#1891)](https://github.com/alibaba/formily/commit/c136e349) :point_right: ( [aloha](https://github.com/aloha) ) 1. [fix(react): fix select type validate error #1838 (#1844)](https://github.com/alibaba/formily/commit/b7975baf) :point_right: ( [张威](https://github.com/张威) ) 1. [fix(antd): fix sideEffects mismatch when use babel-plugin-import (#1843)](https://github.com/alibaba/formily/commit/eaccb72a) :point_right: ( [KM.Seven](https://github.com/KM.Seven) ) 1. [fix(core): fix object field's children auto clean but they are not additionalProperty (#1840)](https://github.com/alibaba/formily/commit/dd313646) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): fix ArrayField operation will trigger memo leak (#1831)](https://github.com/alibaba/formily/commit/021c155a) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(path): fix segments match (#1826)](https://github.com/alibaba/formily/commit/6e541dcb) :point_right: ( [砂糖梨子](https://github.com/砂糖梨子) ) 1. [fix(antd/next): form-grid and layout props optional with default value (#1809)](https://github.com/alibaba/formily/commit/2738e418) :point_right: ( [gwsbhqt](https://github.com/gwsbhqt) ) 1. [fix(vue): fix field doesnt update correctly in designable mode (#1799)](https://github.com/alibaba/formily/commit/837cfc0b) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [fix(element): fix vuepress doc not identify fetch (#1769)](https://github.com/alibaba/formily/commit/bc4348e3) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [fix(element): add rollup external to fix element package size (#1766)](https://github.com/alibaba/formily/commit/8104dbfb) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [fix(vue): fix vue typing (#1730)](https://github.com/alibaba/formily/commit/b51a2198) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [fix(react): fix x-content is not work with array type (#1719)](https://github.com/alibaba/formily/commit/2cd60d32) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(reactive): fix Tracker memo leak in StrictMode (#1715)](https://github.com/alibaba/formily/commit/e9f23c39) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(json-schema): fix typo about transformer](https://github.com/alibaba/formily/commit/498d3119) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(core): fix lifecycle not working after call form.setXXX (#1699)](https://github.com/alibaba/formily/commit/01c5fb89) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [fix(shared): fix defaults merge with null will get unexpect results #1644](https://github.com/alibaba/formily/commit/d39c426f) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(json-schema): fix findComponent return unexpected result (#1625)](https://github.com/alibaba/formily/commit/3453c69d) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [fix(antd/next): remove FormButtonGroup.FormItem colon #1623](https://github.com/alibaba/formily/commit/48137547) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(antd): fix DatePicker week formatting errors (#1614)](https://github.com/alibaba/formily/commit/dbdd1984) :point_right: ( [sun](https://github.com/sun) ) 1. [fix(vue): fix unmount a field in a wrong lifecycle function.(#1609) (#1611)](https://github.com/alibaba/formily/commit/26896482) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [fix(types): fix global.d.ts](https://github.com/alibaba/formily/commit/df8561d6) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(project): fix typings](https://github.com/alibaba/formily/commit/c8aff09b) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(vue): add deep copy to decorator props (#1587)](https://github.com/alibaba/formily/commit/710f5e1b) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [fix(core): fix createForm memory leak](https://github.com/alibaba/formily/commit/5f11459b) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(antd/next): fix arrayCollapse will throw error in accordion mode](https://github.com/alibaba/formily/commit/4c88ca7f) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(next): fix style missing due to wrong sideEffects (#1564)](https://github.com/alibaba/formily/commit/9fb8b93e) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [fix(antd/next): fix dumi lost style due to treeshaking (#1549)](https://github.com/alibaba/formily/commit/20d6b4f2) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [fix(core): fix array path calculation #1533](https://github.com/alibaba/formily/commit/29249000) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(react): fix useFormEffects not support StrictMode #1491](https://github.com/alibaba/formily/commit/0198b0c4) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(core): fix field value restored incorrectly when hidden toggled (#1529)](https://github.com/alibaba/formily/commit/047c98af) :point_right: ( [JustDs](https://github.com/JustDs) ) 1. [fix(vue): remove empty default slots of fields (#1517)](https://github.com/alibaba/formily/commit/00a80b4b) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [fix(react): fix ReactComponentPropsByPathValue type return error result (#1507)](https://github.com/alibaba/formily/commit/fb7654eb) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [fix(core): fix reactive query #1494](https://github.com/alibaba/formily/commit/a0ca5b2b) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(validator): fix typo](https://github.com/alibaba/formily/commit/b1a83d2b) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(path): fix realative path for sibling in array (#1492)](https://github.com/alibaba/formily/commit/860264d6) :point_right: ( [JustDs](https://github.com/JustDs) ) 1. [fix(json-schema): remove array patch state logic](https://github.com/alibaba/formily/commit/73bd9a47) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(antd/next): fix gridSpan calculate algorithm (#1440)](https://github.com/alibaba/formily/commit/3b1f1cfa) :point_right: ( [Nokecy](https://github.com/Nokecy) ) 1. [fix(antd): fix btn is too big in small mode (#1455)](https://github.com/alibaba/formily/commit/c33df277) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [fix(vue): fix a type error in ISchemaMarkupFieldProps (#1454)](https://github.com/alibaba/formily/commit/43abadc5) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [fix(core): fix the effects of IFormProps losing generic type (#1418)](https://github.com/alibaba/formily/commit/ee8d118d) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [fix Form.submit miss return values (#1382)](https://github.com/alibaba/formily/commit/57c2c1b3) :point_right: ( [林法鑫](https://github.com/林法鑫) ) 1. [fix(doc): fix next doc (#1385)](https://github.com/alibaba/formily/commit/77e2c486) :point_right: ( [Lind](https://github.com/Lind) ) 1. [fix(antd/next): fix the feedbackLayout type definition error of the form-layout (#1372)](https://github.com/alibaba/formily/commit/3c5f6f7c) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [fix json-schema SchemaReaction type error (#1367)](https://github.com/alibaba/formily/commit/adae3da5) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [fix(reactive-react): fix browser crash in strict-mode async linkages scence](https://github.com/alibaba/formily/commit/feb64875) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(next): fix scss variables](https://github.com/alibaba/formily/commit/c99a380e) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(vue): mapProps、mapReadPretty listeners bug](https://github.com/alibaba/formily/commit/b5f39ce0) :point_right: ( [p(^-^q)]() ) 1. [fix(array-table): give toFieldProps an options](https://github.com/alibaba/formily/commit/edf3cab2) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(antd): fix validated form-item box-shadow styles (#1265)](https://github.com/alibaba/formily/commit/589b9b8b) :point_right: ( [Fog3211](https://github.com/Fog3211) ) 1. [fix(react/vue): fix onChange can not pass to voidField's component props. (#1264)](https://github.com/alibaba/formily/commit/1764f6ee) :point_right: ( [林法鑫](https://github.com/林法鑫) ) 1. [fix(core): fix reset logic for ArrayField/ObjectField](https://github.com/alibaba/formily/commit/909c5907) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(reactive): fix tojs recursive dependence stack overflow (#1245)](https://github.com/alibaba/formily/commit/675df0ce) :point_right: ( [gwsbhqt](https://github.com/gwsbhqt) ) 1. [fix(core): rollback onFieldInit behavior](https://github.com/alibaba/formily/commit/15f9a56d) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(antd): Prevent native events bubbles](https://github.com/alibaba/formily/commit/11e14a39) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(core): Fix the problem of onChange event catching exception](https://github.com/alibaba/formily/commit/8d6e1c2b) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(path): fix accessor](https://github.com/alibaba/formily/commit/4fde9ca0) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(antd): fix multiple select small/large styles](https://github.com/alibaba/formily/commit/7b628cef) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix antd styles (#1181)](https://github.com/alibaba/formily/commit/2083b01e) :point_right: ( [Dark](https://github.com/Dark) ) 1. [fix(core): untracked update values](https://github.com/alibaba/formily/commit/4b54d376) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix: use form.setValuesIn instead of field.removeProperty (#1160)](https://github.com/alibaba/formily/commit/f5fc7e61) :point_right: ( [soulwu](https://github.com/soulwu) ) 1. [fix(form-grid): improve performace](https://github.com/alibaba/formily/commit/f1b7afd2) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(core): fix observable componentProps](https://github.com/alibaba/formily/commit/dfe2e213) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(devtools): fix serialize function](https://github.com/alibaba/formily/commit/36aef5b8) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(core): Fix the problem that the initialValues cannot be synchronized to values repeatedly](https://github.com/alibaba/formily/commit/09e0f70b) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(next): fix month picker (#1115)](https://github.com/alibaba/formily/commit/f77b2ca2) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(vue): fix connect](https://github.com/alibaba/formily/commit/727169ba) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix: fix form help validate status error (#1071)](https://github.com/alibaba/formily/commit/82d50df4) :point_right: ( [Yohox](https://github.com/Yohox) ) 1. [fix(next): fix children not rendered](https://github.com/alibaba/formily/commit/52ece397) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(react): fix form render dirty check (#1056)](https://github.com/alibaba/formily/commit/5aeed554) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): fix input change trigger order](https://github.com/alibaba/formily/commit/1cebca66) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(next-components): Replace ArrayList.Item with Table.Column. Fix #1034 (#1045)](https://github.com/alibaba/formily/commit/e116838a) :point_right: ( [soulwu](https://github.com/soulwu) ) 1. [fix(core): fix hasChanged return type (#1036)](https://github.com/alibaba/formily/commit/d0eb66b6) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix Upload preview image (#1031)](https://github.com/alibaba/formily/commit/e2bfcce9) :point_right: ( [liunian](https://github.com/liunian) ) 1. [fix(antd-components): missing 'key' prop warning when table draggable (#1011)](https://github.com/alibaba/formily/commit/a69cdad1) :point_right: ( [daief](https://github.com/daief) ) 1. [fix: compat legal props (#1007)](https://github.com/alibaba/formily/commit/5dde72ae) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [fix(schema-renderer): fix schema field lazy state (#999)](https://github.com/alibaba/formily/commit/8faab444) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(shared): update cool-path version, ensure bug to be fixed (#988)](https://github.com/alibaba/formily/commit/5ae37fe0) :point_right: ( [soulwu](https://github.com/soulwu) ) 1. [fix(schema-renderer): Fix expression complie perf bug (#986)](https://github.com/alibaba/formily/commit/0e8383ee) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: compat ie10-11 for antd3 (#985)](https://github.com/alibaba/formily/commit/74fa86c9) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [fix: 回滚 mutators.move 行为 (#984)](https://github.com/alibaba/formily/commit/010e1495) :point_right: ( [soulwu](https://github.com/soulwu) ) 1. [fix: mutator insert (#977)](https://github.com/alibaba/formily/commit/f3356321) :point_right: ( [xiaowanzi](https://github.com/xiaowanzi) ) 1. [fix(core): fix field default sync exception (#970)](https://github.com/alibaba/formily/commit/d0872817) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(layout): type typo (#962)](https://github.com/alibaba/formily/commit/9b9f052f) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [fix(core): fix move down throw errors and fix null assign merge throw errors (#961)](https://github.com/alibaba/formily/commit/854feec2) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): use form batch to sync errors in array state exchanging](https://github.com/alibaba/formily/commit/0e4880fb) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(editor): remove import lodash/fp](https://github.com/alibaba/formily/commit/a105cff3) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(core): fix form ref values (#952)](https://github.com/alibaba/formily/commit/777596b7) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(schema): compat eva expression actions (#951)](https://github.com/alibaba/formily/commit/aed0369b) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [fix(core): fix antd table get row key (#946)](https://github.com/alibaba/formily/commit/6bda3296) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(@formily/core): fix unmountClearStates flags is not right (#944)](https://github.com/alibaba/formily/commit/754a55f4) :point_right: ( [soulwu](https://github.com/soulwu) ) 1. [fix(antd,next): fix ie.tsx ssr bug (#936)](https://github.com/alibaba/formily/commit/0d3c3810) :point_right: ( [Markey](https://github.com/Markey) ) 1. [fix: issue 853 and 860 (#928)](https://github.com/alibaba/formily/commit/c1774308) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [bugfix (#920)](https://github.com/alibaba/formily/commit/4f41b564) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): sync form state (#906)](https://github.com/alibaba/formily/commit/de32802a) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(react): fix ArrayTable index and FormSpy (#904)](https://github.com/alibaba/formily/commit/944891f7) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(layout): inset mode comflict with labelAlign top (#903)](https://github.com/alibaba/formily/commit/9906a0ce) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [fix(core): fix array list mutators (#888)](https://github.com/alibaba/formily/commit/50f4e9e5) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(next/components): incorrect size #884 (#885)](https://github.com/alibaba/formily/commit/c930e27d) :point_right: ( [锦此](https://github.com/锦此) ) 1. [fix(components): fix datepicker format and checkbox editable style (#881)](https://github.com/alibaba/formily/commit/99ad146f) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(schema-renderer): fixed connect onBlur/onFocus throw errors (#874)](https://github.com/alibaba/formily/commit/54012b46) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: megalayout columns (#871)](https://github.com/alibaba/formily/commit/9bff1f29) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [fix(schema-renderer): fix virtual box can not receive visible ant display (#869)](https://github.com/alibaba/formily/commit/1d7d94e6) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: remove warning of addon before (#863)](https://github.com/alibaba/formily/commit/110238c6) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [fix(react): fix useField/useVirtualField props assign (#858)](https://github.com/alibaba/formily/commit/e71e527a) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(schema-editor): fix dependencies (#857)](https://github.com/alibaba/formily/commit/78f02c38) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(antd/next): fix button-group typings (#855)](https://github.com/alibaba/formily/commit/08077729) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(react): fix formSpy conflict with parent SchemaForm (#854)](https://github.com/alibaba/formily/commit/e122c9d9) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(antd/next): fix FormTextBox doesnot support className (#851)](https://github.com/alibaba/formily/commit/e40bdf2b) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(antd): fix labelCol/wrapperCol can not be overwriten (#850)](https://github.com/alibaba/formily/commit/4f87465c) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): fix unmounteRemoveValue property is not work #827 (#847)](https://github.com/alibaba/formily/commit/f53d02ae) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(react-schema-renderer): fix x-linkages typings (#823)](https://github.com/alibaba/formily/commit/06c1a310) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(array-table): remove min-width style property (#820)](https://github.com/alibaba/formily/commit/22d03df2) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): fix immer autoFreeze and reset Native Object (#816)](https://github.com/alibaba/formily/commit/aff23189) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: arrayTable style (#813)](https://github.com/alibaba/formily/commit/fe913dd9) :point_right: ( [xiaowanzi](https://github.com/xiaowanzi) ) 1. [fix: FormTab components parseDefaultActiveKey (#802)](https://github.com/alibaba/formily/commit/2fb128b0) :point_right: ( [xiaowanzi](https://github.com/xiaowanzi) ) 1. [fix: Add default export for the antd (#787)](https://github.com/alibaba/formily/commit/5f5d4190) :point_right: ( [Rex Guo](https://github.com/Rex Guo) ) 1. [fix(react-schema-editor): improve SchemaEditor types (#786)](https://github.com/alibaba/formily/commit/944b6f7a) :point_right: ( [kenve](https://github.com/kenve) ) 1. [fix: readme typo (#785)](https://github.com/alibaba/formily/commit/56ef8829) :point_right: ( [WanTong](https://github.com/WanTong) ) 1. [fix(antd): fix FormItem type definition (#784)](https://github.com/alibaba/formily/commit/a53b46a7) :point_right: ( [kenve](https://github.com/kenve) ) 1. [fix(next): add onPageSizeChange (#777)](https://github.com/alibaba/formily/commit/b2df2d40) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(core): add lifecycle buffer gc (#773)](https://github.com/alibaba/formily/commit/360c2110) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(share): fix toArr if param is a proxy (#760)](https://github.com/alibaba/formily/commit/fca3890e) :point_right: ( [林法鑫](https://github.com/林法鑫) ) 1. [fix(antd): fix error auto scroll is not work for antd4 (#750)](https://github.com/alibaba/formily/commit/9d0f2196) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix x-index order algorithm (#724)](https://github.com/alibaba/formily/commit/17ae9ddb) :point_right: ( [JerryLyu](https://github.com/JerryLyu) ) 1. [fix(printer): fix print schema (#710)](https://github.com/alibaba/formily/commit/eb4b4e37) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: doc typo of antd (#699)](https://github.com/alibaba/formily/commit/a10efdf9) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [fix(antd-components): fix password component bugs (#672)](https://github.com/alibaba/formily/commit/bf6128eb) :point_right: ( [JerryLyu](https://github.com/JerryLyu) ) 1. [fix(project): compat uform (#666)](https://github.com/alibaba/formily/commit/74008983) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(meet): fix ci (#660)](https://github.com/alibaba/formily/commit/0aba4483) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(@formily/meet): fix pacakge config (#659)](https://github.com/alibaba/formily/commit/06837f9e) :point_right: ( [DarK-AleX-alibaba](https://github.com/DarK-AleX-alibaba) ) 1. [fix: upload children (#631)](https://github.com/alibaba/formily/commit/9c0095c1) :point_right: ( [JeromeYangtao](https://github.com/JeromeYangtao) ) 1. [fix: fix type lint (#628)](https://github.com/alibaba/formily/commit/8215d7f4) :point_right: ( [soulwu](https://github.com/soulwu) ) 1. [fix(antd/next): fix antd/next table arr[0] path (#624)](https://github.com/alibaba/formily/commit/fb64eae7) :point_right: ( [WingGao](https://github.com/WingGao) ) 1. [fix: 616 (#622)](https://github.com/alibaba/formily/commit/23ff1447) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [fix(@uform/core/react): fix #613 #615 (#618)](https://github.com/alibaba/formily/commit/8dc609f9) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(@uform/shared): fix isValid (#604)](https://github.com/alibaba/formily/commit/4136691d) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(@uform/core): fix submit catch error (#603)](https://github.com/alibaba/formily/commit/406f9fb9) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(uform/core): recover field visible/display state after parent changed (#567)](https://github.com/alibaba/formily/commit/d270ef78) :point_right: ( [小黄黄](https://github.com/小黄黄) ) 1. [fix: issue#540 (#549)](https://github.com/alibaba/formily/commit/4ae1759d) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [fix: build on windows (#539)](https://github.com/alibaba/formily/commit/8ad99322) :point_right: ( [WingGao](https://github.com/WingGao) ) 1. [bugfix: add config files and fix the build error messages](https://github.com/alibaba/formily/commit/2da0edae) :point_right: ( [云数](https://github.com/云数) ) 1. [fix(@uform/core): add onFormReset hook](https://github.com/alibaba/formily/commit/8633ae5f) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(@uform/core): add values to submit resolve callback params (#508)](https://github.com/alibaba/formily/commit/06c4f631) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: form effect demo (#499)](https://github.com/alibaba/formily/commit/93f87ad2) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [fix schema property `minItems ` (#493)](https://github.com/alibaba/formily/commit/26e12aa1) :point_right: ( [李力](https://github.com/李力) ) 1. [fix: use omit to elegant &](https://github.com/alibaba/formily/commit/72e51a61) :point_right: ( [quirkyshop](https://github.com/quirkyshop) ) 1. [fix: types merge error](https://github.com/alibaba/formily/commit/950a1930) :point_right: ( [quirkyshop](https://github.com/quirkyshop) ) 1. [fix(@uform/antd): Warning Received `true` for a non-boolean attribute `inline` (#494)](https://github.com/alibaba/formily/commit/46fbcb44) :point_right: ( [GODI13](https://github.com/GODI13) ) 1. [fix(@uform/core): fix init visible can not remove value (#492)](https://github.com/alibaba/formily/commit/a6dcc18d) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: merge uform master](https://github.com/alibaba/formily/commit/84d2bf17) :point_right: ( [秋逢](https://github.com/秋逢) ) 1. [fix: printer get api and add get form schema to doc (#482)](https://github.com/alibaba/formily/commit/f01988ff) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [fix(@uform/antd/next/react): doc (#471)](https://github.com/alibaba/formily/commit/6d73c6cd) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [fix(@uform/validator): fix maximum rule get message logic (#468)](https://github.com/alibaba/formily/commit/752c09e3) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix: Not in the browser](https://github.com/alibaba/formily/commit/676ff5f5) :point_right: ( [jinc.cjc](https://github.com/jinc.cjc) ) 1. [fix: in miniapp, globalSelf is existing](https://github.com/alibaba/formily/commit/4b6a9c08) :point_right: ( [jinc.cjc](https://github.com/jinc.cjc) ) 1. [fix: in miniapp (worker runtime) , globalThis is not a function](https://github.com/alibaba/formily/commit/745a0d9f) :point_right: ( [jinc.cjc](https://github.com/jinc.cjc) ) 1. [fix(@uform/next): formitem compatibility (#440)](https://github.com/alibaba/formily/commit/3bfe515b) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [fix: 引入 next 样式](https://github.com/alibaba/formily/commit/9d12b489) :point_right: ( [秋逢](https://github.com/秋逢) ) 1. [fix(antd): return null while time field get falsy value (#372)](https://github.com/alibaba/formily/commit/269a1706) :point_right: ( [腰花](https://github.com/腰花) ) 1. [fix: [onFieldChange] types](https://github.com/alibaba/formily/commit/dc4fa80c) :point_right: ( [jinc.cjc](https://github.com/jinc.cjc) ) 1. [fix window is not defined (#312)](https://github.com/alibaba/formily/commit/a089fa89) :point_right: ( [Neil](https://github.com/Neil) ) 1. [fix(globalThis): fix ReferenceError (#309)](https://github.com/alibaba/formily/commit/9efc90a6) :point_right: ( [Neil](https://github.com/Neil) ) 1. [fix: ButtonGroup missing the definition of align prop (#297)](https://github.com/alibaba/formily/commit/a989364f) :point_right: ( [atzcl](https://github.com/atzcl) ) 1. [fix(core): Increase lastValidateValue value processing during initialization (#276)](https://github.com/alibaba/formily/commit/045f6fea) :point_right: ( [atzcl](https://github.com/atzcl) ) 1. [fix: getSchema returning undefined doesn't break setIn (#269)](https://github.com/alibaba/formily/commit/da1f7a21) :point_right: ( [Kiho · Cham](https://github.com/Kiho · Cham) ) 1. [fix: remove react unstable concurrent (#270)](https://github.com/alibaba/formily/commit/0f7edab9) :point_right: ( [Kiho · Cham](https://github.com/Kiho · Cham) ) 1. [fix(antd): improve week type moment parse regex (#254)](https://github.com/alibaba/formily/commit/88654b80) :point_right: ( [Wayne Zhu](https://github.com/Wayne Zhu) ) 1. [fix(examples): remove the onChange of next/Detail (#257)](https://github.com/alibaba/formily/commit/62ae0cbb) :point_right: ( [atzcl](https://github.com/atzcl) ) 1. [fix(@uform/antd): add typings entry file (#250)](https://github.com/alibaba/formily/commit/a9063a2e) :point_right: ( [atzcl](https://github.com/atzcl) ) 1. [fix(@uform/core): add scheduler backward compat (#251)](https://github.com/alibaba/formily/commit/ed948348) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix FormTextBox margin value (#237)](https://github.com/alibaba/formily/commit/6148e332) :point_right: ( [合木](https://github.com/合木) ) 1. [fix validator of id card to support tail x (#227)](https://github.com/alibaba/formily/commit/33291e3e) :point_right: ( [合木](https://github.com/合木) ) 1. [fix(@uform/react): invariant initialValues will not be changed when form rerender (#214)](https://github.com/alibaba/formily/commit/b9efa4ca) :point_right: ( [Kiho · Cham](https://github.com/Kiho · Cham) ) 1. [fix(@uform/antd): Fix Antd Input loading state automatically loses focus (#207)](https://github.com/alibaba/formily/commit/3824942b) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(@uform/antd): support password add size props and use Input.Password in antd(#192)](https://github.com/alibaba/formily/commit/633dd302) :point_right: ( [atzcl](https://github.com/atzcl) ) 1. [fix(@uform/core): fix field props transformer is not work](https://github.com/alibaba/formily/commit/8686c7c8) :point_right: ( [合木](https://github.com/合木) ) 1. [fix(typings): correction FormLayout、Submit typings (#182)](https://github.com/alibaba/formily/commit/11dde612) :point_right: ( [atzcl](https://github.com/atzcl) ) 1. [fix(utils): adjust the order of getting self (#178)](https://github.com/alibaba/formily/commit/4ef2e1ca) :point_right: ( [Kiho · Cham](https://github.com/Kiho · Cham) ) 1. [fix(@uform/core): Fix the parameters of changeEditable api which have been defined in interface IField. (#180)](https://github.com/alibaba/formily/commit/54daf28d) :point_right: ( [Rain](https://github.com/Rain) ) 1. [fix(docs): fix docs without display property description (#176)](https://github.com/alibaba/formily/commit/24d12be5) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(typescript): fix typescript config](https://github.com/alibaba/formily/commit/546d9f01) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix(typescript): fix ts build can not transplie jsx](https://github.com/alibaba/formily/commit/0dfcba7c) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [fix: move string-length into utils (#154)](https://github.com/alibaba/formily/commit/b84803b4) :point_right: ( [Kevin Tan](https://github.com/Kevin Tan) ) 1. [fix(@uform/utils): fix setIn with number key can not auto create array](https://github.com/alibaba/formily/commit/48aa6d57) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix(@uform/utils): Fix the exception of setIn when undefiend value is assigned undefined property](https://github.com/alibaba/formily/commit/7cb63161) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix(@uform/core): Fix value synchronization of field state](https://github.com/alibaba/formily/commit/38dc0aa6) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix: antd select should not have max-width by default (#112)](https://github.com/alibaba/formily/commit/b4a494a1) :point_right: ( [Kevin Tan](https://github.com/Kevin Tan) ) 1. [fix(@uform/core): Fixed the value was not cached when the field was hidden #113](https://github.com/alibaba/formily/commit/402daff2) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix array table will show labels wrapped by form card](https://github.com/alibaba/formily/commit/60e0917b) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(@uform/utils): fix bug of every and some (#88)](https://github.com/alibaba/formily/commit/36ab9da0) :point_right: ( [Chen YuBen](https://github.com/Chen YuBen) ) 1. [fix(next-ts): fix ts lint errors](https://github.com/alibaba/formily/commit/759f4f24) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix(@uform/core): Fix the issue of the onFieldChange event after the Field is removed (#72)](https://github.com/alibaba/formily/commit/30cd1e56) :point_right: ( [Janry](https://github.com/Janry) ) 1. [fix(@uform/core): Optimize the 'errors' information structure](https://github.com/alibaba/formily/commit/be680e02) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix(project): Fix known issues. 1. Improve the Button API Description 2. Improve the Field API Description 3. Fix showLoading Submit Component is not work 4. Fix x-index is not work with array table 5. Improve Field Subtree Parsing Performance](https://github.com/alibaba/formily/commit/826ebce1) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix(@uform/react): Fixing index.d.ts can not found registerFormField. #29](https://github.com/alibaba/formily/commit/6c287413) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix(@uform/react): Fix Form List error in JSON Schema driver usecase #22](https://github.com/alibaba/formily/commit/6d11c4bd) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix(@uform/antd): fix upload field is not work when uploading some files #18](https://github.com/alibaba/formily/commit/fbc22e74) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix(@uform/core): fix setFormState Promise resolve is not wait rerender completed.](https://github.com/alibaba/formily/commit/d9a24d44) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix(@uform/react): fix field dynamic hidden will effect other field. When the virtual box without name is hidden in the dynamic display, it will affect the dynamic hiding of the adjacent virtual box.](https://github.com/alibaba/formily/commit/97bb44d9) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix(@uform/nexxt): fix time picker click will throw error](https://github.com/alibaba/formily/commit/e9659ac3) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix(docs): improve Form Schema](https://github.com/alibaba/formily/commit/83a3137f) :point_right: ( [harryyu](https://github.com/harryyu) ) 1. [fix(docs): fix docs can not scroll in ios](https://github.com/alibaba/formily/commit/a6e53c2e) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix(@uform/utils): fix isEmpty'result is not correct when ['','']](https://github.com/alibaba/formily/commit/091c2f17) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix(@uform/core): fix bug - fix bug that async schema default property is not work - fix bug that visible property is not work by setFieldState when FormInit](https://github.com/alibaba/formily/commit/8864ba99) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix(@uform/next/antd): fix FormButtonGroup will throw error when root component rerendering](https://github.com/alibaba/formily/commit/ccd93349) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix: 修改版本号](https://github.com/alibaba/formily/commit/a26a5013) :point_right: ( [cnt1992](https://github.com/cnt1992) ) 1. [fix(next): replace fusion next package name](https://github.com/alibaba/formily/commit/db2061e8) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [fix(pkg): add access=public to allow lerna to publish scoped package](https://github.com/alibaba/formily/commit/b41d1fab) :point_right: ( [janryWang](https://github.com/janryWang) ) ### :memo: Documents Changes 1. [docs: add @formily/tdesign-react links (#3265)](https://github.com/alibaba/formily/commit/57510b42) :point_right: ( [zFitness](https://github.com/zFitness) ) 1. [docs: fix typo (#3247)](https://github.com/alibaba/formily/commit/ac807c13) :point_right: ( [Weiqi Wu](https://github.com/Weiqi Wu) ) 1. [docs(reactive): add assignment statement (#3210)](https://github.com/alibaba/formily/commit/297532f8) :point_right: ( [zhangrenyang](https://github.com/zhangrenyang) ) 1. [docs: fix contribution.zh-CN error (doc -> docs) (#3202)](https://github.com/alibaba/formily/commit/a4974d23) :point_right: ( [Akong](https://github.com/Akong) ) 1. [docs(antd): fix Select component docs error (#3199)](https://github.com/alibaba/formily/commit/ee70cde1) :point_right: ( [微笑](https://github.com/微笑) ) 1. [docs: delete useless code (#3198)](https://github.com/alibaba/formily/commit/8ef12b43) :point_right: ( [zhangrenyang](https://github.com/zhangrenyang) ) 1. [docs: fix demo error (#3173)](https://github.com/alibaba/formily/commit/91e44698) :point_right: ( [PlutoCA](https://github.com/PlutoCA) ) 1. [docs: update codesandbox templates that use the latest formily (#2980)](https://github.com/alibaba/formily/commit/7bb26f98) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [docs: add vant link (#2851)](https://github.com/alibaba/formily/commit/de85f9f7) :point_right: ( [摇了摇头 oO](https://github.com/摇了摇头oO) ) 1. [docs: update issue-helper api](https://github.com/alibaba/formily/commit/ea4b1009) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs: fix a typo in Field.zh-CN.md (#2825)](https://github.com/alibaba/formily/commit/248ba3b0) :point_right: ( [stefango](https://github.com/stefango) ) 1. [docs(core): update setValidationLanguage to setValidateLanguage (#2674)](https://github.com/alibaba/formily/commit/31bc258d) :point_right: ( [JuFeng Zhang](https://github.com/JuFeng Zhang) ) 1. [docs(core): update form-path doc path](https://github.com/alibaba/formily/commit/7f901de7) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs: update qrcode](https://github.com/alibaba/formily/commit/fe10bfdb) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(core): improve docs (#2636)](https://github.com/alibaba/formily/commit/436dedbe) :point_right: ( [Hencky](https://github.com/Hencky) ) 1. [docs(element): update element brandName & codesandbox (#2608)](https://github.com/alibaba/formily/commit/26861a8f) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [docs(react): update field document urls (#2585)](https://github.com/alibaba/formily/commit/98628470) :point_right: ( [燃冰](https://github.com/燃冰) ) 1. [docs: improve site show brandName (#2574)](https://github.com/alibaba/formily/commit/483f79f1) :point_right: ( [Dark](https://github.com/Dark) ) 1. [docs(react): fix the typo on ISchemaFieldProps (#2528)](https://github.com/alibaba/formily/commit/0c5c6f1e) :point_right: ( [B2D1](https://github.com/B2D1) ) 1. [docs: update Chinese guide 1.x link (#2515)](https://github.com/alibaba/formily/commit/bf0d9b8b) :point_right: ( [csrigogogo](https://github.com/csrigogogo) ) 1. [docs: update structure image](https://github.com/alibaba/formily/commit/ad485978) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs: update QueryList docs (#2475)](https://github.com/alibaba/formily/commit/f84703b5) :point_right: ( [Janry](https://github.com/Janry) ) 1. [docs(core): update links in Form model Chinese doc (#2414)](https://github.com/alibaba/formily/commit/d6cdf71a) :point_right: ( [haloworld](https://github.com/haloworld) ) 1. [docs: fix scenes url (#2384)](https://github.com/alibaba/formily/commit/3538b171) :point_right: ( [PlutoCA](https://github.com/PlutoCA) ) 1. [docs: add issues-helper badge (#2359)](https://github.com/alibaba/formily/commit/a99feb43) :point_right: ( [xrkffgg](https://github.com/xrkffgg) ) 1. [docs(reactive): update reactive docs](https://github.com/alibaba/formily/commit/db4c35ff) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs: update errors to use selfErrors](https://github.com/alibaba/formily/commit/731ddc02) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(vue): add more scopedSlot content tests and readme (#2226)](https://github.com/alibaba/formily/commit/ff7e790f) :point_right: ( [lirui](https://github.com/lirui) ) 1. [docs(project): update login-register.md](https://github.com/alibaba/formily/commit/79f948b3) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [doc: fix typo for Ant Design in docs/guide/quick-start.md (#2201)](https://github.com/alibaba/formily/commit/151f6845) :point_right: ( [vagusX](https://github.com/vagusX) ) 1. [docs: add notice for onFormValuesChange (#2146)](https://github.com/alibaba/formily/commit/c8176e53) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [docs(site): update Pack on Demand doc (#2086)](https://github.com/alibaba/formily/commit/c0c50ace) :point_right: ( [vimvinter](https://github.com/vimvinter) ) 1. [docs(designable): add designable form docs](https://github.com/alibaba/formily/commit/fef3600d) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(site): improve home site contributors ui](https://github.com/alibaba/formily/commit/7592bafe) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(site): add serverless functions](https://github.com/alibaba/formily/commit/d872ea4c) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(site): update fragment linkage case](https://github.com/alibaba/formily/commit/7e5e2625) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(main): add schema fragment controlled case (#1852)](https://github.com/alibaba/formily/commit/2212486b) :point_right: ( [Janry](https://github.com/Janry) ) 1. [docs(project): add translated docs (#1822)](https://github.com/alibaba/formily/commit/79ab341f) :point_right: ( [Janry](https://github.com/Janry) ) 1. [docs(react): improve ObjectField demo code (#1727)](https://github.com/alibaba/formily/commit/ccfba03a) :point_right: ( [砂糖梨子](https://github.com/砂糖梨子) ) 1. [docs(core): fix HTML Anchor jump link (#1639)](https://github.com/alibaba/formily/commit/3feaf906) :point_right: ( [后浪](https://github.com/后浪) ) 1. [docs: issue helper add codesandbox template (#1623)](https://github.com/alibaba/formily/commit/a7d2726c) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [docs(core): fix Type declaration errors in the document and code of setFieldState method (#1605)](https://github.com/alibaba/formily/commit/bb4f175f) :point_right: ( [后浪](https://github.com/后浪) ) 1. [docs(core): add Type number and integer for ValidatorFormats (#1599)](https://github.com/alibaba/formily/commit/03591144) :point_right: ( [codetyphon](https://github.com/codetyphon) ) 1. [docs(json-schema): add definitions and doc](https://github.com/alibaba/formily/commit/e729e007) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(readme): add download stats](https://github.com/alibaba/formily/commit/09ec8e52) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(all): add inject global styles](https://github.com/alibaba/formily/commit/70852e91) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(issue-helper): improve issue-helper](https://github.com/alibaba/formily/commit/e4d10d13) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(react): improve schema static declarations document (#1310)](https://github.com/alibaba/formily/commit/02aee29f) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [docs(antd): fix antd time picker ref (#1282)](https://github.com/alibaba/formily/commit/affa40c4) :point_right: ( [Pandazki](https://github.com/Pandazki) ) 1. [docs(antd/next): add useIndex api](https://github.com/alibaba/formily/commit/b36efe4a) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(vue): update vue schema docs](https://github.com/alibaba/formily/commit/a54cf82b) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(site): add english doc](https://github.com/alibaba/formily/commit/fd75b1ec) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(main): fix main site docs](https://github.com/alibaba/formily/commit/cd6a3474) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(fusion): update fusion docs](https://github.com/alibaba/formily/commit/1256a385) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs: JOSN -> JSON (#1196)](https://github.com/alibaba/formily/commit/87837801) :point_right: ( [zkylearner](https://github.com/zkylearner) ) 1. [docs(all): fix lint](https://github.com/alibaba/formily/commit/5c7a77fb) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(formily): add quick-start doc](https://github.com/alibaba/formily/commit/e29857ee) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(antd): add form-layout doc](https://github.com/alibaba/formily/commit/f167a750) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(project): add contribution.md](https://github.com/alibaba/formily/commit/a6748df8) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [doc:improve validate documents (#1000)](https://github.com/alibaba/formily/commit/3a145304) :point_right: ( [wangmingxu](https://github.com/wangmingxu) ) 1. [docs(antd): mardown special symbol escape (#882)](https://github.com/alibaba/formily/commit/9c969cc9) :point_right: ( [kromalee](https://github.com/kromalee) ) 1. [docs: add type definition of x-linkages and x-mega-props (#876)](https://github.com/alibaba/formily/commit/c93b5171) :point_right: ( [Empireo](https://github.com/Empireo) ) 1. [docs(antd): fix registerVirtualBox demo (#834)](https://github.com/alibaba/formily/commit/02fcd0d4) :point_right: ( [kenve](https://github.com/kenve) ) 1. [docs(antd/component): fix labelAlign type and remove labelTextAlign (#817)](https://github.com/alibaba/formily/commit/3704873c) :point_right: ( [kenve](https://github.com/kenve) ) 1. [docs: fix spelling (#791)](https://github.com/alibaba/formily/commit/f27a66ba) :point_right: ( [kenve](https://github.com/kenve) ) 1. [docs: formatted with prettier (#768)](https://github.com/alibaba/formily/commit/cb7f095d) :point_right: ( [kenve](https://github.com/kenve) ) 1. [docs(antd-components): update import package name (#758)](https://github.com/alibaba/formily/commit/c038dbdd) :point_right: ( [Janry](https://github.com/Janry) ) 1. [docs: add introduction and support FormTab and support FieldState. unmountRemoveValue (#752)](https://github.com/alibaba/formily/commit/bfaa3ed7) :point_right: ( [Janry](https://github.com/Janry) ) 1. [doc(next/antd): array item docs optimization (#749)](https://github.com/alibaba/formily/commit/b12bce24) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [docs : add complex-field-component.md (#737)](https://github.com/alibaba/formily/commit/1235a11a) :point_right: ( [Janry](https://github.com/Janry) ) 1. [doc: add form and formitem (#700)](https://github.com/alibaba/formily/commit/aaa4742a) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [docs(@formily/react-schema-renderer): fix docs example (#681)](https://github.com/alibaba/formily/commit/a546e6a2) :point_right: ( [朱建](https://github.com/朱建) ) 1. [docs: update next/antd (#661)](https://github.com/alibaba/formily/commit/611125c7) :point_right: ( [quirkyvar](https://github.com/quirkyvar) ) 1. [docs(project): fix docs codesandbox config](https://github.com/alibaba/formily/commit/0c65601d) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [docs(examples): fix FormStep examples (#593)](https://github.com/alibaba/formily/commit/27018c6c) :point_right: ( [常泽清](https://github.com/常泽清) ) 1. [doc: add questions(customize action) (#289)](https://github.com/alibaba/formily/commit/baecbaab) :point_right: ( [xiaowanzi](https://github.com/xiaowanzi) ) 1. [docs(Submit): fix table style (#203)](https://github.com/alibaba/formily/commit/d59436b3) :point_right: ( [atzcl](https://github.com/atzcl) ) 1. [docs: add detail of createForm (#156)](https://github.com/alibaba/formily/commit/ae8bb439) :point_right: ( [Kevin Tan](https://github.com/Kevin Tan) ) 1. [docs: optimize demo of form detail in docs (#150)](https://github.com/alibaba/formily/commit/b04d4135) :point_right: ( [合木](https://github.com/合木) ) 1. [docs(antd-relations): fix MM visible toggle is not work](https://github.com/alibaba/formily/commit/a930f78c) :point_right: ( [Janry](https://github.com/Janry) ) 1. [docs(Field_React): fix rule description](https://github.com/alibaba/formily/commit/827cb26a) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [docs(questions): add Q/A](https://github.com/alibaba/formily/commit/b98c0565) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [docs(api): fix form text box docs](https://github.com/alibaba/formily/commit/69b3c5a9) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [docs(docs): remove statis](https://github.com/alibaba/formily/commit/3203efbe) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [docs: add HarrisFeng as a contributor (#93)](https://github.com/alibaba/formily/commit/255d153e) :point_right: ( [allcontributors[bot]](https://github.com/allcontributors[bot]) ) 1. [docs: improve the English version (#3)](https://github.com/alibaba/formily/commit/d592cbf9) :point_right: ( [Harry Yu](https://github.com/Harry Yu) ) 1. [docs(api): update SchemaForm API links](https://github.com/alibaba/formily/commit/0573af76) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [docs(site): move github pages==>netlify](https://github.com/alibaba/formily/commit/dc9abdfa) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [docs(all): sort api table](https://github.com/alibaba/formily/commit/930ce7aa) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [docs(API): Fix jump link can't jump in doc site. #59](https://github.com/alibaba/formily/commit/724affdb) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [docs: remove useless column in field api table (#61)](https://github.com/alibaba/formily/commit/49be9871) :point_right: ( [Kiho · Cham](https://github.com/Kiho · Cham) ) 1. [docs(@uform/docs): Optimize package bundle size](https://github.com/alibaba/formily/commit/c42ea06a) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [docs(examples): add international docs #25](https://github.com/alibaba/formily/commit/aaa22278) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [docs(site_pages): add gitter.im sidebar](https://github.com/alibaba/formily/commit/5809a987) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [docs(next/antd): add createAsyncFormActions docs](https://github.com/alibaba/formily/commit/4ab122e1) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [docs: add README.md](https://github.com/alibaba/formily/commit/52fc2c2d) :point_right: ( [cnt1992](https://github.com/cnt1992) ) ### :rose: Improve code quality 1. [refactor(vue): change Field component type to functional (#2773)](https://github.com/alibaba/formily/commit/ffbaba25) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [refactor(vue): switch type files for vue2/vue3 in postinstall (#2640)](https://github.com/alibaba/formily/commit/6015b7c8) :point_right: ( [月落音阑](https://github.com/月落音阑) ) 1. [refactor(grid): use data-grid-span for base grid span](https://github.com/alibaba/formily/commit/712aba94) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [refactor(core): revert field unmount to skip validate (#2379)](https://github.com/alibaba/formily/commit/8a016794) :point_right: ( [Janry](https://github.com/Janry) ) 1. [refactor(element): redesign form-grid and improve form-layout (#2337)](https://github.com/alibaba/formily/commit/9e468fae) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [refactor(antd/next/element): adjust the read priority of Form context](https://github.com/alibaba/formily/commit/f0c29bbc) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [refactor(react): silent useForm for child form sence (#2302)](https://github.com/alibaba/formily/commit/c2c2e305) :point_right: ( [Janry](https://github.com/Janry) ) 1. [refactor(core): reduce core package size (#2261)](https://github.com/alibaba/formily/commit/84f3fc1b) :point_right: ( [Janry](https://github.com/Janry) ) 1. [refactor(element): refactor element slot pass way (#2236)](https://github.com/alibaba/formily/commit/da28fe7e) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [refactor(project): support more features for page description (#2099)](https://github.com/alibaba/formily/commit/6162ad5d) :point_right: ( [Janry](https://github.com/Janry) ) 1. [refactor(json-schema): use with statement for compiler](https://github.com/alibaba/formily/commit/f913b35b) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [refactor(reactive): change model default batch annotation to action annotation](https://github.com/alibaba/formily/commit/6162639b) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [refactor(element): refactor FormDialog/FormDrawer & refactor component export type (#1892)](https://github.com/alibaba/formily/commit/cc3cb360) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [refactor(project): remove Formily.\* use cases (#1820)](https://github.com/alibaba/formily/commit/72a2958c) :point_right: ( [Janry](https://github.com/Janry) ) 1. [refactor(designable-ant): expose upload component's textContent property in setting form (#1818)](https://github.com/alibaba/formily/commit/15344449) :point_right: ( [nekic](https://github.com/nekic) ) 1. [refactor(reactive): fix #1598 and support #1586 and super performance optimization](https://github.com/alibaba/formily/commit/a1e72006) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [refactor(designable-antd): refactor and add DesignableArrayTable](https://github.com/alibaba/formily/commit/97c78dbd) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [refactor(antd/next): improve docs and support x-component/x-decorator ReactComponent style](https://github.com/alibaba/formily/commit/65bfef1e) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [refactor(core): controlled ==> designable](https://github.com/alibaba/formily/commit/ac79c196) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [refactor(reactive-react): refactor observer function (#1523)](https://github.com/alibaba/formily/commit/55b93420) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [refactor(antd/next): rewrite PreviewText to JSXComponent (#1509)](https://github.com/alibaba/formily/commit/3f6c34d2) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [refactor(json-schema): refactor stringify type to fix literal type is erased (#1508)](https://github.com/alibaba/formily/commit/43e79a61) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [refactor(core): modify IFormState type (#1434)](https://github.com/alibaba/formily/commit/57a7ea37) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [refactor(reactive): add benchmark scripts](https://github.com/alibaba/formily/commit/6954a1fb) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [refactor(project): update deps declaration](https://github.com/alibaba/formily/commit/0b846317) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [refactor: adjust the umd compilation process of the ui library (#1206)](https://github.com/alibaba/formily/commit/e3fc6ade) :point_right: ( [atzcl](https://github.com/atzcl) ) 1. [refactor: update rollup config (#1193)](https://github.com/alibaba/formily/commit/a8d119c0) :point_right: ( [Dark](https://github.com/Dark) ) 1. [refactor(antd): fine adjustment (#1188)](https://github.com/alibaba/formily/commit/ea022745) :point_right: ( [atzcl](https://github.com/atzcl) ) 1. [refactor: remove disabled, update props name, update NodeTypes enum(#1155)](https://github.com/alibaba/formily/commit/43972bae) :point_right: ( [soulwu](https://github.com/soulwu) ) 1. [refactor(project): remove react-shared-components](https://github.com/alibaba/formily/commit/6f6dbed4) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [refactor(devtools): update npm scripts](https://github.com/alibaba/formily/commit/c449fbbf) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [refactor(react): improve form-spy (#824)](https://github.com/alibaba/formily/commit/c4dc2144) :point_right: ( [Janry](https://github.com/Janry) ) 1. [refactor(@uform/react-schema-editor): update (#606)](https://github.com/alibaba/formily/commit/179cd62a) :point_right: ( [Andy](https://github.com/Andy) ) 1. [refactor:code and style refactor (#522)](https://github.com/alibaba/formily/commit/24b3503e) :point_right: ( [Andy](https://github.com/Andy) ) 1. [refactor(antd): adjust the handling of importing components on demand (#485)](https://github.com/alibaba/formily/commit/2fb41e9a) :point_right: ( [atzcl](https://github.com/atzcl) ) 1. [refactor(typings): update FormStep、dispatch、notify typings](https://github.com/alibaba/formily/commit/929ef2c6) :point_right: ( [atzcl](https://github.com/atzcl) ) 1. [refactor: 代码优化](https://github.com/alibaba/formily/commit/e9f2c04e) :point_right: ( [秋逢](https://github.com/秋逢) ) 1. [refactor: improve test case (#375)](https://github.com/alibaba/formily/commit/dfec008a) :point_right: ( [Janry](https://github.com/Janry) ) 1. [refactor(@uform/core): remove processing test case](https://github.com/alibaba/formily/commit/56835f9e) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [refactor(build): add build docs flow in CI and remove dynamic style inject](https://github.com/alibaba/formily/commit/1fb5cc07) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [refactor: next in TypeScript (#206)](https://github.com/alibaba/formily/commit/33e4bfb8) :point_right: ( [Kiho · Cham](https://github.com/Kiho · Cham) ) 1. [refactor: use isEqual instead of isEmpty](https://github.com/alibaba/formily/commit/41aa26e8) :point_right: ( [monkindey](https://github.com/monkindey) ) 1. [refactor(pkg): update eslint-plugin-react version](https://github.com/alibaba/formily/commit/a9f0c7ce) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [refactor(test): update react-test-library==>@test-library/react](https://github.com/alibaba/formily/commit/a97ffa0b) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [refactor(project): merge alibaba/uform master](https://github.com/alibaba/formily/commit/b050eeaa) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [refactor(pkg): add ts deps](https://github.com/alibaba/formily/commit/bfdfb822) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [refactor(project): move @alifd/next and antd dependencies to peerDependencies](https://github.com/alibaba/formily/commit/201a53d2) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [refactor(docs): rebuild docs](https://github.com/alibaba/formily/commit/18388943) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [refactor(ci): update .travis.yml](https://github.com/alibaba/formily/commit/9396e9d6) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [refactor(docs): move \_config.yml to root dir](https://github.com/alibaba/formily/commit/1670178a) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [refactor: monaco editor amd](https://github.com/alibaba/formily/commit/4535cbe0) :point_right: ( [cnt1992](https://github.com/cnt1992) ) 1. [refactor: split next version](https://github.com/alibaba/formily/commit/b77cedb1) :point_right: ( [cnt1992](https://github.com/cnt1992) ) 1. [refactor(builder): delete package-lock.json and config/jest](https://github.com/alibaba/formily/commit/d35820c4) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [refactor(gitignore): remove lib](https://github.com/alibaba/formily/commit/8677e38d) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [refactor(project): LESENCE.md ==> LICENSE.md](https://github.com/alibaba/formily/commit/1968d1f3) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [refactor(project): add test scripts It’s almost done](https://github.com/alibaba/formily/commit/e8a90213) :point_right: ( [janryWang](https://github.com/janryWang) ) ### :rocket: Improve Performance 1. [perf(core): improve form change trigger performance (#3236)](https://github.com/alibaba/formily/commit/8e8a661e) :point_right: ( [Janry](https://github.com/Janry) ) 1. [perf(antd/next): improve ArrayTable performance](https://github.com/alibaba/formily/commit/2c982289) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [perf: improve total performance 20% (#2589)](https://github.com/alibaba/formily/commit/2d981385) :point_right: ( [Janry](https://github.com/Janry) ) 1. [perf(path): use Map replace LRUMap](https://github.com/alibaba/formily/commit/1141e580) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [perf(reactive-react): improve performace with immediate](https://github.com/alibaba/formily/commit/6d6a18f4) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [perf(core): improve validate perf (#755)](https://github.com/alibaba/formily/commit/3ea64169) :point_right: ( [Janry](https://github.com/Janry) ) 1. [perf(core): fix nested path update performance (#722)](https://github.com/alibaba/formily/commit/130feeae) :point_right: ( [Janry](https://github.com/Janry) ) 1. [perf(array): shorten the code (#678)](https://github.com/alibaba/formily/commit/f8706760) :point_right: ( [Neil](https://github.com/Neil) ) ### :hammer_and_wrench: Update Workflow Scripts 1. [build: add peerDependenciesMeta (#3026)](https://github.com/alibaba/formily/commit/bbc2a51b) :point_right: ( [うまる](https://github.com/うまる) ) 1. [build(sourcemap): add "sourcesContent" to the output source map (#2399)](https://github.com/alibaba/formily/commit/3305cf80) :point_right: ( [zengguirong](https://github.com/zengguirong) ) 1. [build: fix build global may be failed (#1744)](https://github.com/alibaba/formily/commit/818aa132) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [build: fix git message sort incorrect (#1708)](https://github.com/alibaba/formily/commit/617ce88c) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [build: add sourcemap support (#1687)](https://github.com/alibaba/formily/commit/7bb433bb) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [build(shared): external path package](https://github.com/alibaba/formily/commit/be3ae401) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [build(project): buld project](https://github.com/alibaba/formily/commit/fc455da7) :point_right: ( [janryWang](https://github.com/janryWang) ) ### :construction: Add/Update Test Cases 1. [test(json-schema): add test of transformer in json-schema (#2975)](https://github.com/alibaba/formily/commit/c3228191) :point_right: ( [Zardddddd60](https://github.com/Zardddddd60) ) 1. [test(code): optimize test case of core/lifecycle (#2874)](https://github.com/alibaba/formily/commit/f1766ecc) :point_right: ( [Zardddddd60](https://github.com/Zardddddd60) ) 1. [test(reactive): adding missing tests and correcting existing tests (#2525)](https://github.com/alibaba/formily/commit/432f6204) :point_right: ( [Yiliang Wang](https://github.com/Yiliang Wang) ) 1. [test: update package.json](https://github.com/alibaba/formily/commit/288a8777) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [test(core): add designable tests (#1972)](https://github.com/alibaba/formily/commit/6a437c8b) :point_right: ( [Janry](https://github.com/Janry) ) 1. [test(core): nested reaction should recall the tracker (#1696)](https://github.com/alibaba/formily/commit/a6b81042) :point_right: ( [小黄黄](https://github.com/小黄黄) ) 1. [test: update jest config (#1634)](https://github.com/alibaba/formily/commit/f228a405) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [test(reactive): add mark tests and fix docs typo](https://github.com/alibaba/formily/commit/b3b2679e) :point_right: ( [gwsbhqt](https://github.com/gwsbhqt) ) 1. [test(project): update mobx => @formily/reactive](https://github.com/alibaba/formily/commit/7ae0a923) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [test(json-schema): update snapshot](https://github.com/alibaba/formily/commit/0c5947a8) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [test(validator): add some core tests](https://github.com/alibaba/formily/commit/c5236042) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [test(@uform/react): improve field and virtualField test cases (#438)](https://github.com/alibaba/formily/commit/853e051f) :point_right: ( [dahuang](https://github.com/dahuang) ) 1. [test(@uform/utils): add setIn testcase](https://github.com/alibaba/formily/commit/67a82e67) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [test(effects): remove unnecessary button tags](https://github.com/alibaba/formily/commit/7d25ac4c) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [test(project): add large test cases](https://github.com/alibaba/formily/commit/68fd2e1c) :point_right: ( [janryWang](https://github.com/janryWang) ) ### :blush: Other Changes 1. [chore(deps): bump moment from 2.29.3 to 2.29.4 (#3267)](https://github.com/alibaba/formily/commit/88df0daa) :point_right: ( [dependabot[bot]](https://github.com/dependabot[bot]) ) 1. [chore: remove getValueByValue](https://github.com/alibaba/formily/commit/2ca7aaf5) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(deps): bump parse-url from 6.0.0 to 6.0.2 (#3255)](https://github.com/alibaba/formily/commit/679fbb74) :point_right: ( [dependabot[bot]](https://github.com/dependabot[bot]) ) 1. [chore(reactive): improve strict mode update strategy](https://github.com/alibaba/formily/commit/c3002bde) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(deps-dev): bump semver-regex from 3.1.3 to 3.1.4 (#3166)](https://github.com/alibaba/formily/commit/ca97cae3) :point_right: ( [dependabot[bot]](https://github.com/dependabot[bot]) ) 1. [chore(reactive): revert batch tracker (#3112)](https://github.com/alibaba/formily/commit/604d74ac) :point_right: ( [Janry](https://github.com/Janry) ) 1. [chore: add carbon ad tag](https://github.com/alibaba/formily/commit/679efc54) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(antd/next): replace useForm to useParentForm in Form component](https://github.com/alibaba/formily/commit/43a3d6b8) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(antd/next): revert editable](https://github.com/alibaba/formily/commit/16a376d3) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(json-schema): improve typings](https://github.com/alibaba/formily/commit/d116d272) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore: change pr template and commit message specific link (#2742)](https://github.com/alibaba/formily/commit/129cd693) :point_right: ( [zhouxinyong](https://github.com/zhouxinyong) ) 1. [chore(grid): improve strictAutoFit](https://github.com/alibaba/formily/commit/d485a49e) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(next): export ExtendTableProps](https://github.com/alibaba/formily/commit/ad82905b) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore: improve code style (#2579)](https://github.com/alibaba/formily/commit/4a083bad) :point_right: ( [Janry](https://github.com/Janry) ) 1. [chore: add dingtalk notification for release](https://github.com/alibaba/formily/commit/35a18c48) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(element): update ts-import-plugin version (#2518)](https://github.com/alibaba/formily/commit/4f27990d) :point_right: ( [Muyao](https://github.com/Muyao) ) 1. [chore: add ESNext and DOM lib to TS compiler options (#2507)](https://github.com/alibaba/formily/commit/a51d1898) :point_right: ( [Yiliang Wang](https://github.com/Yiliang Wang) ) 1. [chore: fix yarn.lock](https://github.com/alibaba/formily/commit/8305c18f) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(antd/next): compat antd@4.17 and remove antd-icons from fusion package (#2492)](https://github.com/alibaba/formily/commit/cc325699) :point_right: ( [Janry](https://github.com/Janry) ) 1. [chore: change site domain v2.formilyjs.org -> formilyjs.org](https://github.com/alibaba/formily/commit/342493a0) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore: remove build global scripts (#2474)](https://github.com/alibaba/formily/commit/4cb7e9f9) :point_right: ( [Janry](https://github.com/Janry) ) 1. [chore: update workflow](https://github.com/alibaba/formily/commit/e84a4769) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(grid): update readme](https://github.com/alibaba/formily/commit/9738292c) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(desingbale): move designable-antd/next to designable repo](https://github.com/alibaba/formily/commit/84327d2d) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(workflow): fix actions](https://github.com/alibaba/formily/commit/12dacdcc) :point_right: ( [Janry](https://github.com/Janry) ) 1. [chore(designable): lock version](https://github.com/alibaba/formily/commit/b61ad907) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(react): compat ReactNative with SchemaField only json-schema mode](https://github.com/alibaba/formily/commit/77dd47e4) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(docs): add antd-formily-boost link](https://github.com/alibaba/formily/commit/4fb9ff8d) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(ts): map @formily/\* to src folder during development (#1917)](https://github.com/alibaba/formily/commit/65259a06) :point_right: ( [JuFeng Zhang](https://github.com/JuFeng Zhang) ) 1. [chore(validator): improve validator (#1918)](https://github.com/alibaba/formily/commit/b1681bff) :point_right: ( [Janry](https://github.com/Janry) ) 1. [chore(flow): add release.yml](https://github.com/alibaba/formily/commit/301a89c1) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(setters): improve DataSourceSetter ui](https://github.com/alibaba/formily/commit/1c12f543) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(core): improve display model (#1713)](https://github.com/alibaba/formily/commit/bad483da) :point_right: ( [Janry](https://github.com/Janry) ) 1. [chore(designable-antd): improve playgroun ui](https://github.com/alibaba/formily/commit/2d07630c) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(path): add benchmark case](https://github.com/alibaba/formily/commit/9533e049) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore: replace 'disgusting' with 'sophisticated' (#1574)](https://github.com/alibaba/formily/commit/d14c042e) :point_right: ( [Riting LIU](https://github.com/Riting LIU) ) 1. [chore(pkg): add workspaces](https://github.com/alibaba/formily/commit/d8af530e) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(github): update pr template](https://github.com/alibaba/formily/commit/b3149307) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(dumi): update next css link](https://github.com/alibaba/formily/commit/6843d946) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(pkg): update lint-staged scripts](https://github.com/alibaba/formily/commit/ddd8fc9a) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(project): prettier all code and change style behavior](https://github.com/alibaba/formily/commit/3792c221) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(scripts): remove mapCoverage.js](https://github.com/alibaba/formily/commit/3b3c3134) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(workflow): Update check-pr-title.yml (#1490)](https://github.com/alibaba/formily/commit/9243908d) :point_right: ( [xrkffgg](https://github.com/xrkffgg) ) 1. [chore(workflow): rename main.yml ==>commitlint.yml](https://github.com/alibaba/formily/commit/45734661) :point_right: ( [Janry](https://github.com/Janry) ) 1. [chore(actions): update commit checker action](https://github.com/alibaba/formily/commit/573b60fe) :point_right: ( [Janry](https://github.com/Janry) ) 1. [chore(pkg): add preversion/version lerna scripts hook](https://github.com/alibaba/formily/commit/d933f1fe) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(pkg): change the execution timing of the changelog generator](https://github.com/alibaba/formily/commit/0ff511f6) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore(scripts): slice changelog counts](https://github.com/alibaba/formily/commit/fead7843) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore: improve github pull request template (#1328)](https://github.com/alibaba/formily/commit/353e87a7) :point_right: ( [liuwei](https://github.com/liuwei) ) 1. [ci(core): fix tests](https://github.com/alibaba/formily/commit/faaceba0) :point_right: ( [janrywang](https://github.com/janrywang) ) 1. [chore: unify ts dependencies (#296)](https://github.com/alibaba/formily/commit/5268ce80) :point_right: ( [Kevin Tan](https://github.com/Kevin Tan) ) 1. [chore(travis): Guaranteed dependency peering (#288)](https://github.com/alibaba/formily/commit/97885c2c) :point_right: ( [atzcl](https://github.com/atzcl) ) 1. [chore(docs): UFrom --> UForm (#228)](https://github.com/alibaba/formily/commit/e55d8400) :point_right: ( [Kiho · Cham](https://github.com/Kiho · Cham) ) 1. [chore(docs): remove unused file and correct antd multiple example (#184)](https://github.com/alibaba/formily/commit/eee944f5) :point_right: ( [Kiho · Cham](https://github.com/Kiho · Cham) ) 1. [chore: fix tsc build errors (#174)](https://github.com/alibaba/formily/commit/c43397c1) :point_right: ( [Kevin Tan](https://github.com/Kevin Tan) ) 1. [chore: resolve the conflict](https://github.com/alibaba/formily/commit/22a7c32f) :point_right: ( [monkindey](https://github.com/monkindey) ) 1. [chore: remove tslint and use typescript-eslint (#159)](https://github.com/alibaba/formily/commit/97caa9cd) :point_right: ( [Kevin Tan](https://github.com/Kevin Tan) ) 1. [chore(project): release v0.1.15 (#94)](https://github.com/alibaba/formily/commit/bc3125d2) :point_right: ( [Janry](https://github.com/Janry) ) 1. [chore(scripts): correct git commit specific url)](https://github.com/alibaba/formily/commit/341b2ffb) :point_right: ( [monkindey](https://github.com/monkindey) ) 1. [chore(alpha): change version to v0.1.0-beta.20](https://github.com/alibaba/formily/commit/5bead131) :point_right: ( [janryWang](https://github.com/janryWang) ) 1. [chore: merge](https://github.com/alibaba/formily/commit/4b7aacb9) :point_right: ( [cnt1992](https://github.com/cnt1992) ) 1. [chore: delete no use files](https://github.com/alibaba/formily/commit/49deb94f) :point_right: ( [cnt1992](https://github.com/cnt1992) ) 1. [chore: rebuild](https://github.com/alibaba/formily/commit/2b95a387) :point_right: ( [cnt1992](https://github.com/cnt1992) ) 1. [chore: add react & react-dom in package.json](https://github.com/alibaba/formily/commit/3b814059) :point_right: ( [cnt1992](https://github.com/cnt1992) ) 1. [chore: upgrade webpack-dev-server](https://github.com/alibaba/formily/commit/2dfa848c) :point_right: ( [cnt1992](https://github.com/cnt1992) ) ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2015-present, Alibaba Group Holding Limited. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ English | [简体中文](./README.zh-cn.md)

PRs Welcome

--- ## Background In React, the whole tree rendering performance problem of the form is very obvious in the controlled mode. Especially for the scene of data linkage, it is easy to cause the page to be stuck. To solve this problem, we have distributed the management of the state of each form field, which significantly improves the performance of the form operations. At the same time, we deeply integrate the JSON Schema protocol to help you solve the problem of back-end driven form rendering quickly. ## Features - 🖼 Designable, You can quickly develop forms at low cost through [Form Builder](https://designable-antd.formilyjs.org/). - 🚀 High performance, fields managed independently, rather rerender the whole tree. - 💡 Integrated Alibaba Fusion and Ant Design components are guaranteed to work out of the box. - 🎨 JSON Schema applied for BackEnd. JSchema applied for FrontEnd. Two paradigms can be converted to each other. - 🏅 Side effects are managed independently, making form data linkages easier than ever before. - 🌯 Override most complicated form layout use cases. ## Form Builder ![https://designable-antd.formilyjs.org/](https://img.alicdn.com/imgextra/i3/O1CN01xAJj1y1wcGzXYc1Uq_!!6000000006328-2-tps-2980-1740.png) ## WebSite 2.0 https://formilyjs.org 1.0 https://v1.formilyjs.org ## Community - [formilyjs](https://github.com/formilyjs) - [designable](https://github.com/alibaba/designable) - [icejs](https://github.com/alibaba/ice) ## How to contribute? - [Contribute document](https://formilyjs.org/zh-CN/guide/contribution) ## Contributors This project exists thanks to all the people who contribute. ## LICENSE Formily is open source software licensed as [MIT](https://github.com/alibaba/formily/blob/master/LICENSE.md). ================================================ FILE: README.zh-cn.md ================================================ [English](./README.md) | 简体中文

PRs Welcome

--- ## 背景 在 React 中,在受控模式下,表单的整树渲染问题非常明显。特别是对于数据联动的场景,很容易导致页面卡顿,为了解决这个问题,我们将每个表单字段的状态做了分布式管理,从而大大提升了表单操作性能。同时,我们深度整合了 JSON Schema 协议,可以帮助您快速解决后端驱动表单渲染的问题。 ## 特性 - 🖼 可设计,借助 Form Builder 可以快速搭建表单 - 🚀 高性能,字段分布式渲染,大大减轻 React 渲染压力 - 💡 支持 Ant Design/Fusion Next 组件体系 - 🎨 JSX 标签化写法/JSON Schema 数据驱动方案无缝迁移过渡 - 🏅 副作用逻辑独立管理,涵盖各种复杂联动校验逻辑 - 🌯 支持各种表单复杂布局方案 ## Form Builder ![https://designable-antd.formilyjs.org/](https://img.alicdn.com/imgextra/i3/O1CN01xAJj1y1wcGzXYc1Uq_!!6000000006328-2-tps-2980-1740.png) ## 官网 2.0 https://formilyjs.org 1.0 https://v1.formilyjs.org ## 生态产品 - [formilyjs](https://github.com/formilyjs) - [designable](https://github.com/alibaba/designable) - [icejs](https://github.com/alibaba/ice) ## 如何贡献 - [贡献指南](https://formilyjs.org/zh-CN/guide/contribution) ## 贡献者 This project exists thanks to all the people who contribute. ## LICENSE Formily is open source software licensed as [MIT.](https://github.com/alibaba/formily/blob/master/LICENSE.md) ================================================ FILE: commitlint.config.js ================================================ module.exports = { extends: ['@commitlint/config-conventional'] } ================================================ FILE: devtools/.eslintrc ================================================ { "parser": "@typescript-eslint/parser", "extends": [ "plugin:react/recommended", "plugin:@typescript-eslint/recommended", "prettier/@typescript-eslint" ], "env": { "node": true }, "plugins": ["@typescript-eslint", "react", "prettier", "markdown"], "parserOptions": { "sourceType": "module", "ecmaVersion": 10, "ecmaFeatures": { "jsx": true } }, "settings": { "react": { "version": "detect" } }, "rules": { "prettier/prettier": 0, // don't force es6 functions to include space before paren "space-before-function-paren": 0, "react/prop-types": 0, "react/no-find-dom-node": 0, "react/display-name": 0, // allow specifying true explicitly for boolean props "react/jsx-boolean-value": 0, "react/no-did-update-set-state": 0, // maybe we should no-public "@typescript-eslint/explicit-member-accessibility": 0, "@typescript-eslint/interface-name-prefix": 0, "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/explicit-function-return-type": 0, "@typescript-eslint/no-parameter-properties": 0, "@typescript-eslint/array-type": 0, "@typescript-eslint/no-object-literal-type-assertion": 0, "@typescript-eslint/no-use-before-define": 0, "@typescript-eslint/no-unused-vars": 1, "@typescript-eslint/no-namespace": 0, "@typescript-eslint/ban-types": 0, "@typescript-eslint/adjacent-overload-signatures": 0, "@typescript-eslint/explicit-module-boundary-types": 0, "@typescript-eslint/triple-slash-reference": 0, "@typescript-eslint/no-empty-function": 0, "no-console": [ "error", { "allow": ["warn", "error", "info"] } ], "prefer-const": 0, "no-var": 1, "prefer-rest-params": 0 }, "overrides": [ { "files": ["**/*.md.{jsx,tsx}"], "processor": "markdown/markdown" }, { "files": ["**/*.md/*.{jsx,tsx}"], "rules": { "@typescript-eslint/no-unused-vars": "error", "no-unused-vars": "error", "no-console": "off", "react/display-name": "off", "react/prop-types": "off" } }, { "files": ["**/*.md/*.{js,ts}"], "rules": { "@typescript-eslint/no-unused-vars": "off", "no-unused-vars": "off", "no-console": "off", "react/display-name": "off", "react/prop-types": "off" } } ] } ================================================ FILE: devtools/chrome-extension/.npmignore ================================================ node_modules *.log build docs doc-site __tests__ .eslintrc jest.config.js tsconfig.json .umi src ================================================ FILE: devtools/chrome-extension/LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2015-present, Alibaba Group Holding Limited. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: devtools/chrome-extension/config/webpack.base.ts ================================================ import path from 'path' import fs from 'fs-extra' const getEntry = (src) => { return [path.resolve(__dirname, '../src/extension/', src)] } // 先确保删除package目录,再创建新的 const packageDir = path.resolve(__dirname, '../package') if (fs.existsSync(packageDir)) { fs.removeSync(packageDir) } fs.ensureDirSync(packageDir) fs.copy(path.resolve(__dirname, '../assets'), packageDir) fs.copy( path.resolve(__dirname, '../src/extension/manifest.json'), path.resolve(__dirname, '../package/manifest.json') ) export default { mode: 'development', devtool: 'inline-source-map', // 嵌入到源文件中 entry: { popup: getEntry('./popup.tsx'), devtools: getEntry('./devtools.tsx'), devpanel: getEntry('./devpanel.tsx'), content: getEntry('./content.ts'), backend: getEntry('./backend.ts'), demo: getEntry('../app/demo.tsx'), inject: getEntry('./inject.ts'), background: getEntry('./background.ts'), }, output: { path: path.resolve(__dirname, '../package'), filename: 'js/[name].bundle.js', }, resolve: { modules: ['node_modules'], extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], }, module: { rules: [ { test: /\.tsx?$/, use: [ { loader: require.resolve('ts-loader'), options: { transpileOnly: true, }, }, ], }, { test: /\.css$/, use: [ { loader: require.resolve('style-loader'), options: { singleton: true, }, }, require.resolve('css-loader'), ], }, { test: /\.html?$/, loader: require.resolve('file-loader'), options: { name: '[name].[ext]', }, }, ], }, } ================================================ FILE: devtools/chrome-extension/config/webpack.dev.ts ================================================ import baseConfig from './webpack.base' import HtmlWebpackPlugin from 'html-webpack-plugin' import webpack from 'webpack' import path from 'path' const PORT = 3000 const createPages = (pages) => { return pages.map(({ filename, template, chunk }) => { return new HtmlWebpackPlugin({ filename, template, inject: 'body', chunks: [chunk], }) }) } for (let key in baseConfig.entry) { if (Array.isArray(baseConfig.entry[key])) { baseConfig.entry[key].push( require.resolve('webpack/hot/dev-server'), `${require.resolve('webpack-dev-server/client')}?http://localhost:${PORT}` ) } } module.exports = { ...baseConfig, plugins: [ ...createPages([ { filename: 'index.html', template: path.resolve( __dirname, '../src/extension/views/devtools.ejs' ), chunk: 'demo', }, ]), new webpack.HotModuleReplacementPlugin(), ], devServer: { open: true, port: PORT, }, } ================================================ FILE: devtools/chrome-extension/config/webpack.prod.ts ================================================ import baseConfig from './webpack.base' import HtmlWebpackPlugin from 'html-webpack-plugin' import path from 'path' const createPages = (pages) => { return pages.map(({ filename, template, chunk }) => { return new HtmlWebpackPlugin({ filename, template, inject: 'body', chunks: [chunk], }) }) } module.exports = { ...baseConfig, mode: 'production', plugins: [ ...createPages([ { filename: 'popup.html', template: path.resolve(__dirname, '../src/extension/views/popup.ejs'), chunk: 'popup', }, { filename: 'devtools.html', template: path.resolve( __dirname, '../src/extension/views/devtools.ejs' ), chunk: 'devtools', }, { filename: 'devpanel.html', template: path.resolve( __dirname, '../src/extension/views/devpanel.ejs' ), chunk: 'devpanel', }, ]), ], } ================================================ FILE: devtools/chrome-extension/package.json ================================================ { "name": "@formily/chrome-extension", "version": "2.3.7", "private": true, "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/alibaba/formily.git" }, "types": "esm/index.d.ts", "bugs": { "url": "https://github.com/alibaba/formily/issues" }, "homepage": "https://github.com/alibaba/formily#readme", "engines": { "npm": ">=3.0.0" }, "scripts": { "build:devtools": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack-cli --config config/webpack.prod.ts", "build:zip": "rimraf package.zip && zip -r package.zip package", "start": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack-dev-server --config config/webpack.dev.ts" }, "dependencies": { "@formily/core": "2.3.7", "@formily/shared": "2.3.7", "react": "^18.0.0", "react-dom": "^18.0.0", "react-json-view": "^1.19.1", "react-treebeard": "^3.2.4" }, "publishConfig": { "access": "public" }, "gitHead": "2c44ae410a73f02735c63c6430e021a50e21f3ec" } ================================================ FILE: devtools/chrome-extension/src/app/components/FieldTree.tsx ================================================ import React, { useState, useEffect, useRef } from 'react' import styled from 'styled-components' import { FormPath, isObj } from '@formily/shared' import { Treebeard, decorators } from 'react-treebeard' import * as filters from './filter' import SearchBox from './SearchBox' const createTree = (dataSource: any, cursor?: any) => { const tree: any = {} const getParentPath = (key: string) => { let parentPath: FormPath = FormPath.parse(key) let i = 0 while (true) { parentPath = parentPath.parent() if (dataSource[parentPath.toString()]) { return parentPath } if (i > parentPath.segments.length) return parentPath i++ } } const findParent = (key: string): any => { const parentPath = getParentPath(key) const _findParent = (node: any) => { if (FormPath.parse(node.path).match(parentPath)) { return node } else { for (let i = 0; i < node?.children?.length; i++) { const parent = _findParent(node.children[i]) if (parent) { return parent } } } } return _findParent(tree) } Object.keys(dataSource || {}).forEach((key) => { if (key == '') { tree.name = 'Form' tree.path = key tree.toggled = true tree.data = dataSource[key] if (cursor && cursor.current && cursor.current.path === key) { tree.active = true cursor.current = tree } } else { const node: any = { name: key, path: key, toggled: true, data: dataSource[key], } if (cursor && cursor.current && cursor.current.path === key) { node.active = true cursor.current = node } const parent = findParent(key) if (parent) { node.name = (node.path || '').slice( parent && parent.path ? parent.path.length + 1 : 0 ) parent.children = parent.children || [] parent.children.push(node) } } }) return tree } const theme = { tree: { base: { listStyle: 'none', margin: 0, padding: 0, color: '#9DA5AB', fontFamily: 'lucida grande ,tahoma,verdana,arial,sans-serif', fontSize: '8px', background: 'none', marginBottom: '50px', }, node: { base: { position: 'relative', background: 'none', }, link: { cursor: 'pointer', position: 'relative', padding: '0px 5px', display: 'block', }, activeLink: { background: '#3D424A', }, toggle: { base: { position: 'relative', display: 'inline-block', verticalAlign: 'top', marginLeft: '-5px', height: '22px', width: '20px', zIndex: 2, }, wrapper: { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)', width: 4, height: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', }, height: 6, width: 4, arrow: { fill: '#9DA5AB', strokeWidth: 0, }, }, header: { base: { display: 'inline-block', verticalAlign: 'top', color: '#9DA5AB', }, connector: { width: '2px', height: '12px', borderLeft: 'solid 2px black', borderBottom: 'solid 2px black', position: 'absolute', top: '0px', left: '-21px', }, title: { lineHeight: '24px', verticalAlign: 'middle', }, }, subtree: { listStyle: 'none', paddingLeft: '19px', }, loading: { color: '#E2C089', }, }, }, } const Header = (props) => { const { node, style, customStyles } = props const title = node.data?.title ? node.data.title : '' return (
{ node.toggled = false }} >
{node.name} {isObj(title) ? ((title as any).title ?? '') : title}
) } const ToolBar = styled.div` border-bottom: 1px solid #3d424a; height: 20px; padding: 10px 10px; padding: 5px; overflow: auto; position: sticky; top: 0; background: #282c34; z-index: 100; ` export const FieldTree = styled(({ className, dataSource, onSelect }) => { const allDataRef = useRef(createTree(dataSource)) const cursor = useRef(allDataRef.current) const [keyword, setKeyword] = useState('') const searchTimer = useRef(null) const [data, setData] = useState(allDataRef.current) const filterData = () => { if (!keyword) return data const finded = filters.filterTree(data, keyword) return filters.expandFilteredNodes(finded, keyword) } const onToggle = (node: any, toggled: boolean) => { cursor.current.active = false node.active = true if (node.children && node.children.length) { node.toggled = toggled } cursor.current = node setData(data) if (onSelect) { onSelect(node) } } const onSearch = ({ target: { value } }) => { clearTimeout(searchTimer.current) searchTimer.current = setTimeout(() => { setKeyword(value.trim()) }, 100) } useEffect(() => { allDataRef.current = createTree(dataSource, cursor) setData(allDataRef.current) }, [dataSource]) return (
) })` position: relative; overflow: auto; height: calc(100% - 40px); user-select: none; .highlight { position: absolute; top: 0; right: 0; left: -100%; height: 100%; z-index: 0; &.active { background: #3d424a; } } .node-header:hover .highlight { background: #3d424a; } ` ================================================ FILE: devtools/chrome-extension/src/app/components/LeftPanel.tsx ================================================ import React, { useState } from 'react' import { Tabs } from './Tabs' import { FieldTree } from './FieldTree' import styled from 'styled-components' export const LeftPanel = styled(({ className, dataSource, onSelect }) => { const [current, setCurrent] = useState(0) return (
{ setCurrent(index) onSelect({ current: index, key: '', }) }} /> { if (onSelect) { onSelect({ current, key: node.path, }) } }} />
) })` width: 50%; min-width: 50%; ` ================================================ FILE: devtools/chrome-extension/src/app/components/RightPanel.tsx ================================================ import React from 'react' import styled from 'styled-components' import ReactJson from 'react-json-view' export const RightPanel = styled(({ className, dataSource }) => { return (
) })` border-left: 1px solid #3d424a; flex-grow: 2; overflow: auto; padding: 10px; .react-json-view { background: none !important; font-size: 12px !important; } ` ================================================ FILE: devtools/chrome-extension/src/app/components/SearchBox.tsx ================================================ import React from 'react' import styled from 'styled-components' const SerachBox = styled.div` display: flex; align-items: center; height: 100%; .input-addon { padding: 0 5px; } .form-control { width: 50%; border: none; background: transparent; color: white; outline: none; } ` const SearchIcon = () => { return ( ) } export default ({ onSearch }) => { return (
) } ================================================ FILE: devtools/chrome-extension/src/app/components/Tabs.tsx ================================================ import React from 'react' import styled from 'styled-components' import { toArr } from '@formily/shared' export const Tabs = styled(({ className, dataSource, current, onChange }) => { current = current || 0 return (
{toArr(dataSource).map((item, index) => { return (
{ if (onChange) { onChange(index) } }} > Form#{index + 1}
) })}
) })` height: 36px; border-bottom: 1px solid #3d424a; display: flex; line-height: 36px; width: 100%; overflow: scroll; &::-webkit-scrollbar { display: none; } .tab-item { cursor: pointer; transition: 0.15s all ease-in-out; border-right: 1px solid #3d424a; padding: 0 10px; font-size: 12px; &:hover { background: #1d1f25; } &.active { background: #1d1f25; } } ` ================================================ FILE: devtools/chrome-extension/src/app/components/filter.ts ================================================ // Helper functions for filtering export const defaultMatcher = (filterText, node) => { return node.name.toLowerCase().indexOf(filterText.toLowerCase()) !== -1 } export const findNode = (node, filter, matcher) => { return ( matcher(filter, node) || // i match (node.children && // or i have decendents and one of them match node.children.length && !!node.children.find((child) => findNode(child, filter, matcher))) ) } export const filterTree = (node, filter, matcher = defaultMatcher) => { // If im an exact match then all my children get to stay if (matcher(filter, node) || !node.children) { return node } // If not then only keep the ones that match or have matching descendants const filtered = node.children .filter((child) => findNode(child, filter, matcher)) .map((child) => filterTree(child, filter, matcher)) return Object.assign({}, node, { children: filtered }) } export const expandFilteredNodes = (node, filter, matcher = defaultMatcher) => { let children = node.children if (!children || children.length === 0) { return Object.assign({}, node, { toggled: false }) } const childrenWithMatches = node.children.filter((child) => findNode(child, filter, matcher) ) const shouldExpand = childrenWithMatches.length > 0 // If im going to expand, go through all the matches and see if thier children need to expand if (shouldExpand) { children = childrenWithMatches.map((child) => { return expandFilteredNodes(child, filter, matcher) }) } return Object.assign({}, node, { children: children, toggled: shouldExpand, }) } ================================================ FILE: devtools/chrome-extension/src/app/demo.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom' import App from './index' const dataSource = [ { '': { pristine: false, valid: true, invalid: false, loading: false, validating: false, initialized: true, submitting: false, errors: [], warnings: [], values: { aa: true, cc: true, gg: 'aaaa', }, initialValues: { aa: true, cc: true, }, mounted: true, unmounted: false, props: {}, displayName: 'FormState', }, block: { name: 'block', path: 'block', initialized: true, visible: true, display: true, mounted: true, unmounted: false, props: { key: 'block', type: 'object', name: 'block', 'x-component': 'block', 'x-props': { title: 'Block1', }, 'x-component-props': { title: 'Block1', }, }, displayName: 'VirtualFieldState', }, 'block.aa': { name: 'aa', path: 'block.aa', dataType: 'boolean', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: [true], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: true, initialValue: true, rules: [], required: false, mounted: true, unmounted: false, props: { key: 'aa', name: 'aa', type: 'boolean', 'x-component': 'radio', default: true, enum: [ { label: '是', value: true, }, { label: '否', value: false, }, ], title: '是否隐藏AA', }, displayName: 'FieldState', }, 'block.bb': { name: 'bb', path: 'block.bb', dataType: 'string', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: false, display: true, loading: false, validating: false, errors: [], values: [null], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, rules: [], required: false, mounted: true, unmounted: false, props: { key: 'bb', name: 'bb', type: 'string', title: 'AA', }, displayName: 'FieldState', }, 'block.cc': { name: 'cc', path: 'block.cc', dataType: 'boolean', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: [true], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: true, initialValue: true, rules: [], required: false, mounted: true, unmounted: false, props: { key: 'cc', name: 'cc', type: 'boolean', title: '是否隐藏DD', default: true, 'x-component': 'radio', enum: [ { label: '是', value: true, }, { label: '否', value: false, }, ], }, displayName: 'FieldState', }, dd: { name: 'dd', path: 'dd', initialized: true, visible: false, display: true, mounted: true, unmounted: false, props: { key: 'dd', type: 'object', name: 'dd', 'x-component': 'block', 'x-props': { title: 'Block2', }, 'x-component-props': { title: 'Block2', }, }, displayName: 'VirtualFieldState', }, kk: { name: 'kk', path: 'kk', initialized: true, visible: true, display: true, mounted: true, unmounted: false, props: { key: 'kk', type: 'object', name: 'kk', 'x-component': 'block', 'x-props': { title: 'Block3', }, 'x-component-props': { title: 'Block3', }, }, displayName: 'VirtualFieldState', }, 'kk.gg': { name: 'gg', path: 'kk.gg', dataType: 'string', initialized: true, pristine: false, valid: true, modified: true, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: ['aaaa'], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: 'aaaa', rules: [], required: false, mounted: true, unmounted: false, props: { key: 'gg', name: 'gg', type: 'string', title: 'GG', 'x-props': { showSearch: true, filterLocal: false, style: { width: 200, }, }, enum: [ { label: 'aaaa', value: 'aaaa', extra: ['x1', 'x2', 'x3'], }, { label: 'bbbb', value: 'bbbb', extra: ['x4', 'x5', 'x6'], }, { label: 'cccc', value: 'cccc', extra: ['x7', 'x8', 'x9'], }, ], }, displayName: 'FieldState', }, 'kk.hh': { name: 'hh', path: 'kk.hh', dataType: 'string', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: false, display: true, loading: false, validating: false, errors: [], values: [null], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, rules: [], required: false, mounted: true, unmounted: false, props: { key: 'hh', name: 'hh', type: 'string', title: 'HH', enum: [], 'x-props': { style: { width: 200, }, }, }, displayName: 'FieldState', }, }, { '': { pristine: true, valid: true, invalid: false, loading: false, validating: false, initialized: true, submitting: false, errors: [], warnings: [], values: {}, initialValues: {}, mounted: true, unmounted: false, props: {}, displayName: 'FormState', }, total: { name: 'total', path: 'total', dataType: 'number', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: [null], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, rules: [ { required: true, }, ], required: true, mounted: true, unmounted: false, props: { key: 'total', name: 'total', type: 'number', required: true, title: '总价', }, displayName: 'FieldState', }, count: { name: 'count', path: 'count', dataType: 'number', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: [null], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, rules: [ { required: true, }, ], required: true, mounted: true, unmounted: false, props: { key: 'count', name: 'count', type: 'number', required: true, title: '数量', }, displayName: 'FieldState', }, price: { name: 'price', path: 'price', dataType: 'number', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: [null], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, rules: [ { required: true, }, ], required: true, mounted: true, unmounted: false, props: { key: 'price', name: 'price', type: 'number', required: true, title: '单价', }, displayName: 'FieldState', }, }, { '': { pristine: true, valid: true, invalid: false, loading: false, validating: false, initialized: true, submitting: false, errors: [], warnings: [], values: {}, initialValues: {}, mounted: true, unmounted: false, props: {}, displayName: 'FormState', }, NO_NAME_FIELD_$0: { name: 'NO_NAME_FIELD_$0', path: 'NO_NAME_FIELD_$0', initialized: true, visible: true, display: true, mounted: true, unmounted: false, props: { key: 'NO_NAME_FIELD_$0', type: 'object', name: 'NO_NAME_FIELD_$0', 'x-component': 'block', 'x-props': { title: 'Block1', }, 'x-component-props': { title: 'Block1', }, }, displayName: 'VirtualFieldState', }, 'NO_NAME_FIELD_$0.aa': { name: 'aa', path: 'NO_NAME_FIELD_$0.aa', dataType: 'string', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: [null], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, rules: [], required: false, mounted: true, unmounted: false, props: { key: 'aa', name: 'aa', type: 'string', enum: ['aaaaa', 'bbbbb', 'ccccc', 'ddddd', 'eeeee'], title: 'AA', }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.bb': { name: 'bb', path: 'NO_NAME_FIELD_$0.bb', dataType: 'string', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: false, display: true, loading: false, validating: false, errors: [], values: [null], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, rules: [], required: false, mounted: true, unmounted: false, props: { key: 'bb', type: 'string', name: 'bb', title: 'BB', enum: [], }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.cc': { name: 'cc', path: 'NO_NAME_FIELD_$0.cc', dataType: 'string', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: [null], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, rules: [], required: false, mounted: true, unmounted: false, props: { key: 'cc', type: 'string', name: 'cc', title: 'CC', }, displayName: 'FieldState', }, }, { '': { pristine: true, valid: true, invalid: false, loading: false, validating: false, initialized: true, submitting: false, errors: [], warnings: [], values: {}, initialValues: {}, mounted: true, unmounted: false, props: {}, displayName: 'FormState', }, aa: { name: 'aa', path: 'aa', dataType: 'string', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: [null], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, rules: [ { required: true, }, ], required: true, mounted: true, unmounted: false, props: { key: 'aa', name: 'aa', type: 'string', required: true, title: 'AA', }, displayName: 'FieldState', }, bb: { name: 'bb', path: 'bb', dataType: 'string', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: [null], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, rules: [], required: false, mounted: true, unmounted: false, props: { key: 'bb', type: 'string', name: 'bb', title: 'BB', enum: ['111', '222'], }, displayName: 'FieldState', }, }, { '': { pristine: false, valid: true, invalid: false, loading: false, validating: false, initialized: true, submitting: false, errors: [], warnings: [], values: { aa: [ { bb: 'aaaaa', dd: [ { ff: '是', ee: '是', }, ], cc: '1111', }, { bb: 'ccccc', dd: [ { ff: '是', ee: '否', }, ], cc: '1111', }, ], }, initialValues: { aa: [ { bb: 'aaaaa', dd: [ { ee: '是', ff: '是', }, ], }, { bb: 'ccccc', dd: [ { ee: '否', ff: '是', }, ], }, ], }, mounted: true, unmounted: false, props: {}, displayName: 'FormState', }, NO_NAME_FIELD_$0: { name: 'NO_NAME_FIELD_$0', path: 'NO_NAME_FIELD_$0', initialized: true, visible: true, display: true, mounted: true, unmounted: false, props: { key: 'NO_NAME_FIELD_$0', type: 'object', name: 'NO_NAME_FIELD_$0', 'x-component': 'block', 'x-props': { title: 'Block1', }, 'x-component-props': { title: 'Block1', }, }, displayName: 'VirtualFieldState', }, 'NO_NAME_FIELD_$0.aa': { name: 'aa', path: 'NO_NAME_FIELD_$0.aa', dataType: 'array', initialized: true, pristine: false, valid: true, modified: true, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: [ [ { bb: 'aaaaa', dd: [ { ff: '是', ee: '是', }, ], cc: '1111', }, { bb: 'ccccc', dd: [ { ff: '是', ee: '否', }, ], cc: '1111', }, ], ], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: [ { bb: 'aaaaa', dd: [ { ff: '是', ee: '是', }, ], cc: '1111', }, { bb: 'ccccc', dd: [ { ff: '是', ee: '否', }, ], cc: '1111', }, ], initialValue: [ { bb: 'aaaaa', dd: [ { ee: '是', ff: '是', }, ], }, { bb: 'ccccc', dd: [ { ee: '否', ff: '是', }, ], }, ], rules: [], required: false, mounted: true, unmounted: false, props: { key: 'aa', type: 'array', name: 'aa', }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.aa.0': { name: 'aa.0', path: 'NO_NAME_FIELD_$0.aa.0', dataType: 'object', initialized: true, pristine: false, valid: true, modified: true, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: [ { bb: 'aaaaa', dd: [ { ff: '是', ee: '是', }, ], cc: '1111', }, ], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: { bb: 'aaaaa', dd: [ { ff: '是', ee: '是', }, ], cc: '1111', }, initialValue: { bb: 'aaaaa', dd: [ { ee: '是', ff: '是', }, ], }, rules: [], required: false, mounted: true, unmounted: false, props: { type: 'object', }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$1': { name: 'aa.0.NO_NAME_FIELD_$1', path: 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$1', initialized: true, visible: true, display: true, mounted: true, unmounted: false, props: { key: 'NO_NAME_FIELD_$1', type: 'object', name: 'NO_NAME_FIELD_$1', 'x-component': 'block', 'x-props': { title: '基本信息', }, 'x-component-props': { title: '基本信息', }, }, displayName: 'VirtualFieldState', }, 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$1.NO_NAME_FIELD_$2': { name: 'aa.0.NO_NAME_FIELD_$2', path: 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$1.NO_NAME_FIELD_$2', initialized: true, visible: true, display: true, mounted: true, unmounted: false, props: { key: 'NO_NAME_FIELD_$2', type: 'object', name: 'NO_NAME_FIELD_$2', 'x-component': 'layout', 'x-props': { inline: true, }, 'x-component-props': { inline: true, }, }, displayName: 'VirtualFieldState', }, 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$1.NO_NAME_FIELD_$2.bb': { name: 'aa.0.bb', path: 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$1.NO_NAME_FIELD_$2.bb', dataType: 'string', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: ['aaaaa'], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: 'aaaaa', initialValue: 'aaaaa', rules: [], required: false, mounted: true, unmounted: false, props: { key: 'bb', type: 'string', name: 'bb', enum: ['aaaaa', 'bbbbb', 'ccccc', 'ddddd', 'eeeee'], title: 'BB', }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$1.NO_NAME_FIELD_$2.cc': { name: 'aa.0.cc', path: 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$1.NO_NAME_FIELD_$2.cc', dataType: 'string', initialized: true, pristine: false, valid: true, modified: true, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: ['1111'], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: '1111', rules: [], required: false, mounted: true, unmounted: false, props: { key: 'cc', type: 'string', name: 'cc', enum: ['1111', '2222'], title: 'CC', }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$1.NO_NAME_FIELD_$2.gg': { name: 'aa.0.gg', path: 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$1.NO_NAME_FIELD_$2.gg', dataType: 'string', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: [null], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, rules: [], required: false, mounted: true, unmounted: false, props: { key: 'gg', type: 'string', name: 'gg', title: 'GG', 'x-props': { style: { width: 200, }, }, }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$3': { name: 'aa.0.NO_NAME_FIELD_$3', path: 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$3', initialized: true, visible: true, display: true, mounted: true, unmounted: false, props: { key: 'NO_NAME_FIELD_$3', type: 'object', name: 'NO_NAME_FIELD_$3', 'x-component': 'block', 'x-props': { title: '嵌套Array', }, 'x-component-props': { title: '嵌套Array', }, }, displayName: 'VirtualFieldState', }, 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$3.dd': { name: 'aa.0.dd', path: 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$3.dd', dataType: 'array', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: [ [ { ee: '是', ff: '是', }, ], ], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: [ { ee: '是', ff: '是', }, ], initialValue: [ { ee: '是', ff: '是', }, ], rules: [], required: false, mounted: true, unmounted: false, props: { key: 'dd', type: 'array', name: 'dd', }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$3.dd.0': { name: 'aa.0.dd.0', path: 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$3.dd.0', dataType: 'object', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: [ { ee: '是', ff: '是', }, ], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: { ee: '是', ff: '是', }, initialValue: { ee: '是', ff: '是', }, rules: [], required: false, mounted: true, unmounted: false, props: { type: 'object', }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$3.dd.0.NO_NAME_FIELD_$4': { name: 'aa.0.dd.0.NO_NAME_FIELD_$4', path: 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$3.dd.0.NO_NAME_FIELD_$4', initialized: true, visible: true, display: true, mounted: true, unmounted: false, props: { key: 'NO_NAME_FIELD_$4', type: 'object', name: 'NO_NAME_FIELD_$4', 'x-component': 'layout', 'x-props': { inline: true, style: { marginLeft: 20, }, }, 'x-component-props': { inline: true, style: { marginLeft: 20, }, }, }, displayName: 'VirtualFieldState', }, 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$3.dd.0.NO_NAME_FIELD_$4.ee': { name: 'aa.0.dd.0.ee', path: 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$3.dd.0.NO_NAME_FIELD_$4.ee', dataType: 'string', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: ['是'], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: '是', initialValue: '是', rules: [], required: false, mounted: true, unmounted: false, props: { key: 'ee', type: 'string', name: 'ee', enum: ['是', '否'], title: 'EE', description: '是否显示GG', }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$3.dd.0.NO_NAME_FIELD_$4.ff': { name: 'aa.0.dd.0.ff', path: 'NO_NAME_FIELD_$0.aa.0.NO_NAME_FIELD_$3.dd.0.NO_NAME_FIELD_$4.ff', dataType: 'string', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: ['是'], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: '是', initialValue: '是', rules: [], required: false, mounted: true, unmounted: false, props: { key: 'ff', type: 'string', name: 'ff', default: '是', enum: ['是', '否'], title: 'FF', description: '是否显示EE', }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.aa.1': { name: 'aa.1', path: 'NO_NAME_FIELD_$0.aa.1', dataType: 'object', initialized: true, pristine: false, valid: true, modified: true, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: [ { bb: 'ccccc', dd: [ { ff: '是', ee: '否', }, ], cc: '1111', }, ], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: { bb: 'ccccc', dd: [ { ff: '是', ee: '否', }, ], cc: '1111', }, initialValue: { bb: 'ccccc', dd: [ { ee: '否', ff: '是', }, ], }, rules: [], required: false, mounted: true, unmounted: false, props: { type: 'object', }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$1': { name: 'aa.1.NO_NAME_FIELD_$1', path: 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$1', initialized: true, visible: true, display: true, mounted: true, unmounted: false, props: { key: 'NO_NAME_FIELD_$1', type: 'object', name: 'NO_NAME_FIELD_$1', 'x-component': 'block', 'x-props': { title: '基本信息', }, 'x-component-props': { title: '基本信息', }, }, displayName: 'VirtualFieldState', }, 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$1.NO_NAME_FIELD_$2': { name: 'aa.1.NO_NAME_FIELD_$2', path: 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$1.NO_NAME_FIELD_$2', initialized: true, visible: true, display: true, mounted: true, unmounted: false, props: { key: 'NO_NAME_FIELD_$2', type: 'object', name: 'NO_NAME_FIELD_$2', 'x-component': 'layout', 'x-props': { inline: true, }, 'x-component-props': { inline: true, }, }, displayName: 'VirtualFieldState', }, 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$1.NO_NAME_FIELD_$2.bb': { name: 'aa.1.bb', path: 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$1.NO_NAME_FIELD_$2.bb', dataType: 'string', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: ['ccccc'], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: 'ccccc', initialValue: 'ccccc', rules: [], required: false, mounted: true, unmounted: false, props: { key: 'bb', type: 'string', name: 'bb', enum: ['aaaaa', 'bbbbb', 'ccccc', 'ddddd', 'eeeee'], title: 'BB', }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$1.NO_NAME_FIELD_$2.cc': { name: 'aa.1.cc', path: 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$1.NO_NAME_FIELD_$2.cc', dataType: 'string', initialized: true, pristine: false, valid: true, modified: true, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: ['1111'], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: '1111', rules: [], required: false, mounted: true, unmounted: false, props: { key: 'cc', type: 'string', name: 'cc', enum: ['1111', '2222'], title: 'CC', }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$1.NO_NAME_FIELD_$2.gg': { name: 'aa.1.gg', path: 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$1.NO_NAME_FIELD_$2.gg', dataType: 'string', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: false, display: true, loading: false, validating: false, errors: [], values: [null], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, rules: [], required: false, mounted: true, unmounted: false, props: { key: 'gg', type: 'string', name: 'gg', title: 'GG', 'x-props': { style: { width: 200, }, }, }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$3': { name: 'aa.1.NO_NAME_FIELD_$3', path: 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$3', initialized: true, visible: true, display: true, mounted: true, unmounted: false, props: { key: 'NO_NAME_FIELD_$3', type: 'object', name: 'NO_NAME_FIELD_$3', 'x-component': 'block', 'x-props': { title: '嵌套Array', }, 'x-component-props': { title: '嵌套Array', }, }, displayName: 'VirtualFieldState', }, 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$3.dd': { name: 'aa.1.dd', path: 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$3.dd', dataType: 'array', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: [ [ { ee: '否', ff: '是', }, ], ], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: [ { ee: '否', ff: '是', }, ], initialValue: [ { ee: '否', ff: '是', }, ], rules: [], required: false, mounted: true, unmounted: false, props: { key: 'dd', type: 'array', name: 'dd', }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$3.dd.0': { name: 'aa.1.dd.0', path: 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$3.dd.0', dataType: 'object', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: [ { ee: '否', ff: '是', }, ], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: { ee: '否', ff: '是', }, initialValue: { ee: '否', ff: '是', }, rules: [], required: false, mounted: true, unmounted: false, props: { type: 'object', }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$3.dd.0.NO_NAME_FIELD_$4': { name: 'aa.1.dd.0.NO_NAME_FIELD_$4', path: 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$3.dd.0.NO_NAME_FIELD_$4', initialized: true, visible: true, display: true, mounted: true, unmounted: false, props: { key: 'NO_NAME_FIELD_$4', type: 'object', name: 'NO_NAME_FIELD_$4', 'x-component': 'layout', 'x-props': { inline: true, style: { marginLeft: 20, }, }, 'x-component-props': { inline: true, style: { marginLeft: 20, }, }, }, displayName: 'VirtualFieldState', }, 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$3.dd.0.NO_NAME_FIELD_$4.ee': { name: 'aa.1.dd.0.ee', path: 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$3.dd.0.NO_NAME_FIELD_$4.ee', dataType: 'string', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: ['否'], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: '否', initialValue: '否', rules: [], required: false, mounted: true, unmounted: false, props: { key: 'ee', type: 'string', name: 'ee', enum: ['是', '否'], title: 'EE', description: '是否显示GG', }, displayName: 'FieldState', }, 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$3.dd.0.NO_NAME_FIELD_$4.ff': { name: 'aa.1.dd.0.ff', path: 'NO_NAME_FIELD_$0.aa.1.NO_NAME_FIELD_$3.dd.0.NO_NAME_FIELD_$4.ff', dataType: 'string', initialized: true, pristine: true, valid: true, modified: false, touched: false, active: false, visited: false, invalid: false, visible: true, display: true, loading: false, validating: false, errors: [], values: ['是'], ruleErrors: [], ruleWarnings: [], effectErrors: [], warnings: [], effectWarnings: [], editable: true, value: '是', initialValue: '是', rules: [], required: false, mounted: true, unmounted: false, props: { key: 'ff', type: 'string', name: 'ff', default: '是', enum: ['是', '否'], title: 'FF', description: '是否显示EE', }, displayName: 'FieldState', }, }, ] ReactDOM.render( , document.getElementById('root') ) ================================================ FILE: devtools/chrome-extension/src/app/index.tsx ================================================ import React, { useState } from 'react' import { LeftPanel } from './components/LeftPanel' import { RightPanel } from './components/RightPanel' import styled from 'styled-components' export default styled(({ className, dataSource }) => { const [selected, select] = useState({ current: 0, key: '', }) return (
{ select(info) if (chrome && chrome.devtools && chrome.devtools.inspectedWindow) { chrome.devtools.inspectedWindow.eval( `window.__FORMILY_DEV_TOOLS_HOOK__.setVm("${info.key}","${ dataSource[info.current][''].id }")` ) } }} />
) })` display: flex; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow: hidden; color: #36d4c7; background: #282c34; ` ================================================ FILE: devtools/chrome-extension/src/extension/backend.ts ================================================ //inject content script const serializeObject = (obj: any) => { const seens = new WeakMap() const serialize = (obj: any) => { if (Array.isArray(obj)) { return obj.map(serialize) } else if (typeof obj === 'function') { return `f ${obj.displayName || obj.name}(){ }` } else if (typeof obj === 'object') { if (seens.get(obj)) return '#CircularReference' if (!obj) return obj if ('$$typeof' in obj && '_owner' in obj) { seens.set(obj, true) return '#ReactNode' } else if (obj.toJS) { seens.set(obj, true) return obj.toJS() } else if (obj.toJSON) { seens.set(obj, true) return obj.toJSON() } else { seens.set(obj, true) const result = {} for (let key in obj) { result[key] = serialize(obj[key]) } seens.set(obj, false) return result } } return obj } return serialize(obj) } const send = ({ type, id, form, }: { type: string id?: string | number form?: any }) => { const graph = serializeObject(form?.getFormGraph()) window.postMessage( { source: '@formily-devtools-inject-script', type, id, graph: form && JSON.stringify(graph, (key, value) => { if (typeof value === 'symbol') { return value.toString() } return value }), }, '*' ) } send({ type: 'init', }) interface IIdleDeadline { didTimeout: boolean timeRemaining: () => DOMHighResTimeStamp } const HOOK = { hasFormilyInstance: false, hasOpenDevtools: false, store: {}, openDevtools() { this.hasOpenDevtools = true }, closeDevtools() { this.hasOpenDevtools = false }, setVm(fieldId: string, formId: string) { if (fieldId) { globalThis.$vm = this.store[formId].fields[fieldId] } else { globalThis.$vm = this.store[formId] } }, inject(id: number, form: any) { this.hasFormilyInstance = true this.store[id] = form send({ type: 'install', id, form, }) let timer = null const task = () => { globalThis.requestIdleCallback((deadline: IIdleDeadline) => { if (this.store[id]) { if (deadline.timeRemaining() < 16) { task() } else { send({ type: 'update', id, form, }) } } }) } form.subscribe(() => { if (!this.hasOpenDevtools) return clearTimeout(timer) timer = setTimeout(task, 300) }) }, update() { const keys = Object.keys(this.store || {}) keys.forEach((id) => { send({ type: 'update', id, form: this.store[id], }) }) }, unmount(id: number) { delete this.store[id] send({ type: 'uninstall', id, }) }, } globalThis.__FORMILY_DEV_TOOLS_HOOK__ = HOOK globalThis.__UFORM_DEV_TOOLS_HOOK__ = HOOK ================================================ FILE: devtools/chrome-extension/src/extension/background.ts ================================================ // background.ts - Manifest V3版本 const connections = {} // 处理扩展程序的连接请求 chrome.runtime.onConnect.addListener(function (port) { if (port.name === '@formily-devtools-panel-script') { const extensionListener = function (message) { // 原始的连接事件不包含开发者工具网页的标签页标识符, // 所以我们需要显式发送它。 if (message.name == 'init') { connections[message.tabId] = port return } // 其他消息的处理 } // 监听开发者工具网页发来的消息 port.onMessage.addListener(extensionListener) port.onDisconnect.addListener(function (disconnectedPort) { port.onMessage.removeListener(extensionListener) const tabs = Object.keys(connections) for (let i = 0, len = tabs.length; i < len; i++) { if (connections[tabs[i]] == port) { delete connections[tabs[i]] break } } }) } }) // 从内容脚本接收消息,并转发至当前标签页对应的开发者工具网页 chrome.runtime.onMessage.addListener(function (request, sender) { // 来自内容脚本的消息应该已经设置 sender.tab if (sender.tab) { const tabId = sender.tab.id if (tabId && tabId in connections) { connections[tabId].postMessage(request) } } return true }) ================================================ FILE: devtools/chrome-extension/src/extension/content.ts ================================================ window.addEventListener( 'message', (event) => { const { source, ...payload } = event.data if (source === '@formily-devtools-inject-script') { chrome.runtime.sendMessage({ source, ...payload, }) } }, false ) ================================================ FILE: devtools/chrome-extension/src/extension/devpanel.tsx ================================================ import React, { useEffect, useState } from 'react' import ReactDOM from 'react-dom' import App from '../app' const backgroundPageConnection = chrome.runtime.connect({ name: '@formily-devtools-panel-script', }) backgroundPageConnection.postMessage({ name: 'init', tabId: chrome.devtools.inspectedWindow.tabId, }) chrome.devtools.inspectedWindow.eval( 'window.__FORMILY_DEV_TOOLS_HOOK__.openDevtools()' ) const Devtools = () => { const [state, setState] = useState([]) useEffect(() => { let store = {} const update = () => { setState( Object.keys(store).map((key) => { return store[key] }) ) } chrome.devtools.inspectedWindow.eval( 'window.__FORMILY_DEV_TOOLS_HOOK__.update()' ) backgroundPageConnection.onMessage.addListener(({ type, id, graph }) => { if (type === 'init') { store = {} chrome.devtools.inspectedWindow.eval( 'window.__FORMILY_DEV_TOOLS_HOOK__.openDevtools()' ) } else if (type !== 'uninstall') { store[id] = JSON.parse(graph) } else { delete store[id] } update() }) }, []) return } ReactDOM.render(, document.getElementById('root')) ================================================ FILE: devtools/chrome-extension/src/extension/devtools.tsx ================================================ declare let chrome: any let created = false const createPanel = () => { if (created) { return } chrome.devtools.inspectedWindow.eval( 'window.__FORMILY_DEV_TOOLS_HOOK__ && window.__FORMILY_DEV_TOOLS_HOOK__.hasFormilyInstance', (hasFormily: boolean) => { if (!hasFormily) return created = true clearInterval(loadCheckInterval) chrome.devtools.panels.create( 'Formily', 'img/logo/scalable.png', './devpanel.html', function () {} ) } ) } const loadCheckInterval = setInterval(function () { createPanel() }, 1000) createPanel() ================================================ FILE: devtools/chrome-extension/src/extension/inject.ts ================================================ import backend from 'raw-loader!./backend' function nullthrows(x: any, message?: string) { if (x != null) { return x } const error: any = new Error( message !== undefined ? message : 'Got unexpected ' + x ) error.framesToPop = 1 // Skip nullthrows's own stack frame. throw error } function injectCode(code) { const script = document.createElement('script') script.textContent = code // This script runs before the element is created, // so we add the script to instead. nullthrows(document.documentElement).appendChild(script) nullthrows(script.parentNode).removeChild(script) } injectCode(`;(function(){ var exports = {}; ${backend} })()`) ================================================ FILE: devtools/chrome-extension/src/extension/manifest.json ================================================ { "version": "0.1.14", "name": "Formily DevTools", "short_name": "Formily DevTools", "description": "Formily DevTools for debugging application's state changes.", "homepage_url": "https://github.com/alibaba/formily", "manifest_version": 3, "action": { "default_icon": "img/logo/scalable.png", "default_title": "Formily DevTools", "default_popup": "popup.html" }, "commands": { "devtools-left": { "description": "DevTools window to left" }, "devtools-right": { "description": "DevTools window to right" }, "devtools-bottom": { "description": "DevTools window to bottom" }, "devtools-remote": { "description": "Remote DevTools" }, "_execute_action": { "suggested_key": { "default": "Ctrl+Shift+E" } } }, "icons": { "16": "img/logo/16x16.png", "48": "img/logo/48x48.png", "128": "img/logo/128x128.png" }, "background": { "service_worker": "js/background.bundle.js", "type": "module" }, "content_scripts": [ { "matches": [""], "exclude_matches": ["*://www.google.com/*"], "js": ["js/content.bundle.js", "js/inject.bundle.js"], "run_at": "document_start", "all_frames": true } ], "devtools_page": "devtools.html", "web_accessible_resources": [ { "resources": ["js/backend.bundle.js"], "matches": [""] } ], "externally_connectable": { "ids": ["*"] }, "host_permissions": ["file://*/*", "http://*/*", "https://*/*"], "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" }, "update_url": "https://clients2.google.com/service/update2/crx" } ================================================ FILE: devtools/chrome-extension/src/extension/popup.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom' ReactDOM.render(
hello world
, document.getElementById('root')) ================================================ FILE: devtools/chrome-extension/src/extension/views/devpanel.ejs ================================================ Formily Devtools
================================================ FILE: devtools/chrome-extension/src/extension/views/devtools.ejs ================================================ Formily Devtools
================================================ FILE: devtools/chrome-extension/src/extension/views/popup.ejs ================================================ Formily Devtools
================================================ FILE: devtools/chrome-extension/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./lib", "paths": { "@formily/*": ["packages/*", "devtools/*"] }, "declaration": true } } ================================================ FILE: devtools/chrome-extension/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["./src/**/*.ts", "./src/**/*.tsx"], "exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"] } ================================================ FILE: docs/functions/contributors.ts ================================================ import { Handler } from '@netlify/functions' import { Octokit } from '@octokit/rest' const octokit = new Octokit({ baseUrl: 'https://api.github.com', auth: process.env.GITHUB_TOKEN, }) const headers = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type', 'Access-Control-Allow-Methods': 'GET', } export const handler: Handler = async (event) => { if (event.httpMethod !== 'GET') { return { statusCode: 405, body: 'Method Not Allowed' } } return { statusCode: 200, headers, body: JSON.stringify( await octokit.repos.listContributors({ owner: 'alibaba', repo: 'formily', per_page: 1000, page: 1, }) ), } } ================================================ FILE: docs/functions/npm-search.ts ================================================ import { Handler } from '@netlify/functions' import qs from 'querystring' import axios from 'axios' const headers = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type', 'Access-Control-Allow-Methods': 'GET', } export const handler: Handler = async (event) => { if (event.httpMethod !== 'GET') { return { statusCode: 405, body: 'Method Not Allowed' } } const params = qs.parse(event.rawQuery) const results = await axios.get( `https://www.npmjs.com/search/suggestions?q=${params.q}&size=100` ) return { statusCode: 200, headers, body: JSON.stringify(results.data), } } ================================================ FILE: docs/guide/advanced/async.md ================================================ # Asynchronous Data Sources Asynchronous data source management, the core is reflected in the dataSource property of the [Field](https://core.formilyjs.org/api/models/field) model. We can modify the dataSource of the Field in effects, or modify the dataSource property in reactions. If the field component (such as Select) has a consumer dataSource property, when the dataSource changes, the corresponding component will automatically re-render. Note: If it is a business custom component, please manually map the dataSource to the custom component, you can use connect or observer + useField Specific cases can refer to: - [Select](https://antd.formilyjs.org/components/select) - [TreeSelect](https://antd.formilyjs.org/components/tree-select) - [Cascader](https://antd.formilyjs.org/components/cascader) ================================================ FILE: docs/guide/advanced/async.zh-CN.md ================================================ # 实现异步数据源 异步数据源管理,核心体现在[Field](https://core.formilyjs.org/zh-CN/api/models/field)模型中的 dataSource 属性,我们可以在 effects 中修改 Field 的 dataSource,也可以在 reactions 中修改 dataSource 属性。 如果字段组件内部(比如 Select)有消费 dataSource 属性,当 dataSource 发生变化时,对应组件会自动重渲染。 注意:如果是业务自定义组件,请手动映射dataSource到自定义组件中,可以使用 connect,也可以使用 observer + useField 具体案例可以参考: - [Select](https://antd.formilyjs.org/zh-CN/components/select) - [TreeSelect](https://antd.formilyjs.org/zh-CN/components/tree-select) - [Cascader](https://antd.formilyjs.org/zh-CN/components/cascader) ================================================ FILE: docs/guide/advanced/build.md ================================================ # Pack on Demand ## Based on Umi Development #### Install `babel-plugin-import` ```shell npm install babel-plugin-import --save-dev ``` or ```shell yarn add babel-plugin-import --dev ``` #### Plugin Configuration Modify `.umirc.js` or `.umirc.ts` ```js export default { extraBabelPlugins: [ [ 'babel-plugin-import', { libraryName: 'antd', libraryDirectory: 'es', style: true }, 'antd', ], [ 'babel-plugin-import', { libraryName: '@formily/antd', libraryDirectory: 'esm', style: true }, '@formily/antd', ], ], } ``` ## Based on Create-react-app Development First, we need to customize the default configuration of `create-react-app`, here we use [react-app-rewired](https://github.com/timarney/react-app-rewired) (A community solution for custom configuration of `create-react-app`) Introduce `react-app-rewired` and modify the startup configuration in `package.json`. Due to the new [react-app-rewired@2.x](https://github.com/timarney/react-app-rewired#alternatives) version, you also need to install [customize-cra](https://github.com/arackaf/customize-cra). ```shell $ npm install react-app-rewired customize-cra --save-dev ``` or ```shell $ yarn add react-app-rewired customize-cra --dev ``` modify `package.json` ```diff "scripts": { - "start": "react-scripts start", + "start": "react-app-rewired start", - "build": "react-scripts build", + "build": "react-app-rewired build", - "test": "react-scripts test", + "test": "react-app-rewired test", } ``` Then create a `config-overrides.js` in the project root directory to modify the default configuration. ```js module.exports = function override(config, env) { // do stuff with the webpack config... return config } ``` #### Install babel-plugin-import ```shell npm install babel-plugin-import --save-dev ``` or ```shell yarn add babel-plugin-import --dev ``` modify `config-overrides.js` ```diff + const { override, fixBabelImports } = require('customize-cra'); - module.exports = function override(config, env) { - // do stuff with the webpack config... - return config; - }; + module.exports = override( + fixBabelImports('antd', { + libraryName: 'antd', + libraryDirectory: 'es', + style: true + }), + fixBabelImports('@formily/antd', { + libraryName: '@formily/antd', + libraryDirectory: 'esm', + style: true + }), + ); ``` ## Use in Webpack #### Install babel-plugin-import ```shell npm install babel-plugin-import --save-dev ``` or ```shell yarn add babel-plugin-import --dev ``` Modify `.babelrc` or babel-loader ```json { "plugins": [ [ "import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }, "antd" ], [ "import", { "libraryName": "@formily/antd", "libraryDirectory": "esm", "style": true }, "@formily/antd" ] ] } ``` For more configuration, please refer to [babel-plugin-import](https://github.com/ant-design/babel-plugin-import) ================================================ FILE: docs/guide/advanced/build.zh-CN.md ================================================ # 按需打包 ## 基于 Umi 开发 #### 安装 `babel-plugin-import` ```shell npm install babel-plugin-import --save-dev ``` 或者 ```shell yarn add babel-plugin-import --dev ``` #### 插件配置 修改 `.umirc.js`或 `.umirc.ts` ```js export default { extraBabelPlugins: [ [ 'babel-plugin-import', { libraryName: 'antd', libraryDirectory: 'es', style: true }, 'antd', ], [ 'babel-plugin-import', { libraryName: '@formily/antd', libraryDirectory: 'esm', style: true }, '@formily/antd', ], ], } ``` ## 基于 create-react-app 开发 首先我们需要对`create-react-app`的默认配置进行自定义,这里我们使用 [react-app-rewired](https://github.com/timarney/react-app-rewired) (一个对 `create-react-app` 进行自定义配置的社区解决方案)。 引入 `react-app-rewired` 并修改 `package.json` 里的启动配置。由于新的 [react-app-rewired@2.x](https://github.com/timarney/react-app-rewired#alternatives) 版本的关系,你还需要安装 [customize-cra](https://github.com/arackaf/customize-cra)。 ```shell $ npm install react-app-rewired customize-cra --save-dev ``` 或者 ```shell $ yarn add react-app-rewired customize-cra --dev ``` 修改 `package.json` ```diff "scripts": { - "start": "react-scripts start", + "start": "react-app-rewired start", - "build": "react-scripts build", + "build": "react-app-rewired build", - "test": "react-scripts test", + "test": "react-app-rewired test", } ``` 然后在项目根目录创建一个 `config-overrides.js` 用于修改默认配置。 ```js module.exports = function override(config, env) { // do stuff with the webpack config... return config } ``` #### 安装 babel-plugin-import ```shell npm install babel-plugin-import --save-dev ``` 或者 ```shell yarn add babel-plugin-import --dev ``` 修改`config-overrides.js` ```diff + const { override, fixBabelImports } = require('customize-cra'); - module.exports = function override(config, env) { - // do stuff with the webpack config... - return config; - }; + module.exports = override( + fixBabelImports('antd', { + libraryName: 'antd', + libraryDirectory: 'es', + style: true + }), + fixBabelImports('@formily/antd', { + libraryName: '@formily/antd', + libraryDirectory: 'esm', + style: true + }), + ); ``` ## 在 Webpack 中使用 #### 安装 babel-plugin-import ```shell npm install babel-plugin-import --save-dev ``` 或者 ```shell yarn add babel-plugin-import --dev ``` 修改 `.babelrc` 或者 babel-loader ```json { "plugins": [ [ "import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }, "antd" ], [ "import", { "libraryName": "@formily/antd", "libraryDirectory": "esm", "style": true }, "@formily/antd" ] ] } ``` 更多配置请参考 [babel-plugin-import](https://github.com/ant-design/babel-plugin-import) ================================================ FILE: docs/guide/advanced/business-logic.md ================================================ # Manage Business Logic In the previous document, we can actually find that Formily has provided the ability to describe the logic locally, that is, the x-reactions/reactions property of the field component. And in Schema, x-reactions can pass both functions and a structured object. Of course, there are also effects inherited from Formily 1.x, So to summarize, the ways to describe logic in Formily 2.x are: - Effects or reactions property in pure JSX mode - Effects or structured x-reactions property in Schema mode - Effects or functional x-reactions property in Schema mode With so many ways of describing logic, how should we choose? What scenarios are best practices? First, we need to understand the positioning of effects and reactions. First of all, reactions are responders used on specific field properties. They will be executed repeatedly based on the data changes that the function depends on. Its biggest advantage is that it is simple, straightforward and easy to understand, such as: ```tsx pure /* eslint-disable */ { /**specific logic implementation**/ }} /> ``` Then, effects are used to implement the side-effect isolation logic management model. Its biggest advantage is that it can make the view code easier to maintain in a scenario with a large number of fields. At the same time, it also has the ability to process fields in batches. For example, we declare x-reactions in the field properties of A, B, C. If the x-reactions logic of these three fields are exactly the same, then we only need to write this in effects: ```ts onFieldReact('*(A,B,C)', (field) => { //...logic }) ``` Another advantage of using effects is that a series of reusable logic plug-ins can be implemented, which can be very convenient logic pluggable, and at the same time can do some things like global monitoring. In this way, do we not need to define the logic locally? No, the premise of the above writing is that for a large number of fields, if the view layer is full of reactions, it looks uncomfortable, so it is a better strategy to consider extracting logic from unified maintenance. On the contrary, if the number of fields is small and the logic is relatively simple, it is also good to write reactions directly on the field attributes, which is clear. At the same time, because JSON Schema can be consumed by the configuration system, we need to logically configure a specific field on the configuration interface. So we still need to support local definition logic capabilities, and also need to support structured description logic, such as: ```json { "x-reactions": { "dependencies": ["aa"], "fulfill": { "state": { "visible": "{{$deps[0] == '123'}}" } } } } ``` This can well solve the linkage requirements of most configuration scenarios. However, there is another scenario, that is, our linkage process is asynchronous, the logic is very complicated, or there is a large amount of data processing, then we can only consider open up the ability to describe functional states, such as: ```json { "x-reactions": "{{(field)=>{/**specific logic implementation**/}}}" } ``` This is very similar to a low-code configuration. Of course, we can also register a series of general logic functions in the context scope: ```json { "x-reactions": "{{customFunction}}" } ``` In conclusion, the way we manage business logic has the following priorities: - Pure source mode - The number of fields is huge and the logic is complex, and the logic defined in effects is preferred. - The number of fields is small, the logic is simple, and the logic defined in reactions is preferred - Schema mode - There is no asynchronous logic, structured reactions are preferred to define logic. - There is asynchronous logic, or a large number of calculations, the functional state reactions are preferred to define logic. For how to play with effects in effects, we mainly look at the [@formily/core](https://core.formilyjs.org) document. ================================================ FILE: docs/guide/advanced/business-logic.zh-CN.md ================================================ # 管理业务逻辑 在前面的文档中,我们其实可以发现 Formily 已经提供了局部描述逻辑的能力,也就是字段组件的 x-reactions/reactions 属性,而且在 Schema 中,x-reactions 既能传函数,也能传一个结构化对象,当然,还有 Formily1.x 继承下来的 effects,那么总结一下,在 Formily2.x 中描述逻辑的方式有: - 纯 JSX 模式下的 effects 或 reactions 属性 - Schema 模式下的 effects 或结构化 x-reactions 属性 - Schema 模式下的 effects 或函数态 x-reactions 属性 这么多描述逻辑的方式,我们该如何选择?什么场景下是最佳实践呢?首先,我们要理解清楚 effects 和 reactions 的定位。 首先,reactions 是用在具体字段属性上的响应器,它会基于函数内依赖的数据变化而重复执行,它最大的优点就是简单直接,容易理解,比如: ```tsx pure /* eslint-disable */ { /**具体逻辑实现**/ }} /> ``` 然后,effects 是用于实现副作用隔离逻辑管理模型,它最大的优点就是在字段数量超多的场景下,可以让视图代码变得更易维护,同时它还有一个能力,就是可以批量化的对字段做处理。比如我们在 A,B,C 字段属性显示声明 x-reactions,如果这 3 个字段的 x-reactions 逻辑都是一模一样的,那我们在 effects 中只需这么写即可: ```ts onFieldReact('*(A,B,C)', (field) => { //...逻辑 }) ``` 使用 effects 还有一个好处就是可以实现一系列的可复用逻辑插件,可以做到很方便的逻辑可拔插,同时还能做一些全局监控之类的事情。 这样看来,是不是我们就不需要局部定义逻辑了? 并不是,上面的写法的前提是对于字段数量很多,如果视图层满屏的 reactions,看着是很难受的,所以考虑将逻辑抽离统一维护则是一个比较好的策略。相反,如果字段数量很少,逻辑相对简单的,直接在字段属性上写 reactions 也是不错的,清晰明了。 同时,因为 JSON Schema 是可以给配置化系统消费的,我们需要在配置界面上对具体某个字段做逻辑配置。所以我们还是需要支持局部定义逻辑能力,同时还需要支持结构化描述逻辑,比如: ```json { "x-reactions": { "dependencies": ["aa"], "fulfill": { "state": { "visible": "{{$deps[0] == '123'}}" } } } } ``` 这样可以很好的解决大部分配置场景的联动需求了,但是,还有一种场景,就是我们的联动过程是存在异步的,逻辑非常复杂的,或者存在大量数据处理的,那我们就只能考虑开放函数态描述的能力了,比如: ```json { "x-reactions": "{{(field)=>{/**具体逻辑实现**/}}}" } ``` 这种就很像是低代码配置了,当然,我们也可以在上下文作用域中注册一系列的通用逻辑函数: ```json { "x-reactions": "{{customFunction}}" } ``` 最终总结下来,我们管理业务逻辑的方式,有以下优先级: - 纯源码模式 - 字段数量庞大,逻辑复杂,优先选择 effects 中定义逻辑 - 字段数量少,逻辑简单,优先选择 reactions 中定义逻辑 - Schema 模式 - 不存在异步逻辑,优先选择结构化 reactions 定义逻辑 - 存在异步逻辑,或者大量计算,优先选择函数态 reactions 定义逻辑 对于 effects 中如何玩出花来,我们主要看[@formily/core](https://core.formilyjs.org/zh-CN)文档即可 ================================================ FILE: docs/guide/advanced/calculator.md ================================================ # Calculator Linkage calculator is mainly used for evaluation and summarization in the process of filling in the form. In Formily 1.x, the cost of realizing this kind of demand is very high. In 2.x, we can easily implement it with the help of reactions. ## Markup Schema Case ```tsx import React from 'react' import { Form, FormItem, NumberPicker, ArrayTable, Editable, Input, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Editable, Input, NumberPicker, ArrayTable, }, }) const form = createForm() export default () => { return (
0}}', fulfill: { state: { value: '{{$deps[0].reduce((total,item)=>item.total ? total+item.total : total,0)}}', }, }, }} /> 提交
) } ``` ## JSON Schema Case ```tsx import React from 'react' import { Form, FormItem, NumberPicker, ArrayTable, Editable, Input, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Editable, Input, NumberPicker, ArrayTable, }, }) const form = createForm() const schema = { type: 'object', properties: { projects: { type: 'array', title: 'Projects', 'x-decorator': 'FormItem', 'x-component': 'ArrayTable', items: { type: 'object', properties: { column_1: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 50, title: 'Sort', align: 'center', }, properties: { sortable: { type: 'void', 'x-component': 'ArrayTable.SortHandle', }, }, }, column_2: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 50, title: 'Index', align: 'center', }, properties: { index: { type: 'void', 'x-component': 'ArrayTable.Index', }, }, }, column_3: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Price', }, properties: { price: { type: 'number', default: 0, 'x-decorator': 'Editable', 'x-component': 'NumberPicker', 'x-component-props': { addonAfter: '$', }, }, }, }, column_4: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Count', }, properties: { count: { type: 'number', default: 0, 'x-decorator': 'Editable', 'x-component': 'NumberPicker', 'x-component-props': { addonAfter: '$', }, }, }, }, column_5: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Total', }, properties: { total: { type: 'number', 'x-read-pretty': true, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', 'x-component-props': { addonAfter: '$', }, 'x-reactions': { dependencies: ['.price', '.count'], when: '{{$deps[0] && $deps[1]}}', fulfill: { state: { value: '{{$deps[0] * $deps[1]}}', }, }, }, }, }, }, column_6: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Operations', }, properties: { item: { type: 'void', 'x-component': 'FormItem', properties: { remove: { type: 'void', 'x-component': 'ArrayTable.Remove', }, moveDown: { type: 'void', 'x-component': 'ArrayTable.MoveDown', }, moveUp: { type: 'void', 'x-component': 'ArrayTable.MoveUp', }, }, }, }, }, }, }, properties: { add: { type: 'void', title: 'Add', 'x-component': 'ArrayTable.Addition', }, }, }, total: { type: 'number', title: 'Total', 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', 'x-component-props': { addonAfter: '$', }, 'x-pattern': 'readPretty', 'x-reactions': { dependencies: ['.projects'], when: '{{$deps[0].length > 0}}', fulfill: { state: { value: '{{$deps[0].reduce((total,item)=>item.total ? total+item.total : total,0)}}', }, }, }, }, }, } export default () => { return (
submit ) } ``` ================================================ FILE: docs/guide/advanced/calculator.zh-CN.md ================================================ # 实现联动计算器 联动计算器,主要用于在填写表单的过程中做求值汇总,在 Formily1.x 中实现这类需求的成本非常非常高,在 2.x 中,我们可以借助 reactions 轻松实现 ## Markup Schema 案例 ```tsx import React from 'react' import { Form, FormItem, NumberPicker, ArrayTable, Editable, Input, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Editable, Input, NumberPicker, ArrayTable, }, }) const form = createForm() export default () => { return (
0}}', fulfill: { state: { value: '{{$deps[0].reduce((total,item)=>item.total ? total+item.total : total,0)}}', }, }, }} /> 提交
) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Form, FormItem, NumberPicker, ArrayTable, Editable, Input, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Editable, Input, NumberPicker, ArrayTable, }, }) const form = createForm() const schema = { type: 'object', properties: { projects: { type: 'array', title: 'Projects', 'x-decorator': 'FormItem', 'x-component': 'ArrayTable', items: { type: 'object', properties: { column_1: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 50, title: 'Sort', align: 'center', }, properties: { sortable: { type: 'void', 'x-component': 'ArrayTable.SortHandle', }, }, }, column_2: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 50, title: 'Index', align: 'center', }, properties: { index: { type: 'void', 'x-component': 'ArrayTable.Index', }, }, }, column_3: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Price', }, properties: { price: { type: 'number', default: 0, 'x-decorator': 'Editable', 'x-component': 'NumberPicker', 'x-component-props': { addonAfter: '$', }, }, }, }, column_4: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Count', }, properties: { count: { type: 'number', default: 0, 'x-decorator': 'Editable', 'x-component': 'NumberPicker', 'x-component-props': { addonAfter: '$', }, }, }, }, column_5: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Total', }, properties: { total: { type: 'number', 'x-read-pretty': true, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', 'x-component-props': { addonAfter: '$', }, 'x-reactions': { dependencies: ['.price', '.count'], when: '{{$deps[0] && $deps[1]}}', fulfill: { state: { value: '{{$deps[0] * $deps[1]}}', }, }, }, }, }, }, column_6: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Operations', }, properties: { item: { type: 'void', 'x-component': 'FormItem', properties: { remove: { type: 'void', 'x-component': 'ArrayTable.Remove', }, moveDown: { type: 'void', 'x-component': 'ArrayTable.MoveDown', }, moveUp: { type: 'void', 'x-component': 'ArrayTable.MoveUp', }, }, }, }, }, }, }, properties: { add: { type: 'void', title: 'Add', 'x-component': 'ArrayTable.Addition', }, }, }, total: { type: 'number', title: 'Total', 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', 'x-component-props': { addonAfter: '$', }, 'x-pattern': 'readPretty', 'x-reactions': { dependencies: ['.projects'], when: '{{$deps[0].length > 0}}', fulfill: { state: { value: '{{$deps[0].reduce((total,item)=>item.total ? total+item.total : total,0)}}', }, }, }, }, }, } export default () => { return (
提交 ) } ``` ================================================ FILE: docs/guide/advanced/controlled.md ================================================ # Form Controlled Formily 2.x has given up supporting controlled mode for form components and field components. Because the internal management state mode of the form itself is not a controlled mode, there will be many boundary problems in the process of changing the controlled mode to the uncontrolled mode. At the same time, the controlled mode will have a large number of dirty inspection processes, and the performance is very poor. Instead, the controlled mode itself can solve most of the problems. So Formily no longer supports the controlled mode, but if we insist on implementing ordinary React controlled, we can still support it. It can only achieve value control, not field-level control, which is the Field component we use. The properties will only take effect during the first rendering. Any changes to the properties in the future will not be automatically updated. If you want to update automatically, unless you recreate the Form instance (obviously this will lose all the previously maintained state). Therefore, we more recommend using [@formily/reactive](https://reactive.formilyjs.org) to achieve responsive control, which can achieve both value control and field-level control. ## Value Controlled Ordinary controlled mode, which will rely heavily on dirty checking to achieve data synchronization, and the number of component renderings will be very high. ```tsx import React, { useMemo, useState, useEffect, useRef } from 'react' import { createForm, onFormValuesChange } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const MyForm = (props) => { const form = useMemo( () => createForm({ values: props.values, effects: () => { onFormValuesChange((form) => { props.onChange(form.values) }) }, }), [] ) const count = useRef(1) useEffect(() => { form.setValues(props.values, 'overwrite') }, [JSON.stringify(props.values)]) return (
Form component rendering times:{count.current++}
) } export default () => { const [values, setValues] = useState({ input: '' }) const count = useRef(1) return ( <> { setValues({ ...values, input: event.target.value }) }} /> { setValues({ ...values }) }} /> root component rendering times: {count.current++} ) } ``` ## Responsive Value Controlled Responsive control is mainly to use [@formily/reactive](https://reactive.formilyjs.org) to achieve responsive updates, we can easily achieve two-way binding, while the performance is full of normal controlled updates. ```tsx import React, { useMemo, useRef } from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' import { observable } from '@formily/reactive' import { observer } from '@formily/reactive-react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const MyForm = (props) => { const count = useRef(1) const form = useMemo( () => createForm({ values: props.values, }), [] ) return (
Form component rendering times:{count.current++}
) } const Controller = observer((props) => { const count = useRef(1) return ( { props.values.input = event.target.value }} /> Controller component rendering times:{count.current++} ) }) export default () => { const count = useRef(1) const values = useMemo(() => observable({ input: '', }) ) return ( <> root component rendering times:{count.current++} ) } ``` ## Schema Controlled There will be a requirement for the form configuration scenario. The Schema of the form will change frequently. In fact, it is equivalent to frequently creating new forms. The state of the previous operation should be discarded. ```tsx import React, { useMemo, useState } from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' import { Button, Space } from 'antd' const SchemaField = createSchemaField({ components: { Input, FormItem, Select, }, }) export default () => { const [current, setCurrent] = useState({}) const form = useMemo(() => createForm(), [current]) return (
) } ``` ## Schema fragment linkage (top level control) The most important thing for fragment linkage is to manually clean up the field model, otherwise the UI cannot be synchronized ```tsx import React, { useMemo, useRef } from 'react' import { createForm } from '@formily/core' import { createSchemaField, observer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const SchemaField = createSchemaField({ components: { Input, FormItem, Select, }, }) const DYNAMIC_INJECT_SCHEMA = { type_1: { type: 'void', properties: { aa: { type: 'string', title: 'AA', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { placeholder: 'Input', }, }, }, }, type_2: { type: 'void', properties: { aa: { type: 'string', title: 'AA', 'x-decorator': 'FormItem', enum: [ { label: '111', value: '111', }, { label: '222', value: '222' }, ], 'x-component': 'Select', 'x-component-props': { placeholder: 'Select', }, }, bb: { type: 'string', title: 'BB', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, } const App = observer(() => { const oldTypeRef = useRef() const form = useMemo(() => createForm(), []) const currentType = form.values.type const schema = { type: 'object', properties: { type: { type: 'string', title: 'Type', enum: [ { label: 'type 1', value: 'type_1' }, { label: 'type 2', value: 'type_2' }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', }, container: DYNAMIC_INJECT_SCHEMA[currentType], }, } if (oldTypeRef.current !== currentType) { form.clearFormGraph('container.*') //Recycle field model } oldTypeRef.current = currentType return (
) }) export default App ``` ## Schema fragment linkage (custom component) ```tsx import React, { useMemo, useState, useEffect } from 'react' import { createForm } from '@formily/core' import { createSchemaField, RecursionField, useForm, useField, observer, } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const Custom = observer(() => { const field = useField() const form = useForm() const [schema, setSchema] = useState({}) useEffect(() => { form.clearFormGraph(`${field.address}.*`) //Recycle field model //Can be obtained asynchronously setSchema(DYNAMIC_INJECT_SCHEMA[form.values.type]) }, [form.values.type]) return ( ) }) const SchemaField = createSchemaField({ components: { Input, FormItem, Select, Custom, }, }) const DYNAMIC_INJECT_SCHEMA = { type_1: { type: 'void', properties: { aa: { type: 'string', title: 'AA', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { placeholder: 'Input', }, }, }, }, type_2: { type: 'void', properties: { aa: { type: 'string', title: 'AA', 'x-decorator': 'FormItem', enum: [ { label: '111', value: '111', }, { label: '222', value: '222' }, ], 'x-component': 'Select', 'x-component-props': { placeholder: 'Select', }, }, bb: { type: 'string', title: 'BB', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, } const App = observer(() => { const form = useMemo(() => createForm(), []) const schema = { type: 'object', properties: { type: { type: 'string', title: 'Type', enum: [ { label: 'type 1', value: 'type_1' }, { label: 'type 2', value: 'type_2' }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', }, container: { type: 'object', 'x-component': 'Custom', }, }, } return (
) }) export default App ``` ## Field Level Control ### Best Practices It is recommended to use [@formily/reactive](https://reactive.formilyjs.org) to achieve responsive control. ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' import { observable } from '@formily/reactive' import { observer } from '@formily/reactive-react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() const obs = observable({ input: '', }) const Controller = observer(() => { return ( { obs.input = event.target.value }} /> ) }) export default () => { return ( <>
{ field.component[1].placeholder = obs.input || 'controlled target' }} />
) } ``` ### Anti-pattern It is not possible to update automatically when using traditional controlled mode. ```tsx import React, { useState } from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => { const [value, setValue] = useState('') return ( <> { setValue(event.target.value) }} />
) } ``` ================================================ FILE: docs/guide/advanced/controlled.zh-CN.md ================================================ # 实现表单受控 Formily2.x 已经放弃了给表单组件和字段组件支持受控模式,因为表单内部管理状态模式本身就不是受控模式,在将受控模式转为非受控模式的过程中会有很多边界问题,同时受控模式会存在大量的脏检查过程,性能很不好,反而非受控模式本身就可以解决大部分问题了。 所以 Formily 就不再支持受控模式了,但是如果我们硬要实现普通 React 受控,还是可以支持的,只不过只能实现值受控,不能实现字段级受控,也就是我们使用的 Field 组件,属性只会在初次渲染时生效,未来属性发生任何变化都不会自动更新,想要自动更新,除非重新创建 Form 实例(显然这样会丢失所有之前维护好的状态)。 所以,我们更加推荐的是使用[@formily/reactive](https://reactive.formilyjs.org/zh-CN) 实现响应式受控,既能实现值受控,也能实现字段级受控 ## 值受控 普通受控模式,会强依赖脏检查实现数据同步,同时组件渲染次数会非常高 ```tsx import React, { useMemo, useState, useEffect, useRef } from 'react' import { createForm, onFormValuesChange } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const MyForm = (props) => { const form = useMemo( () => createForm({ values: props.values, effects: () => { onFormValuesChange((form) => { props.onChange(form.values) }) }, }), [] ) const count = useRef(1) useEffect(() => { form.setValues(props.values, 'overwrite') }, [JSON.stringify(props.values)]) return (
Form组件渲染次数:{count.current++}
) } export default () => { const [values, setValues] = useState({ input: '' }) const count = useRef(1) return ( <> { setValues({ ...values, input: event.target.value }) }} /> { setValues({ ...values }) }} /> 根组件渲染次数:{count.current++} ) } ``` ## 响应式值受控 响应式受控主要是使用[@formily/reactive](https://reactive.formilyjs.org/zh-CN)实现响应式更新,我们可以轻松实现双向绑定,同时性能完爆普通受控更新 ```tsx import React, { useMemo, useRef } from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' import { observable } from '@formily/reactive' import { observer } from '@formily/reactive-react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const MyForm = (props) => { const count = useRef(1) const form = useMemo( () => createForm({ values: props.values, }), [] ) return (
Form组件渲染次数:{count.current++}
) } const Controller = observer((props) => { const count = useRef(1) return ( { props.values.input = event.target.value }} /> Controller组件渲染次数:{count.current++} ) }) export default () => { const count = useRef(1) const values = useMemo(() => observable({ input: '', }) ) return ( <> 根组件渲染次数:{count.current++} ) } ``` ## Schema 受控 对于表单配置化场景会有一个需求,表单的 Schema 会发生频繁改变,其实就相当于频繁创建新表单了,之前操作的状态就应该丢弃了 ```tsx import React, { useMemo, useState } from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' import { Button, Space } from 'antd' const SchemaField = createSchemaField({ components: { Input, FormItem, Select, }, }) export default () => { const [current, setCurrent] = useState({}) const form = useMemo(() => createForm(), [current]) return (
) } ``` ## Schema 片段联动(顶层控制) 片段联动最重要的是需要手动清理字段模型,否则无法做到 UI 同步 ```tsx import React, { useMemo, useRef } from 'react' import { createForm } from '@formily/core' import { createSchemaField, observer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const SchemaField = createSchemaField({ components: { Input, FormItem, Select, }, }) const DYNAMIC_INJECT_SCHEMA = { type_1: { type: 'void', properties: { aa: { type: 'string', title: 'AA', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { placeholder: 'Input', }, }, }, }, type_2: { type: 'void', properties: { aa: { type: 'string', title: 'AA', 'x-decorator': 'FormItem', enum: [ { label: '111', value: '111', }, { label: '222', value: '222' }, ], 'x-component': 'Select', 'x-component-props': { placeholder: 'Select', }, }, bb: { type: 'string', title: 'BB', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, } const App = observer(() => { const oldTypeRef = useRef() const form = useMemo(() => createForm(), []) const currentType = form.values.type const schema = { type: 'object', properties: { type: { type: 'string', title: '类型', enum: [ { label: '类型1', value: 'type_1' }, { label: '类型2', value: 'type_2' }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', }, container: DYNAMIC_INJECT_SCHEMA[currentType], }, } if (oldTypeRef.current !== currentType) { form.clearFormGraph('container.*') //回收字段模型 } oldTypeRef.current = currentType return (
) }) export default App ``` ## Schema 片段联动(自定义组件) ```tsx import React, { useMemo, useState, useEffect } from 'react' import { createForm } from '@formily/core' import { createSchemaField, RecursionField, useForm, useField, observer, } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const Custom = observer(() => { const field = useField() const form = useForm() const [schema, setSchema] = useState({}) useEffect(() => { form.clearFormGraph(`${field.address}.*`) //回收字段模型 //可以异步获取 setSchema(DYNAMIC_INJECT_SCHEMA[form.values.type]) }, [form.values.type]) return ( ) }) const SchemaField = createSchemaField({ components: { Input, FormItem, Select, Custom, }, }) const DYNAMIC_INJECT_SCHEMA = { type_1: { type: 'void', properties: { aa: { type: 'string', title: 'AA', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { placeholder: 'Input', }, }, }, }, type_2: { type: 'void', properties: { aa: { type: 'string', title: 'AA', 'x-decorator': 'FormItem', enum: [ { label: '111', value: '111', }, { label: '222', value: '222' }, ], 'x-component': 'Select', 'x-component-props': { placeholder: 'Select', }, }, bb: { type: 'string', title: 'BB', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, } const App = observer(() => { const form = useMemo(() => createForm(), []) const schema = { type: 'object', properties: { type: { type: 'string', title: '类型', enum: [ { label: '类型1', value: 'type_1' }, { label: '类型2', value: 'type_2' }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', }, container: { type: 'object', 'x-component': 'Custom', }, }, } return (
) }) export default App ``` ## 字段级受控 ### 最佳实践 推荐使用[@formily/reactive](https://reactive.formilyjs.org/zh-CN) 实现响应式受控 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' import { observable } from '@formily/reactive' import { observer } from '@formily/reactive-react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() const obs = observable({ input: '', }) const Controller = observer(() => { return ( { obs.input = event.target.value }} /> ) }) export default () => { return ( <>
{ field.component[1].placeholder = obs.input || '受控者' }} />
) } ``` ### 反模式 使用传统受控模式是无法自动更新的 ```tsx import React, { useState } from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => { const [value, setValue] = useState('') return ( <> { setValue(event.target.value) }} />
) } ``` ================================================ FILE: docs/guide/advanced/custom.md ================================================ # Custom Components The realization of business custom components mainly uses the Hooks API and observer API in [@formily/react](https://react.formilyjs.org) or [@formily/vue](https://vue.formilyjs.org). To access the ready-made component library, we mainly use connect/mapProps/mapReadPretty API. If you want to implement some more complex custom components, we strongly recommend looking directly at the source code of [@formily/antd](https://github.com/alibaba/formily/tree/formily_next/packages/antd/src) or [@formily/next](https://github.com/alibaba/formily/tree/formily_next/packages/next/src). ================================================ FILE: docs/guide/advanced/custom.zh-CN.md ================================================ # 实现自定义组件 实现业务自定义组件主要是使用[@formily/react](https://react.formilyjs.org/zh-CN) 或[@formily/vue](https://vue.formilyjs.org)中的 Hooks API 与 observer API 接入现成组件库的话,我们主要使用 connect/mapProps/mapReadPretty API 如果想要实现一些更复杂的自定义组件,我们强烈推荐直接看[@formily/antd](https://github.com/alibaba/formily/tree/formily_next/packages/antd/src)或 [@formily/next](https://github.com/alibaba/formily/tree/formily_next/packages/next/src)的源码 ================================================ FILE: docs/guide/advanced/destructor.md ================================================ # Compatible solution for front-end and back-end data differences Many times, we always encounter scenarios where the front-end data structure does not match the back-end data structure. The seemingly simple problem is actually very uncomfortable to solve. The most common problems are: The output of the front-end date range component is an array structure, but the format required by the back-end is to split a flat data structure. This problem is largely limited by the back-end domain model. Because from the perspective of back-end model design, splitting the flat structure is the best solution; But from the perspective of front-end componentization, the array structure is the best; So each side has its truth, but unfortunately, it can only cancel such an unequal treaty at the front end every time. However, with Formily, you don’t need to feel uncomfortable for such an embarrassing situation. **Formily provides the ability to deconstruct the path, which can help users quickly solve such problems.** Let's take a look at an example ## Markup Schema Case ```tsx import React from 'react' import { Form, FormItem, DatePicker, FormButtonGroup, Radio, Submit, } from '@formily/antd' import { createForm, onFieldValueChange } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, DatePicker, Radio, }, }) const form = createForm({ effects() { onFieldValueChange('visible_destructor', (field) => { form.setFieldState('[startDate,endDate]', (state) => { state.visible = !!field.value }) }) }, }) export default () => { return (
          
            {(form) => JSON.stringify(form.values, null, 2)}
          
        
submit
) } ``` ## JSON Schema Cases ```tsx import React from 'react' import { Form, FormItem, DatePicker, FormButtonGroup, Radio, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, DatePicker, Radio, }, }) const form = createForm() const schema = { type: 'object', properties: { visible_destructor: { type: 'boolean', title: 'Whether to display deconstructed fields', default: true, enum: [ { label: 'yes', value: true }, { label: 'no', value: false }, ], 'x-decorator': 'FormItem', 'x-component': 'Radio.Group', }, undestructor: { type: 'string', title: 'before deconstruction', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', }, '[startDate,endDate]': { type: 'string', title: 'after deconstruction', default: ['2020-11-20', '2021-12-30'], 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-reactions': { dependencies: ['visible_destructor'], fulfill: { state: { visible: '{{!!$deps[0]}}', }, }, }, }, }, } export default () => { return (
          
            {(form) => JSON.stringify(form.values, null, 2)}
          
        
submit ) } ``` ## Pure JSX Case ```tsx import React from 'react' import { Form, FormItem, DatePicker, FormButtonGroup, Radio, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { Field, FormConsumer } from '@formily/react' const form = createForm() export default () => { return (
{ field.visible = !!field.query('visible_destructor').value() }} />
          
            {(form) => JSON.stringify(form.values, null, 2)}
          
        
submit ) } ``` ================================================ FILE: docs/guide/advanced/destructor.zh-CN.md ================================================ # 前后端数据差异兼容方案 很多时候,我们总会遇到前端数据结构与后端数据结构不匹配的场景,看似很简单的问题,其实解决起来非常的让人难受,最常见的问题就是: 前端日期范围组件输出的是数组结构,但是后端要求的格式是拆分扁平数据结构,这种问题很大程度是受后端领域模型所限制,因为从后端模型设计的角度来看,拆分扁平结构是最佳方案; 但从前端组件化角度来看,数组结构又是最佳的; 所以哪一边都有其道理,可惜的是,每次都只能前端去消化这样一个不平等条约,不过,有了 Formily,你就完全不需要为这样一个尴尬局面而难受了,**Formily 提供了解构路径的能力,可以帮助用户快速解决这类问题。**,下面可以看看例子 ## Markup Schema 案例 ```tsx import React from 'react' import { Form, FormItem, DatePicker, FormButtonGroup, Radio, Submit, } from '@formily/antd' import { createForm, onFieldValueChange } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, DatePicker, Radio, }, }) const form = createForm({ effects() { onFieldValueChange('visible_destructor', (field) => { form.setFieldState('[startDate,endDate]', (state) => { state.visible = !!field.value }) }) }, }) export default () => { return (
          
            {(form) => JSON.stringify(form.values, null, 2)}
          
        
提交
) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Form, FormItem, DatePicker, FormButtonGroup, Radio, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, DatePicker, Radio, }, }) const form = createForm() const schema = { type: 'object', properties: { visible_destructor: { type: 'boolean', title: '是否显示解构字段', default: true, enum: [ { label: '是', value: true }, { label: '否', value: false }, ], 'x-decorator': 'FormItem', 'x-component': 'Radio.Group', }, undestructor: { type: 'string', title: '解构前', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', }, '[startDate,endDate]': { type: 'string', title: '解构后', default: ['2020-11-20', '2021-12-30'], 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-reactions': { dependencies: ['visible_destructor'], fulfill: { state: { visible: '{{!!$deps[0]}}', }, }, }, }, }, } export default () => { return (
          
            {(form) => JSON.stringify(form.values, null, 2)}
          
        
提交 ) } ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Form, FormItem, DatePicker, FormButtonGroup, Radio, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { Field, FormConsumer } from '@formily/react' const form = createForm() export default () => { return (
{ field.visible = !!field.query('visible_destructor').value() }} />
          
            {(form) => JSON.stringify(form.values, null, 2)}
          
        
提交 ) } ``` ================================================ FILE: docs/guide/advanced/input.less ================================================ input { background-color: transparent !important; } ================================================ FILE: docs/guide/advanced/layout.md ================================================ # Form Layout The form layout mainly uses [@formily/antd](https://antd.formilyjs.org) or [@formily/next](https://fusion.formilyjs.org): - [FormLayout](http://antd.formilyjs.org/components/form-layout) Component - [FormItem](http://antd.formilyjs.org/components/form-item) Component - [FormGrid](http://antd.formilyjs.org/components/form-grid) Component - [Space](http://antd.formilyjs.org/components/space) Component These 4 components can basically solve all complex form layout scenarios, we only need to flexibly combine these components. ================================================ FILE: docs/guide/advanced/layout.zh-CN.md ================================================ # 实现表单布局 表单布局主要是使用[@formily/antd](https://antd.formilyjs.org/zh-CN) 或 [@formily/next](https://fusion.formilyjs.org/zh-CN) 中的: - [FormLayout](https://antd.formilyjs.org/zh-CN/components/form-layout) 组件 - [FormItem](https://antd.formilyjs.org/zh-CN/components/form-item) 组件 - [FormGrid](https://antd.formilyjs.org/zh-CN/components/form-grid) 组件 - [Space](https://antd.formilyjs.org/zh-CN/components/space) 组件 这 4 个组件基本上能解决所有复杂表单布局场景,我们只需要灵活的组合使用这几个组件即可。 ================================================ FILE: docs/guide/advanced/linkages.md ================================================ # Linkage Logic There is only one mode to realize linkage logic in Formily 1.x, that is, active mode. It is necessary to monitor the event changes of one or more fields to control the state of another or more fields. This is very convenient for one-to-many linkage scenarios, but it is very troublesome for many-to-one scenarios. It is necessary to monitor the changes of multiple fields to control the state of a field. Therefore, Formily 2.x provides a responsive mechanism that allows the linkage to support passive linkage. You only need to pay attention to the field that a field depends on. When the dependent field changes, the dependent field can be automatically linked. ## Active Mode The core of active linkage is based on - [FormEffectHooks](https://core.formilyjs.org/api/entry/form-effect-hooks) - [FieldEffectHooks](https://core.formilyjs.org/api/entry/field-effect-hooks) - [setFormState](https://core.formilyjs.org/api/models/form#setformstate) - [setFieldState](https://core.formilyjs.org/api/models/form#setfieldstate) - [SchemaReactions](https://react.formilyjs.org/api/shared/schema#schemareactions) Realize active linkage, the advantage is that it is very convenient to realize one-to-many linkage. ### One-to-One Linkage #### Effects Use Cases ```tsx import React from 'react' import { createForm, onFieldValueChange } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm({ effects() { onFieldValueChange('select', (field) => { form.setFieldState('input', (state) => { //For the initial linkage, if the field cannot be found, setFieldState will push the update into the update queue until the field appears before performing the operation state.display = field.value }) }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions Use Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### One-to-Many Linkage #### Effects Use Cases ```tsx import React from 'react' import { createForm, onFieldValueChange } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm({ effects() { onFieldValueChange('select', (field) => { form.setFieldState('*(input1,input2)', (state) => { //For the initial linkage, if the field cannot be found, setFieldState will push the update into the update queue until the field appears before performing the operation state.display = field.value }) }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions Use Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### Rely on Linkage #### Effects Use Cases ```tsx import React from 'react' import { createForm, onFieldValueChange } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm({ effects() { onFieldValueChange('dim_1', (field) => { const dim1 = field.value const dim2 = field.query('dim_2').value() form.setFieldState('result', (state) => { state.value = dim1 * dim2 }) }) onFieldValueChange('dim_2', (field) => { const dim1 = field.query('dim_1').value() const dim2 = field.value || 0 form.setFieldState('result', (state) => { state.value = dim1 * dim2 }) }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, NumberPicker, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions Use Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, NumberPicker, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### Chain Linkage #### Effects Use Cases ```tsx import React from 'react' import { createForm, onFieldValueChange } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm({ effects() { onFieldValueChange('select', (field) => { form.setFieldState('input1', (state) => { //For the initial linkage, if the field cannot be found, setFieldState will push the update into the update queue until the field appears before performing the operation state.visible = !!field.value }) }) onFieldValueChange('input1', (field) => { form.setFieldState('input2', (state) => { //For the initial linkage, if the field cannot be found, setFieldState will push the update into the update queue until the field appears before performing the operation state.visible = !!field.value }) }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions Use Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### Loop Linkage #### Effects Use Cases ```tsx import React from 'react' import { createForm, onFieldInputValueChange } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, NumberPicker } from '@formily/antd' const form = createForm({ effects() { onFieldInputValueChange('total', (field) => { if (field.value === undefined) return form.setFieldState('count', (state) => { const price = form.values.price if (!price) return state.value = field.value / price }) form.setFieldState('price', (state) => { const count = form.values.count if (!count) return state.value = field.value / count }) }) onFieldInputValueChange('price', (field) => { form.setFieldState('total', (state) => { const count = form.values.count if (count === undefined) return state.value = field.value * count }) }) onFieldInputValueChange('count', (field) => { form.setFieldState('total', (state) => { const price = form.values.price if (price === undefined) return state.value = field.value * price }) }) }, }) const SchemaField = createSchemaField({ components: { FormItem, NumberPicker, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions Use Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, NumberPicker, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### Self Linkage #### Effects Use Cases ```tsx import React from 'react' import { createForm, onFieldValueChange } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' import './input.less' const form = createForm({ effects() { onFieldValueChange('color', (field) => { field.setComponentProps({ style: { backgroundColor: field.value, }, }) }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions Use Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' import './input.less' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### Asynchronous Linkage #### Effects Use Cases ```tsx import React from 'react' import { createForm, onFieldValueChange } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm({ effects() { onFieldValueChange('select', (field) => { field.loading = true setTimeout(() => { field.loading = false form.setFieldState('input', (state) => { //For the initial linkage, if the field cannot be found, setFieldState will push the update into the update queue until the field appears before performing the operation state.display = field.value }) }, 1000) }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions Use Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, scope: { asyncVisible(field, target) { field.loading = true setTimeout(() => { field.loading = false form.setFieldState(target, (state) => { //For the initial linkage, if the field cannot be found, setFieldState will push the update into the update queue until the field appears before performing the operation state.display = field.value }) }, 1000) }, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ## Passive Mode The core of the passive mode is based on - [onFieldReact](https://core.formilyjs.org/api/entry/field-effect-hooks#onfieldreact) Implement global reactive logic - [FieldReaction](https://core.formilyjs.org/api/models/field#fieldreaction) Implement partial responsive logic - [SchemaReactions](https://react.formilyjs.org/api/shared/schema#schemareactions) Implement the structured logical description in the Schema protocol (the internal implementation is based on FieldReaction) ### One-to-One Linkage #### Effects Use Cases ```tsx import React from 'react' import { createForm, onFieldReact } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm({ effects() { onFieldReact('input', (field) => { field.display = field.query('select').value() }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions Use Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### One-to-Many Linkage #### Effects Use Cases ```tsx import React from 'react' import { createForm, onFieldReact } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm({ effects() { onFieldReact('*(input1,input2)', (field) => { field.display = field.query('select').value() }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions Use Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### Rely on Linkage #### Effects Use Cases ```tsx import React from 'react' import { createForm, onFieldReact } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm({ effects() { onFieldReact('result', (field) => { field.value = field.query('dim_1').value() * field.query('dim_2').value() }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, NumberPicker, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions Use Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, NumberPicker, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### Chain Linkage #### Effects Use Cases ```tsx import React from 'react' import { createForm, onFieldReact } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm({ effects() { onFieldReact('input1', (field) => { field.visible = !!field.query('select').value() }) onFieldReact('input2', (field) => { field.visible = !!field.query('input1').value() }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions Use Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### Loop Linkage #### Effects Use Cases ```tsx import React from 'react' import { createForm, onFieldReact } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, NumberPicker } from '@formily/antd' const form = createForm({ effects() { onFieldReact('total', (field) => { const count = field.query('count').value() const price = field.query('price').value() if (count !== undefined && price !== undefined) { field.value = count * price } }) onFieldReact('price', (field) => { const total = field.query('total').value() const count = field.query('count').value() if (total !== undefined && count > 0) { field.value = total / count } }) onFieldReact('count', (field) => { const total = field.query('total').value() const price = field.query('price').value() if (total !== undefined && price > 0) { field.value = total / price } }) }, }) const SchemaField = createSchemaField({ components: { FormItem, NumberPicker, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions Use Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, NumberPicker, }, }) export default () => (
0 ? $deps[0] / $deps[1] : $self.value}}', }, }, }} /> 0 ? $deps[0] / $deps[1] : $self.value}}', }, }, }} /> {() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### Self Linkage #### Effects Use Cases ```tsx import React from 'react' import { createForm, onFieldReact } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' import './input.less' const form = createForm({ effects() { onFieldReact('color', (field) => { field.setComponentProps({ style: { backgroundColor: field.value, }, }) }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions Use Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' import './input.less' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### Asynchronous Linkage #### Effects Use Cases ```tsx import React from 'react' import { createForm, onFieldReact } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm({ effects() { onFieldReact('input', (field) => { const select = field.query('select').take() if (!select) return const selectValue = select.value select.loading = true if (selectValue) { setTimeout(() => { select.loading = false field.display = selectValue }, 1000) } }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions Use Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, scope: { asyncVisible(field) { const select = field.query('select').take() if (!select) return const selectValue = select.value select.loading = true if (selectValue) { setTimeout(() => { select.loading = false field.display = selectValue }, 1000) } }, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ================================================ FILE: docs/guide/advanced/linkages.zh-CN.md ================================================ # 实现联动逻辑 Formily1.x 中实现联动逻辑只有一种模式,也就是主动模式,必须要监听一个或多个字段的事件变化去控制另一个或者多个字段的状态,这样对于一对多联动场景很方便,但是对于多对一场景就很麻烦了,需要监听多个字段的变化去控制一个字段状态,所以 Formily2.x 提供了响应式机制,可以让联动支持被动式联动,只需要关注某个字段所依赖的字段即可,依赖字段变化了,被依赖的字段即可自动联动。 ## 主动模式 主动联动核心是基于 - [FormEffectHooks](https://core.formilyjs.org/zh-CN/api/entry/form-effect-hooks) - [FieldEffectHooks](https://core.formilyjs.org/zh-CN/api/entry/field-effect-hooks) - [setFormState](https://core.formilyjs.org/zh-CN/api/models/form#setformstate) - [setFieldState](https://core.formilyjs.org/zh-CN/api/models/form#setfieldstate) - [SchemaReactions](https://react.formilyjs.org/zh-CN/api/shared/schema#schemareactions) 实现主动联动,优点是实现一对多联动时非常方便 ### 一对一联动 #### Effects 用例 ```tsx import React from 'react' import { createForm, onFieldValueChange } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm({ effects() { onFieldValueChange('select', (field) => { form.setFieldState('input', (state) => { //对于初始联动,如果字段找不到,setFieldState会将更新推入更新队列,直到字段出现再执行操作 state.display = field.value }) }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions 用例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### 一对多联动 #### Effects 用例 ```tsx import React from 'react' import { createForm, onFieldValueChange } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm({ effects() { onFieldValueChange('select', (field) => { form.setFieldState('*(input1,input2)', (state) => { //对于初始联动,如果字段找不到,setFieldState会将更新推入更新队列,直到字段出现再执行操作 state.display = field.value }) }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions 用例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### 依赖联动 #### Effects 用例 ```tsx import React from 'react' import { createForm, onFieldValueChange } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm({ effects() { onFieldValueChange('dim_1', (field) => { const dim1 = field.value const dim2 = field.query('dim_2').value() form.setFieldState('result', (state) => { state.value = dim1 * dim2 }) }) onFieldValueChange('dim_2', (field) => { const dim1 = field.query('dim_1').value() const dim2 = field.value || 0 form.setFieldState('result', (state) => { state.value = dim1 * dim2 }) }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, NumberPicker, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions 用例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, NumberPicker, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### 链式联动 #### Effects 用例 ```tsx import React from 'react' import { createForm, onFieldValueChange } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm({ effects() { onFieldValueChange('select', (field) => { form.setFieldState('input1', (state) => { //对于初始联动,如果字段找不到,setFieldState会将更新推入更新队列,直到字段出现再执行操作 state.visible = !!field.value }) }) onFieldValueChange('input1', (field) => { form.setFieldState('input2', (state) => { //对于初始联动,如果字段找不到,setFieldState会将更新推入更新队列,直到字段出现再执行操作 state.visible = !!field.value }) }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions 用例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### 循环联动 #### Effects 用例 ```tsx import React from 'react' import { createForm, onFieldInputValueChange } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, NumberPicker } from '@formily/antd' const form = createForm({ effects() { onFieldInputValueChange('total', (field) => { if (field.value === undefined) return form.setFieldState('count', (state) => { const price = form.values.price if (!price) return state.value = field.value / price }) form.setFieldState('price', (state) => { const count = form.values.count if (!count) return state.value = field.value / count }) }) onFieldInputValueChange('price', (field) => { form.setFieldState('total', (state) => { const count = form.values.count if (count === undefined) return state.value = field.value * count }) }) onFieldInputValueChange('count', (field) => { form.setFieldState('total', (state) => { const price = form.values.price if (price === undefined) return state.value = field.value * price }) }) }, }) const SchemaField = createSchemaField({ components: { FormItem, NumberPicker, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions 用例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, NumberPicker, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### 自身联动 #### Effects 用例 ```tsx import React from 'react' import { createForm, onFieldValueChange } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' import './input.less' const form = createForm({ effects() { onFieldValueChange('color', (field) => { field.setComponentProps({ style: { backgroundColor: field.value, }, }) }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions 用例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' import './input.less' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### 异步联动 #### Effects 用例 ```tsx import React from 'react' import { createForm, onFieldValueChange } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm({ effects() { onFieldValueChange('select', (field) => { field.loading = true setTimeout(() => { field.loading = false form.setFieldState('input', (state) => { //对于初始联动,如果字段找不到,setFieldState会将更新推入更新队列,直到字段出现再执行操作 state.display = field.value }) }, 1000) }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions 用例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, scope: { asyncVisible(field, target) { field.loading = true setTimeout(() => { field.loading = false form.setFieldState(target, (state) => { //对于初始联动,如果字段找不到,setFieldState会将更新推入更新队列,直到字段出现再执行操作 state.display = field.value }) }, 1000) }, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ## 被动模式 被动模式的核心是基于 - [onFieldReact](https://core.formilyjs.org/zh-CN/api/entry/field-effect-hooks#onfieldreact)实现全局响应式逻辑 - [FieldReaction](https://core.formilyjs.org/zh-CN/api/models/field#fieldreaction)实现局部响应式逻辑 - [SchemaReactions](https://react.formilyjs.org/zh-CN/api/shared/schema#schemareactions)实现 Schema 协议中的结构化逻辑描述(内部是基于 FieldReaction 来实现的) ### 一对一联动 #### Effects 用例 ```tsx import React from 'react' import { createForm, onFieldReact } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm({ effects() { onFieldReact('input', (field) => { field.display = field.query('select').value() }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions 用例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### 一对多联动 #### Effects 用例 ```tsx import React from 'react' import { createForm, onFieldReact } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm({ effects() { onFieldReact('*(input1,input2)', (field) => { field.display = field.query('select').value() }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions 用例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### 依赖联动 #### Effects 用例 ```tsx import React from 'react' import { createForm, onFieldReact } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm({ effects() { onFieldReact('result', (field) => { field.value = field.query('dim_1').value() * field.query('dim_2').value() }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, NumberPicker, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions 用例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, NumberPicker, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### 链式联动 #### Effects 用例 ```tsx import React from 'react' import { createForm, onFieldReact } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm({ effects() { onFieldReact('input1', (field) => { field.visible = !!field.query('select').value() }) onFieldReact('input2', (field) => { field.visible = !!field.query('input1').value() }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions 用例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### 循环联动 #### Effects 用例 ```tsx import React from 'react' import { createForm, onFieldReact } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, NumberPicker } from '@formily/antd' const form = createForm({ effects() { onFieldReact('total', (field) => { const count = field.query('count').value() const price = field.query('price').value() if (count !== undefined && price !== undefined) { field.value = count * price } }) onFieldReact('price', (field) => { const total = field.query('total').value() const count = field.query('count').value() if (total !== undefined && count > 0) { field.value = total / count } }) onFieldReact('count', (field) => { const total = field.query('total').value() const price = field.query('price').value() if (total !== undefined && price > 0) { field.value = total / price } }) }, }) const SchemaField = createSchemaField({ components: { FormItem, NumberPicker, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions 用例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, NumberPicker, }, }) export default () => (
0 ? $deps[0] / $deps[1] : $self.value}}', }, }, }} /> 0 ? $deps[0] / $deps[1] : $self.value}}', }, }, }} /> {() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### 自身联动 #### Effects 用例 ```tsx import React from 'react' import { createForm, onFieldReact } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' import './input.less' const form = createForm({ effects() { onFieldReact('color', (field) => { field.setComponentProps({ style: { backgroundColor: field.value, }, }) }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions 用例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' import './input.less' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ### 异步联动 #### Effects 用例 ```tsx import React from 'react' import { createForm, onFieldReact } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm({ effects() { onFieldReact('input', (field) => { const select = field.query('select').take() if (!select) return const selectValue = select.value select.loading = true if (selectValue) { setTimeout(() => { select.loading = false field.display = selectValue }, 1000) } }) }, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` #### SchemaReactions 用例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormConsumer } from '@formily/react' import { Form, FormItem, Input, Select } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, Select, }, scope: { asyncVisible(field) { const select = field.query('select').take() if (!select) return const selectValue = select.value select.loading = true if (selectValue) { setTimeout(() => { select.loading = false field.display = selectValue }, 1000) } }, }, }) export default () => (
{() => (
{JSON.stringify(form.values, null, 2)}
)}
) ``` ================================================ FILE: docs/guide/advanced/validate.md ================================================ # Form Validation Formily's form validation uses the extremely powerful and flexible @formily/validator validation engine. There are two main scenarios for validation: - Markup(JSON) Schema scene protocol verification property verification, using JSON Schema's own verification property and x-validator property to achieve verification - Pure JSX scene verification properties, use validator property to achieve verification At the same time, we can also implement linkage verification in effects or x-reactions/reactions Specific rule verification document reference [FieldValidator](https://core.formilyjs.org/api/models/field#fieldvalidator) Form validation is an important part of optimizing user experience and ensuring data accuracy in forms. Formily provides various validation methods, including built-in rule validation, built-in format validation, and custom rule validation. In the following sections, we will introduce these validation methods one by one. ## Built-in rule check Built-in rule validation refers to the common validation rules provided by Formily, such as required, max, min, len, enum, const, multipleOf, etc. These rules can be described using JSON Schema properties or the x-validator property. Formily supports multiple ways of writing built-in rules and it is recommended for teams to establish internal conventions based on their usage habits. #### Markup Schema Use Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, NumberPicker, }, }) export default () => (
) ``` #### JSON Schema Use Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, NumberPicker, }, }) const schema = { type: 'object', properties: { required_1: { name: 'required_1', title: 'Required', type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, required_2: { name: 'required_2', title: 'Required', type: 'string', 'x-validator': { required: true, }, 'x-decorator': 'FormItem', 'x-component': 'Input', }, required_3: { name: 'required_3', title: 'Required', type: 'string', 'x-validator': [ { required: true, }, ], 'x-decorator': 'FormItem', 'x-component': 'Input', }, max_1: { name: 'max_1', title: 'Maximum value (>5 error)', type: 'number', maximum: 5, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, max_2: { name: 'max_2', title: 'Maximum value (>5 error)', type: 'number', 'x-validator': { maximum: 5, }, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, max_3: { name: 'max_3', title: 'Maximum value (>5 error)', type: 'number', 'x-validator': [ { maximum: 5, }, ], 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, max_4: { name: 'max_4', title: 'Maximum value (>=5 error))', type: 'number', exclusiveMaximum: 5, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, max_5: { name: 'max_5', title: 'Maximum value (>=5 error))', type: 'number', 'x-validator': { exclusiveMaximum: 5, }, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, max_6: { name: 'max_6', title: 'Maximum value (>=5 error))', type: 'number', 'x-validator': [ { exclusiveMaximum: 5, }, ], 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, min_1: { name: 'min_1', title: 'Minimum value (<5 error))', type: 'number', minimum: 5, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, min_2: { name: 'min_2', title: 'Minimum value (<5 error))', type: 'number', 'x-validator': { minimum: 5, }, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, min_3: { name: 'min_3', title: 'Minimum value (<5 error))', type: 'string', 'x-validator': [ { minimum: 5, }, ], 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, min_4: { name: 'min_4', title: 'Minimum value (<=5 error))', type: 'number', exclusiveMinimum: 5, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, min_5: { name: 'min_5', title: 'Minimum value (<=5 error))', type: 'number', 'x-validator': { exclusiveMinimum: 5, }, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, min_6: { name: 'min_6', title: 'Minimum value (<=5 error))', type: 'number', 'x-validator': [ { exclusiveMinimum: 5, }, ], 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, length_1: { name: 'length_1', title: 'Length is 5', type: 'string', 'x-validator': { len: 5, }, 'x-decorator': 'FormItem', 'x-component': 'Input', }, length_2: { name: 'length_2', title: 'Length is 5', type: 'string', 'x-validator': [ { len: 5, }, ], 'x-decorator': 'FormItem', 'x-component': 'Input', }, maxlength_1: { name: 'maxlength_1', title: 'Maximum length is 5', type: 'string', maxLength: 5, 'x-decorator': 'FormItem', 'x-component': 'Input', }, maxlength_2: { name: 'maxlength_2', title: 'Maximum length is 5', type: 'string', 'x-validator': { max: 5, }, 'x-decorator': 'FormItem', 'x-component': 'Input', }, maxlength_3: { name: 'maxlength_3', title: 'Maximum length is 5', type: 'string', 'x-validator': [ { max: 5, }, ], 'x-decorator': 'FormItem', 'x-component': 'Input', }, minlength_1: { name: 'minlength_1', title: 'Minimum length is 5', type: 'string', minLength: 5, 'x-decorator': 'FormItem', 'x-component': 'Input', }, minlength_2: { name: 'minlength_2', title: 'Minimum length is 5', type: 'string', 'x-validator': { min: 5, }, 'x-decorator': 'FormItem', 'x-component': 'Input', }, minlength_3: { name: 'minlength_3', title: 'Minimum length is 5', type: 'string', 'x-validator': [ { min: 5, }, ], 'x-decorator': 'FormItem', 'x-component': 'Input', }, whitespace: { name: 'whitespace', title: 'Exclude pure whitespace characters', type: 'string', 'x-validator': [ { whitespace: true, }, ], 'x-decorator': 'FormItem', 'x-component': 'Input', }, enum: { name: 'enum', title: 'Enumeration match', type: 'string', 'x-validator': [ { enum: ['1', '2', '3'], }, ], 'x-decorator': 'FormItem', 'x-component': 'Input', }, const: { name: 'const', title: 'Constant match', type: 'string', const: '123', 'x-decorator': 'FormItem', 'x-component': 'Input', }, multipleOf: { name: 'multipleOf', title: 'Divisible match', type: 'string', multipleOf: 2, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, }, } export default () => (
) ``` #### Pure JSX Case ```tsx import React from 'react' import { createForm } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm() export default () => (
) ``` ## Built-in Format Verification #### Markup Schema Cases ```tsx import React, { Fragment } from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const renderFormat = (format: string, key: number) => { return ( ) } const FORMATS = [ 'url', 'email', 'phone', 'ipv6', 'ipv4', 'number', 'integer', 'qq', 'idcard', 'money', 'zh', 'date', 'zip', ] export default () => (
{FORMATS.map(renderFormat)}
) ``` #### JSON Schema Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() const schema = { type: 'object', properties: {}, } const FORMATS = [ 'url', 'email', 'phone', 'ipv6', 'ipv4', 'number', 'integer', 'qq', 'idcard', 'money', 'zh', 'date', 'zip', ] FORMATS.forEach((key) => { Object.assign(schema.properties, { [`${key}_1`]: { title: `${key} format`, type: 'string', required: true, format: key, 'x-decorator': 'FormItem', 'x-component': 'Input', }, [`${key}_2`]: { title: `${key} format`, type: 'string', required: true, 'x-validator': key, 'x-decorator': 'FormItem', 'x-component': 'Input', }, [`${key}_3`]: { title: `${key} format`, type: 'string', required: true, 'x-validator': { format: key, }, 'x-decorator': 'FormItem', 'x-component': 'Input', }, [`${key}_4`]: { title: `${key} format`, type: 'string', required: true, 'x-validator': [key], 'x-decorator': 'FormItem', 'x-component': 'Input', }, [`${key}_5`]: { title: `${key} format`, type: 'string', required: true, 'x-validator': [ { format: key, }, ], 'x-decorator': 'FormItem', 'x-component': 'Input', }, }) }) const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) export default () => (
) ``` #### Pure JSX Cases ```tsx import React, { Fragment } from 'react' import { createForm } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() const renderFormat = (format: string, key: number) => { return ( ) } const FORMATS = [ 'url', 'email', 'phone', 'ipv6', 'ipv4', 'number', 'integer', 'qq', 'idcard', 'money', 'zh', 'date', 'zip', ] export default () => (
{FORMATS.map(renderFormat)}
) ``` ## Custom Rule Verification #### Markup Schema Cases ```tsx import React from 'react' import { createForm, registerValidateRules } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, NumberPicker, }, }) registerValidateRules({ global_1(value) { if (!value) return '' return value !== '123' ? 'error❎' : '' }, global_2(value, rule) { if (!value) return '' return value !== '123' ? rule.message : '' }, global_3(value) { if (!value) return '' return value === '123' }, global_4(value) { if (!value) return '' if (value < 10) { return { type: 'error', message: 'The value cannot be less than 10', } } else if (value < 100) { return { type: 'warning', message: 'The value is within 100', } } else if (value < 1000) { return { type: 'success', message: 'The value is greater than 100 and less than 1000', } } }, }) export default () => (
{ if (!value) return '' return value !== '123' ? 'error❎' : '' }} x-component="Input" x-decorator="FormItem" /> { if (!value) return '' if (value < 10) { return { type: 'error', message: 'The value cannot be less than 10', } } else if (value < 100) { return { type: 'warning', message: 'The value is within 100', } } else if (value < 1000) { return { type: 'success', message: 'The value is greater than 100 and less than 1000', } } }} x-component="NumberPicker" x-decorator="FormItem" />
) ``` #### JSON Schema Cases ```tsx import React from 'react' import { createForm, registerValidateRules } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, NumberPicker, }, }) registerValidateRules({ global_1(value) { if (!value) return '' return value !== '123' ? 'error❎' : '' }, global_2(value, rule) { if (!value) return '' return value !== '123' ? rule.message : '' }, global_3(value) { if (!value) return '' return value === '123' }, global_4(value) { if (!value) return '' if (value < 10) { return { type: 'error', message: 'The value cannot be less than 10', } } else if (value < 100) { return { type: 'warning', message: 'The value is within 100', } } else if (value < 1000) { return { type: 'success', message: 'The value is greater than 100 and less than 1000', } } }, }) const schema = { type: 'object', properties: { global_style_1: { title: 'Global registration style', required: true, 'x-validator': { global_1: true, }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, global_style_2: { title: 'Global registration style', required: true, 'x-validator': { global_2: true, message: 'error❎', }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, global_style_3: { title: 'Global registration style', required: true, 'x-validator': { global_3: true, message: 'error❎', }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, global_style_4: { title: 'Global registration style', required: true, 'x-validator': { global_4: true, }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, validator_style_1: { title: 'Locally defined style', required: true, 'x-validator': `{{(value)=> { if (!value) return '' return value !== '123' ? 'error❎' : '' }}}`, 'x-component': 'Input', 'x-decorator': 'FormItem', }, validator_style_2: { title: 'Locally defined style', required: true, 'x-validator': { validator: `{{(value, rule)=> { if (!value) return '' return value !== '123' ? rule.message : '' }}}`, message: 'error❎', }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, validator_style_3: { title: 'Locally defined style', required: true, 'x-validator': { validator: `{{(value, rule)=> { if (!value) return '' return value === '123' }}}`, message: 'error❎', }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, validator_style_4: { title: 'Locally defined style', required: true, 'x-validator': `{{(value, rule)=> { if (!value) return '' if (value < 10) { return { type: 'error', message: 'The value cannot be less than 10', } } else if (value < 100) { return { type: 'warning', message: 'The value is within 100', } } else if (value < 1000) { return { type: 'success', message: 'The value is greater than 100 and less than 1000', } } }}}`, 'x-component': 'Input', 'x-decorator': 'FormItem', }, }, } export default () => (
) ``` #### Pure JSX Cases ```tsx import React from 'react' import { createForm, registerValidateRules } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm() registerValidateRules({ global_1(value) { if (!value) return '' return value !== '123' ? 'error❎' : '' }, global_2(value, rule) { if (!value) return '' return value !== '123' ? rule.message : '' }, global_3(value) { if (!value) return '' return value === '123' }, global_4(value) { if (!value) return '' if (value < 10) { return { type: 'error', message: 'The value cannot be less than 10', } } else if (value < 100) { return { type: 'warning', message: 'The value is within 100', } } else if (value < 1000) { return { type: 'success', message: 'The value is greater than 100 and less than 1000', } } }, }) export default () => (
{ if (!value) return '' return value !== '123' ? 'error❎' : '' }} component={[Input]} decorator={[FormItem]} /> { if (!value) return '' if (value < 10) { return { type: 'error', message: 'The value cannot be less than 10', } } else if (value < 100) { return { type: 'warning', message: 'The value is within 100', } } else if (value < 1000) { return { type: 'success', message: 'The value is greater than 100 and less than 1000', } } }} component={[NumberPicker]} decorator={[FormItem]} /> ) ``` ## Using Third-Party Validation Libraries With the powerful validation engine of Formily, it is extremely convenient to adapt to third-party validation libraries such as yup. Here is an example of how to use it: #### JSON Schema Cases ```tsx import React from 'react' import { createForm, registerValidateRules } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' import { string } from 'yup' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, NumberPicker, }, }) registerValidateRules({ yup: async (value, rule) => { try { await rule.yup().validate(value) return '' // Return an empty string when validation is successful } catch (err) { return err.errors.join(',') // Return the error message when validation fails } }, }) const schema = { type: 'object', properties: { global_style_1: { title: 'Maximum length is 2', 'x-validator': [ { triggerType: 'onBlur', yup: () => string().required('required'), }, { triggerType: 'onBlur', yup: () => string().max(2, 'Maximum length is 2'), }, ], 'x-component': 'Input', 'x-decorator': 'FormItem', }, global_style_2: { title: 'email', required: true, 'x-validator': { triggerType: 'onBlur', yup: () => string().email(), }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, }, } export default () => (
) ``` #### Pure JSX Cases ```tsx import React from 'react' import { createForm, registerValidateRules } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' import { string, number } from 'yup' const form = createForm() registerValidateRules({ yup: async (value, rule) => { try { await rule.yup().validate(value) return '' // Return an empty string when validation is successful } catch (err) { return err.errors.join(',') // Return the error message when validation fails } }, }) export default () => (
string().email(), }} component={[Input]} decorator={[FormItem]} /> number().max(30), }} component={[NumberPicker]} decorator={[FormItem]} /> string().email(), }} component={[Input]} decorator={[FormItem]} /> ) ``` ## Custom Format Verification #### Markup Schema Cases ```tsx import React from 'react' import { createForm, registerValidateFormats } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) registerValidateFormats({ custom_format: /123/, }) export default () => (
) ``` #### JSON Schema Cases ```tsx import React from 'react' import { createForm, registerValidateFormats } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) registerValidateFormats({ custom_format: /123/, }) const schema = { type: 'object', properties: { global_style_1: { title: 'Global registration style', required: true, 'x-validator': { format: 'custom_format', message: 'error❎', }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, global_style_2: { title: 'Global registration style', required: true, 'x-validator': 'custom_format', 'x-component': 'Input', 'x-decorator': 'FormItem', }, global_style_3: { title: 'Global registration style', required: true, 'x-validator': ['custom_format'], 'x-component': 'Input', 'x-decorator': 'FormItem', }, global_style_4: { title: 'Global registration style', required: true, 'x-validator': { format: 'custom_format', message: 'error❎', }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, validator_style_1: { title: 'Locally defined style', required: true, pattern: /123/, 'x-component': 'Input', 'x-decorator': 'FormItem', }, validator_style_2: { title: 'Locally defined style', required: true, pattern: '123', 'x-component': 'Input', 'x-decorator': 'FormItem', }, validator_style_3: { title: 'Locally defined style', required: true, 'x-validator': { pattern: /123/, message: 'error❎', }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, validator_style_4: { title: 'Locally defined style', required: true, 'x-validator': { pattern: '123', message: 'error❎', }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, }, } export default () => (
) ``` #### Pure JSX Cases ```tsx import React from 'react' import { createForm, registerValidateFormats } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() registerValidateFormats({ custom_format: /123/, }) export default () => (
) ``` ## Asynchronous Verification #### Markup Schema Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) export default () => (
{ return new Promise((resolve) => { setTimeout(() => { if (!value) { resolve('') } if (value === '123') { resolve('') } else { resolve('error❎') } }, 1000) }) }} x-component="Input" x-decorator="FormItem" /> { return new Promise((resolve) => { setTimeout(() => { if (!value) { resolve('') } if (value === '123') { resolve('') } else { resolve('error❎') } }, 1000) }) }, }} x-component="Input" x-decorator="FormItem" />
) ``` #### JSON Schema Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const schema = { type: 'object', properties: { async_validate: { title: 'Asynchronous verification', required: true, 'x-validator': `{{(value) => { return new Promise((resolve) => { setTimeout(() => { if (!value) { resolve('') } if (value === '123') { resolve('') } else { resolve('error❎') } }, 1000) }) }}}`, 'x-component': 'Input', 'x-decorator': 'FormItem', }, async_validate_2: { title: 'Asynchronous verification (onBlur trigger)', required: true, 'x-validator': { triggerType: 'onBlur', validator: `{{(value) => { return new Promise((resolve) => { setTimeout(() => { if (!value) { resolve('') } if (value === '123') { resolve('') } else { resolve('错误❎') } }, 1000) }) }}}`, }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, }, } export default () => (
) ``` #### Pure JSX Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() export default () => (
{ return new Promise((resolve) => { setTimeout(() => { if (!value) { resolve('') } if (value === '123') { resolve('') } else { resolve('error❎') } }, 1000) }) }} component={[Input]} decorator={[FormItem]} /> { return new Promise((resolve) => { setTimeout(() => { if (!value) { resolve('') } if (value === '123') { resolve('') } else { resolve('error ❎') } }, 1000) }) }, }} component={[Input]} decorator={[FormItem]} /> ) ``` ## Linkage Verification #### Markup Schema Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { NumberPicker, FormItem, }, }) export default () => (
{ field.selfErrors = field.query('bb').value() >= field.value ? 'AA must be greater than BB' : '' }} x-component="NumberPicker" x-decorator="FormItem" /> { field.selfErrors = field.query('aa').value() <= field.value ? 'AA must be greater than BB' : '' }} x-component="NumberPicker" x-decorator="FormItem" />
) ``` #### JSON Schema Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { NumberPicker, FormItem, }, }) const schema = { type: 'object', properties: { aa: { title: 'AA', required: true, 'x-reactions': `{{(field) => { field.selfErrors = field.query('bb').value() >= field.value ? 'AA must be greater than BB' : '' }}}`, 'x-component': 'NumberPicker', 'x-decorator': 'FormItem', }, bb: { title: 'BB', required: true, 'x-reactions': { dependencies: ['aa'], fulfill: { state: { selfErrors: "{{$deps[0] <= $self.value ? 'AA must be greater than BB' : ''}}", }, }, }, 'x-component': 'NumberPicker', 'x-decorator': 'FormItem', }, }, } export default () => (
) ``` #### Pure JSX Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, NumberPicker } from '@formily/antd' const form = createForm() export default () => (
{ field.selfErrors = field.query('bb').value() >= field.value ? 'AA must be greater than BB' : '' }} component={[NumberPicker]} decorator={[FormItem]} /> { field.selfErrors = field.query('aa').value() <= field.value ? 'AA must be greater than BB' : '' }} component={[NumberPicker]} decorator={[FormItem]} /> ) ``` ## Custom Verification Messages Mainly through [registerValidateLocale](https://core.formilyjs.org/api/entry/form-validator-registry#registervalidatelocale) to customize the built-in verification messages ```tsx import React from 'react' import { createForm, registerValidateLocale, setValidateLanguage, } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) setValidateLanguage('en-US') registerValidateLocale({ 'en-US': { required: 'Custom required verification message', }, }) export default () => (
) ``` ================================================ FILE: docs/guide/advanced/validate.zh-CN.md ================================================ # 表单校验 Formily 的表单校验使用了极其强大且灵活的@formily/validator 校验引擎,校验主要分两种场景: - Markup(JSON) Schema 场景协议校验属性校验,使用 JSON Schema 本身的校验属性与 x-validator 属性实现校验 - 纯 JSX 场景校验属性,使用 validator 属性实现校验 同时我们还能在 effects 或者 x-reactions/reactions 中实现联动校验 具体规则校验文档参考 [FieldValidator](https://core.formilyjs.org/zh-CN/api/models/field#fieldvalidator) 表单校验是表单中优化用户体验和保证数据准确性的重要一环,Formily 提供了多种校验方式,包括内置规则校验、内置格式校验、自定义规则校验等,下面我们将逐一介绍这些校验方式。 ## 内置规则校验 内置规则校验是指 Formily 提供的一些常用校验规则,比如必填、最大值、最小值、长度、枚举、常量、整除等,实现了最简单和最通用的校验,这些规则可以通过 JSON Schema 的属性描述,也可以通过 x-validator 属性描述。Formily 支持多种形式的内置规则书写方式,建议团队内部根据使用习惯制定团队规范。 #### Markup Schema 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, NumberPicker, }, }) export default () => (
) ``` #### JSON Schema 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, NumberPicker, }, }) const schema = { type: 'object', properties: { required_1: { name: 'required_1', title: '必填', type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, required_2: { name: 'required_2', title: '必填', type: 'string', 'x-validator': { required: true, }, 'x-decorator': 'FormItem', 'x-component': 'Input', }, required_3: { name: 'required_3', title: '必填', type: 'string', 'x-validator': [ { required: true, }, ], 'x-decorator': 'FormItem', 'x-component': 'Input', }, max_1: { name: 'max_1', title: '最大值(>5报错)', type: 'number', maximum: 5, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, max_2: { name: 'max_2', title: '最大值(>5报错)', type: 'number', 'x-validator': { maximum: 5, }, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, max_3: { name: 'max_3', title: '最大值(>5报错)', type: 'number', 'x-validator': [ { maximum: 5, }, ], 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, max_4: { name: 'max_4', title: '最大值(>=5报错)', type: 'number', exclusiveMaximum: 5, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, max_5: { name: 'max_5', title: '最大值(>=5报错)', type: 'number', 'x-validator': { exclusiveMaximum: 5, }, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, max_6: { name: 'max_6', title: '最大值(>=5报错)', type: 'number', 'x-validator': [ { exclusiveMaximum: 5, }, ], 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, min_1: { name: 'min_1', title: '最小值(<5报错)', type: 'number', minimum: 5, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, min_2: { name: 'min_2', title: '最小值(<5报错)', type: 'number', 'x-validator': { minimum: 5, }, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, min_3: { name: 'min_3', title: '最小值(<5报错)', type: 'string', 'x-validator': [ { minimum: 5, }, ], 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, min_4: { name: 'min_4', title: '最小值(<=5报错)', type: 'number', exclusiveMinimum: 5, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, min_5: { name: 'min_5', title: '最小值(<=5报错)', type: 'number', 'x-validator': { exclusiveMinimum: 5, }, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, min_6: { name: 'min_6', title: '最小值(<=5报错)', type: 'number', 'x-validator': [ { exclusiveMinimum: 5, }, ], 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, length_1: { name: 'length_1', title: '长度为5', type: 'string', 'x-validator': { len: 5, }, 'x-decorator': 'FormItem', 'x-component': 'Input', }, length_2: { name: 'length_2', title: '长度为5', type: 'string', 'x-validator': [ { len: 5, }, ], 'x-decorator': 'FormItem', 'x-component': 'Input', }, maxlength_1: { name: 'maxlength_1', title: '最大长度为5', type: 'string', maxLength: 5, 'x-decorator': 'FormItem', 'x-component': 'Input', }, maxlength_2: { name: 'maxlength_2', title: '最大长度为5', type: 'string', 'x-validator': { max: 5, }, 'x-decorator': 'FormItem', 'x-component': 'Input', }, maxlength_3: { name: 'maxlength_3', title: '最大长度为5', type: 'string', 'x-validator': [ { max: 5, }, ], 'x-decorator': 'FormItem', 'x-component': 'Input', }, minlength_1: { name: 'minlength_1', title: '最小长度为5', type: 'string', minLength: 5, 'x-decorator': 'FormItem', 'x-component': 'Input', }, minlength_2: { name: 'minlength_2', title: '最小长度为5', type: 'string', 'x-validator': { min: 5, }, 'x-decorator': 'FormItem', 'x-component': 'Input', }, minlength_3: { name: 'minlength_3', title: '最小长度为5', type: 'string', 'x-validator': [ { min: 5, }, ], 'x-decorator': 'FormItem', 'x-component': 'Input', }, whitespace: { name: 'whitespace', title: '排除纯空白字符', type: 'string', 'x-validator': [ { whitespace: true, }, ], 'x-decorator': 'FormItem', 'x-component': 'Input', }, enum: { name: 'enum', title: '枚举匹配', type: 'string', 'x-validator': [ { enum: ['1', '2', '3'], }, ], 'x-decorator': 'FormItem', 'x-component': 'Input', }, const: { name: 'const', title: '常量匹配', type: 'string', const: '123', 'x-decorator': 'FormItem', 'x-component': 'Input', }, multipleOf: { name: 'multipleOf', title: '整除匹配', type: 'string', multipleOf: 2, 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', }, }, } export default () => (
) ``` #### 纯 JSX 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm() export default () => (
) ``` ## 内置格式校验 #### Markup Schema 案例 ```tsx import React, { Fragment } from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const renderFormat = (format: string, key: number) => { return ( ) } const FORMATS = [ 'url', 'email', 'phone', 'ipv6', 'ipv4', 'number', 'integer', 'qq', 'idcard', 'money', 'zh', 'date', 'zip', ] export default () => (
{FORMATS.map(renderFormat)}
) ``` #### JSON Schema 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() const schema = { type: 'object', properties: {}, } const FORMATS = [ 'url', 'email', 'phone', 'ipv6', 'ipv4', 'number', 'integer', 'qq', 'idcard', 'money', 'zh', 'date', 'zip', ] FORMATS.forEach((key) => { Object.assign(schema.properties, { [`${key}_1`]: { title: `${key}格式`, type: 'string', required: true, format: key, 'x-decorator': 'FormItem', 'x-component': 'Input', }, [`${key}_2`]: { title: `${key}格式`, type: 'string', required: true, 'x-validator': key, 'x-decorator': 'FormItem', 'x-component': 'Input', }, [`${key}_3`]: { title: `${key}格式`, type: 'string', required: true, 'x-validator': { format: key, }, 'x-decorator': 'FormItem', 'x-component': 'Input', }, [`${key}_4`]: { title: `${key}格式`, type: 'string', required: true, 'x-validator': [key], 'x-decorator': 'FormItem', 'x-component': 'Input', }, [`${key}_5`]: { title: `${key}格式`, type: 'string', required: true, 'x-validator': [ { format: key, }, ], 'x-decorator': 'FormItem', 'x-component': 'Input', }, }) }) const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) export default () => (
) ``` #### 纯 JSX 案例 ```tsx import React, { Fragment } from 'react' import { createForm } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() const renderFormat = (format: string, key: number) => { return ( ) } const FORMATS = [ 'url', 'email', 'phone', 'ipv6', 'ipv4', 'number', 'integer', 'qq', 'idcard', 'money', 'zh', 'date', 'zip', ] export default () => (
{FORMATS.map(renderFormat)}
) ``` ## 自定义规则校验 #### Markup Schema 案例 ```tsx import React from 'react' import { createForm, registerValidateRules } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, NumberPicker, }, }) registerValidateRules({ global_1(value) { if (!value) return '' return value !== '123' ? '错误了❎' : '' }, global_2(value, rule) { if (!value) return '' return value !== '123' ? rule.message : '' }, global_3(value) { if (!value) return '' return value === '123' }, global_4(value) { if (!value) return '' if (value < 10) { return { type: 'error', message: '数值不能小于10', } } else if (value < 100) { return { type: 'warning', message: '数值在100以内', } } else if (value < 1000) { return { type: 'success', message: '数值大于100小于1000', } } }, }) export default () => (
{ if (!value) return '' return value !== '123' ? '错误了❎' : '' }} x-component="Input" x-decorator="FormItem" /> { if (!value) return '' if (value < 10) { return { type: 'error', message: '数值不能小于10', } } else if (value < 100) { return { type: 'warning', message: '数值在100以内', } } else if (value < 1000) { return { type: 'success', message: '数值大于100小于1000', } } }} x-component="NumberPicker" x-decorator="FormItem" />
) ``` #### JSON Schema 案例 ```tsx import React from 'react' import { createForm, registerValidateRules } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, NumberPicker, }, }) registerValidateRules({ global_1(value) { if (!value) return '' return value !== '123' ? '错误了❎' : '' }, global_2(value, rule) { if (!value) return '' return value !== '123' ? rule.message : '' }, global_3(value) { if (!value) return '' return value === '123' }, global_4(value) { if (!value) return '' if (value < 10) { return { type: 'error', message: '数值不能小于10', } } else if (value < 100) { return { type: 'warning', message: '数值在100以内', } } else if (value < 1000) { return { type: 'success', message: '数值大于100小于1000', } } }, }) const schema = { type: 'object', properties: { global_style_1: { title: '全局注册风格', required: true, 'x-validator': { global_1: true, }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, global_style_2: { title: '全局注册风格', required: true, 'x-validator': { global_2: true, message: '错误了❎', }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, global_style_3: { title: '全局注册风格', required: true, 'x-validator': { global_3: true, message: '错误了❎', }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, global_style_4: { title: '全局注册风格', required: true, 'x-validator': { global_4: true, }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, validator_style_1: { title: '局部定义风格', required: true, 'x-validator': `{{(value)=> { if (!value) return '' return value !== '123' ? '错误了❎' : '' }}}`, 'x-component': 'Input', 'x-decorator': 'FormItem', }, validator_style_2: { title: '局部定义风格', required: true, 'x-validator': { validator: `{{(value, rule)=> { if (!value) return '' return value !== '123' ? rule.message : '' }}}`, message: '错误了❎', }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, validator_style_3: { title: '局部定义风格', required: true, 'x-validator': { validator: `{{(value, rule)=> { if (!value) return '' return value === '123' }}}`, message: '错误了❎', }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, validator_style_4: { title: '局部定义风格', required: true, 'x-validator': `{{(value, rule)=> { if (!value) return '' if (value < 10) { return { type: 'error', message: '数值不能小于10', } } else if (value < 100) { return { type: 'warning', message: '数值在100以内', } } else if (value < 1000) { return { type: 'success', message: '数值大于100小于1000', } } }}}`, 'x-component': 'Input', 'x-decorator': 'FormItem', }, }, } export default () => (
) ``` #### 纯 JSX 案例 ```tsx import React from 'react' import { createForm, registerValidateRules } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' const form = createForm() registerValidateRules({ global_1(value) { if (!value) return '' return value !== '123' ? '错误了❎' : '' }, global_2(value, rule) { if (!value) return '' return value !== '123' ? rule.message : '' }, global_3(value) { if (!value) return '' return value === '123' }, global_4(value) { if (!value) return '' if (value < 10) { return { type: 'error', message: '数值不能小于10', } } else if (value < 100) { return { type: 'warning', message: '数值在100以内', } } else if (value < 1000) { return { type: 'success', message: '数值大于100小于1000', } } }, }) export default () => (
{ if (!value) return '' return value !== '123' ? '错误了❎' : '' }} component={[Input]} decorator={[FormItem]} /> { if (!value) return '' if (value < 10) { return { type: 'error', message: '数值不能小于10', } } else if (value < 100) { return { type: 'warning', message: '数值在100以内', } } else if (value < 1000) { return { type: 'success', message: '数值大于100小于1000', } } }} component={[NumberPicker]} decorator={[FormItem]} /> ) ``` ## 使用第三方校验库 凭借 Formily 极为强大的校验引擎,能够极为便捷地适配诸如 yup 等第三方校验库。其使用示例如下: #### JSON Schema 案例 ```tsx import React from 'react' import { createForm, registerValidateRules } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' import { string } from 'yup' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, NumberPicker, }, }) registerValidateRules({ yup: async (value, rule) => { try { await rule.yup().validate(value) return '' // 验证成功时返回空字符串 } catch (err) { return err.errors.join(',') // 验证失败时返回错误信息 } }, }) const schema = { type: 'object', properties: { global_style_1: { title: '最大长度为 2', 'x-validator': [ { triggerType: 'onBlur', yup: () => string().required('必填'), }, { triggerType: 'onBlur', yup: () => string().max(2, '最大长度为 2'), }, ], 'x-component': 'Input', 'x-decorator': 'FormItem', }, global_style_2: { title: 'email', required: true, 'x-validator': { triggerType: 'onBlur', yup: () => string().email(), }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, }, } export default () => (
) ``` #### 纯 JSX 案例 ```tsx import React from 'react' import { createForm, registerValidateRules } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, Input, NumberPicker } from '@formily/antd' import { string, number } from 'yup' const form = createForm() registerValidateRules({ yup: async (value, rule) => { try { await rule.yup().validate(value) return '' // 验证成功时返回空字符串 } catch (err) { return err.errors.join(',') // 验证失败时返回错误信息 } }, }) export default () => (
string().email(), }} component={[Input]} decorator={[FormItem]} /> number().max(30), }} component={[NumberPicker]} decorator={[FormItem]} /> string().email(), }} component={[Input]} decorator={[FormItem]} /> ) ``` ## 自定义格式校验 #### Markup Schema 案例 ```tsx import React from 'react' import { createForm, registerValidateFormats } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) registerValidateFormats({ custom_format: /123/, }) export default () => (
) ``` #### JSON Schema 案例 ```tsx import React from 'react' import { createForm, registerValidateFormats } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) registerValidateFormats({ custom_format: /123/, }) const schema = { type: 'object', properties: { global_style_1: { title: '全局注册风格', required: true, 'x-validator': { format: 'custom_format', message: '错误❎', }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, global_style_2: { title: '全局注册风格', required: true, 'x-validator': 'custom_format', 'x-component': 'Input', 'x-decorator': 'FormItem', }, global_style_3: { title: '全局注册风格', required: true, 'x-validator': ['custom_format'], 'x-component': 'Input', 'x-decorator': 'FormItem', }, global_style_4: { title: '全局注册风格', required: true, 'x-validator': { format: 'custom_format', message: '错误❎', }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, validator_style_1: { title: '局部定义风格', required: true, pattern: /123/, 'x-component': 'Input', 'x-decorator': 'FormItem', }, validator_style_2: { title: '局部定义风格', required: true, pattern: '123', 'x-component': 'Input', 'x-decorator': 'FormItem', }, validator_style_3: { title: '局部定义风格', required: true, 'x-validator': { pattern: /123/, message: '错误了❎', }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, validator_style_4: { title: '局部定义风格', required: true, 'x-validator': { pattern: '123', message: '错误了❎', }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, }, } export default () => (
) ``` #### 纯 JSX 案例 ```tsx import React from 'react' import { createForm, registerValidateFormats } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() registerValidateFormats({ custom_format: /123/, }) export default () => (
) ``` ## 异步校验 #### Markup Schema 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) export default () => (
{ return new Promise((resolve) => { setTimeout(() => { if (!value) { resolve('') } if (value === '123') { resolve('') } else { resolve('错误❎') } }, 1000) }) }} x-component="Input" x-decorator="FormItem" /> { return new Promise((resolve) => { setTimeout(() => { if (!value) { resolve('') } if (value === '123') { resolve('') } else { resolve('错误❎') } }, 1000) }) }, }} x-component="Input" x-decorator="FormItem" />
) ``` #### JSON Schema 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const schema = { type: 'object', properties: { async_validate: { title: '异步校验', required: true, 'x-validator': `{{(value) => { return new Promise((resolve) => { setTimeout(() => { if (!value) { resolve('') } if (value === '123') { resolve('') } else { resolve('错误❎') } }, 1000) }) }}}`, 'x-component': 'Input', 'x-decorator': 'FormItem', }, async_validate_2: { title: '异步校验(onBlur触发)', required: true, 'x-validator': { triggerType: 'onBlur', validator: `{{(value) => { return new Promise((resolve) => { setTimeout(() => { if (!value) { resolve('') } if (value === '123') { resolve('') } else { resolve('错误❎') } }, 1000) }) }}}`, }, 'x-component': 'Input', 'x-decorator': 'FormItem', }, }, } export default () => (
) ``` #### 纯 JSX 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() export default () => (
{ return new Promise((resolve) => { setTimeout(() => { if (!value) { resolve('') } if (value === '123') { resolve('') } else { resolve('错误❎') } }, 1000) }) }} component={[Input]} decorator={[FormItem]} /> { return new Promise((resolve) => { setTimeout(() => { if (!value) { resolve('') } if (value === '123') { resolve('') } else { resolve('错误❎') } }, 1000) }) }, }} component={[Input]} decorator={[FormItem]} /> ) ``` ## 联动校验 #### Markup Schema 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { NumberPicker, FormItem, }, }) export default () => (
{ field.selfErrors = field.query('bb').value() >= field.value ? 'AA必须大于BB' : '' }} x-component="NumberPicker" x-decorator="FormItem" /> { field.selfErrors = field.query('aa').value() <= field.value ? 'AA必须大于BB' : '' }} x-component="NumberPicker" x-decorator="FormItem" />
) ``` #### JSON Schema 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, NumberPicker } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { NumberPicker, FormItem, }, }) const schema = { type: 'object', properties: { aa: { title: 'AA', required: true, 'x-reactions': `{{(field) => { field.selfErrors = field.query('bb').value() >= field.value ? 'AA必须大于BB' : '' }}}`, 'x-component': 'NumberPicker', 'x-decorator': 'FormItem', }, bb: { title: 'BB', required: true, 'x-reactions': { dependencies: ['aa'], fulfill: { state: { selfErrors: "{{$deps[0] <= $self.value ? 'AA必须大于BB' : ''}}", }, }, }, 'x-component': 'NumberPicker', 'x-decorator': 'FormItem', }, }, } export default () => (
) ``` #### 纯 JSX 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, NumberPicker } from '@formily/antd' const form = createForm() export default () => (
{ field.selfErrors = field.query('bb').value() >= field.value ? 'AA必须大于BB' : '' }} component={[NumberPicker]} decorator={[FormItem]} /> { field.selfErrors = field.query('aa').value() <= field.value ? 'AA必须大于BB' : '' }} component={[NumberPicker]} decorator={[FormItem]} /> ) ``` ## 定制校验文案 主要通过[registerValidateLocale](https://core.formilyjs.org/zh-CN/api/entry/form-validator-registry#registervalidatelocale)来定制内置校验文案 ```tsx import React from 'react' import { createForm, registerValidateLocale, setValidateLanguage, } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input } from '@formily/antd' const form = createForm() const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) setValidateLanguage('zh-CN') registerValidateLocale({ 'zh-CN': { required: '定制的必填校验文案', }, }) export default () => (
) ``` ================================================ FILE: docs/guide/contribution.md ================================================ # Contribution Guide ## Why become a contributor? Welcome to our community!**Formily** It is the only official open-source form framework announced by Alibaba. Its functions and quality are guaranteed. It has a large number of community users. Participating in contributions can make **Formily** stronger and allow more developers to enjoy a better experience of developing forms. we are very grateful to any people who initiated **Pull Request** for this project. ## What can I contribute? - Add&Update features - Add/Update unit test cases - Fix the existing issue - Documentation improvements - Other ## How to contribute? #### Pull Repository - Original repository: https://github.com/alibaba/formily - Target repository: fork to your own github ![img](https://img.alicdn.com/tfs/TB1NLrjxXY7gK0jSZKzXXaikpXa-2206-490.png) #### Pull Branch The original branch is alibaba/formily master, The branch after pulling should be quirkyshop/formily master > Note: The recommended branch name is [feat]-[name], [feat] is the type of this branch. Featdoc[other] is optional, and [name] is the name, just customize it. eg. unittest-core (meaning: add single test to the core) #### Submit Code The code style follows 2 spaces and no semicolons. Please do not include any console-related methods and debuggers in the code unless it is explained. After the development is completed, submit a pull request to the repository you forked.![img](https://img.alicdn.com/tfs/TB1HSvqxkT2gK0jSZFkXXcIQFXa-2050-898.png)![img](https://img.alicdn.com/tfs/TB1O.6mxbr1gK0jSZR0XXbP8XXa-1696-254.png) > Note the target repository on the left here(base repository is alibaba/formily master) . And then the doc-wiki of the current branch own repository on the right. #### PR Specification Reference documents: https://github.com/alibaba/formily/blob/master/.github/GIT_COMMIT_SPECIFIC.md - PR name: format: `(): ` For example: `feat(core): add unit test` - PR content: List the content of this change - PR requirements: the added feat content, as far as possible, make clear comments. And the corresponding single test coverage should be covered as much 关注梁帅抽大奖 possible. - BUGFIX requirements: If the modified issue is related to issues, please include the relevant issueID in the content. #### Review&Merge The review phase will enter a multi-review process,`@janryWang` is responsible for reviewing whether this change is merged, and other people will also participate in the discussion. The discussion will be stored in the PR of github, and the DingTalk group will also receive corresponding notifications. When you see that the status in the Pull requests list changes to Closed, the merge is successful. ![img](https://img.alicdn.com/tfs/TB1HUnjxXY7gK0jSZKzXXaikpXa-964-104.png) #### Synchronize source repository changes to repository after fork ``` # First, add "upstream" to your branch, that is, the source repository $ git remote add upstream https://github.com/alibaba/formily.git # Get the latest changes to the source repository $ git fetch upstream # Synchronize the changes of the source repository to the local branch $ git pull upstream master [The current local target branch, if not filled in, the current branch will be] ``` #### Project Development ```bash $ cd formily $ yarn install # Install overall project dependencies $ yarn build # Build all projects $ yarn test # Perform unit tests ``` #### Development Document Main project document ```bash $ yarn start ``` Core project documentation ```bash $ yarn workspace @formily/core start ``` React project documentation ```bash $ yarn workspace @formily/react start ``` Vue project documentation ```bash $ yarn workspace @formily/vue start ``` Antd project documentation ```bash $ yarn workspace @formily/antd start ``` Fusion project documentation ```bash $ yarn workspace @formily/next start ``` Reactive project documentation ```bash $ yarn workspace @formily/reactive start ``` ================================================ FILE: docs/guide/contribution.zh-CN.md ================================================ # 贡献指南 ## 为什么要成为贡献者? 欢迎您来到我们的社区!**Formily** 是阿里巴巴唯一官方向外公布的开源表单框架,功能和质量都有一定保证,拥有众多的社区使用者,参与贡献可以使 **Formily** 变得强大,也会让更多开发者能够享受到更好的开发表单的体验,我们非常感谢任何对本项目发起 **Pull Request** 的同学。 ## 我可以贡献什么? - features 新增/修改功能特性 - unitest 新增/修改单测 - bugfix 修复现有 issue 的问题 - doc 文档改进 - other 其他 ## 如何贡献? #### 拉取仓库 - 原始仓库:https://github.com/alibaba/formily - 目标仓库:fork 到自己的 github 上 ![img](https://img.alicdn.com/tfs/TB1NLrjxXY7gK0jSZKzXXaikpXa-2206-490.png) #### 拉取分支 原始分支是 alibaba/formily master,拉取后的分支应该是 quirkyshop/formily master > 注意:建议分支名为[feat]-[name],[feat]是这个分支的类型,可选的有[feat][unitest][docs][bugfix][other],[name]则是名字,自定义就好了。eg. unittest-core(意为:对核心补充单测) #### 提交代码 代码风格遵循 2 空格,无分号,非说明请不要在代码中附带任何 console 相关的方法及 debugger。 开发完成后,到自己 fork 出来的仓库提交 pull request ![img](https://img.alicdn.com/tfs/TB1HSvqxkT2gK0jSZFkXXcIQFXa-2050-898.png)![img](https://img.alicdn.com/tfs/TB1O.6mxbr1gK0jSZR0XXbP8XXa-1696-254.png) > 注意这里的左边目标仓库(base repository 是 alibaba/formily master) ,然后右边当前分支自己仓库的 doc-wiki #### PR 规范 参考文档:https://github.com/alibaba/formily/blob/master/.github/GIT_COMMIT_SPECIFIC.md - PR 名称:格式:`(): ` 举例:`feat(core): add unit test` - PR 内容:列举本次改动的内容 - PR 要求:增加的 feat 内容,尽量做到注释清晰,相应的单测覆盖要尽可能覆盖 - BUGFIX 要求:如果修改的问题和 issues 相关,请在内容中附上相关的 issueID。 #### 审核与合并 审核阶段会进入多 review 的流程,`@janryWang` 负责审核这个改动是否合并,其他同学也会参与讨论,讨论的经过都会留存在 github 的 PR 里,钉钉群也会收到相应的通知。 当看到 Pull requests 列表中的状态变为 Closed 即为合并成功。 ![img](https://img.alicdn.com/tfs/TB1HUnjxXY7gK0jSZKzXXaikpXa-964-104.png) #### 同步源仓库变更到 fork 后的仓库 ``` # 首先在自己的分支增加一个 upstream,即原仓库 $ git remote add upstream https://github.com/alibaba/formily.git # 获取原仓库最新的变更 $ git fetch upstream # 同步原仓库的改动到本地分支 $ git pull upstream master [当前本地目标分支,不填默认就是当前分支] ``` #### 项目开发 ```bash $ cd formily $ yarn install # 安装整体项目依赖 $ yarn build # 构建所有项目 $ yarn test # 执行单元测试 ``` #### 开发文档 主项目文档 ```bash $ yarn start ``` 内核项目文档 ```bash $ yarn workspace @formily/core start ``` React 项目文档 ```bash $ yarn workspace @formily/react start ``` Vue 项目文档 ```bash $ yarn workspace @formily/vue start ``` Antd 项目文档 ```bash $ yarn workspace @formily/antd start ``` Fusion 项目文档 ```bash $ yarn workspace @formily/next start ``` Reactive 项目文档 ```bash $ yarn workspace @formily/reactive start ``` ================================================ FILE: docs/guide/form-builder.md ================================================ # Form designer development guide ## Introduction ![](http://img.alicdn.com/imgextra/i2/O1CN01eI9FLz22tZek2jv7E_!!6000000007178-2-tps-3683-2272.png) Formily Form Designer is an extension package based on [designable](https://github.com/alibaba/designable). It inherits the basic capabilities of designable, and provides Formily basic form building and configuration capabilities. ## Core Concept The core concept of Designable is to turn the designer into a modular combination, everything can be replaced, Designable itself provides a series of out-of-the-box components for users to use, but if users are not satisfied with the components, they can directly replace the components. To achieve maximum flexible customization, that is, Designable itself does not provide any plug-in related APIs ## Install Ant Design users ```bash npm install --save @designable/formily-antd ``` Alibaba Fusion users ```bash npm install --save @designable/formily-next ``` ## Get started quickly [Example Source Code](https://github.com/alibaba/designable/tree/main/formily/antd/playground) ```tsx pure import 'antd/dist/antd.less' import React, { useMemo } from 'react' import ReactDOM from 'react-dom' import { Designer, //Designer root component, mainly used to deliver context DesignerToolsWidget, //Drawing board tool pendant ViewToolsWidget, //View switching tool pendant Workspace, //Workspace components, core components, used to manage drag and drop behavior in the workspace, tree node data, etc... OutlineTreeWidget, //Outline tree component, it will automatically identify the current workspace and display the tree nodes in the workspace ResourceWidget, //Drag and drop the source widget HistoryWidget, //History widget StudioPanel, //Main layout panel CompositePanel, //Left combined layout panel WorkspacePanel, //Workspace layout panel ToolbarPanel, //Toolbar layout panel ViewportPanel, //Viewport layout panel ViewPanel, //View layout panel SettingsPanel, //Configure the form layout panel on the right ComponentTreeWidget, //Component tree renderer } from '@designable/react' import { SettingsForm } from '@designable/react-settings-form' import { createDesigner, GlobalRegistry, Shortcut, KeyCode, } from '@designable/core' import { LogoWidget, ActionsWidget, PreviewWidget, SchemaEditorWidget, MarkupSchemaWidget, } from './widgets' import { saveSchema } from './service' import { Form, Field, Input, Select, TreeSelect, Cascader, Radio, Checkbox, Slider, Rate, NumberPicker, Transfer, Password, DatePicker, TimePicker, Upload, Switch, Text, Card, ArrayCards, ObjectContainer, ArrayTable, Space, FormTab, FormCollapse, FormLayout, FormGrid, } from '../src' GlobalRegistry.registerDesignerLocales({ 'zh-CN': { sources: { Inputs: 'Input controls', Layouts: 'Layout components', Arrays: 'Self-incrementing components', Displays: 'Display components', }, }, 'en-US': { sources: { Inputs: 'Inputs', Layouts: 'Layouts', Arrays: 'Arrays', Displays: 'Displays', }, }, }) const App = () => { const engine = useMemo( () => createDesigner({ shortcuts: [ new Shortcut({ codes: [ [KeyCode.Meta, KeyCode.S], [KeyCode.Control, KeyCode.S], ], handler(ctx) { saveSchema(ctx.engine) }, }), ], rootComponentName: 'Form', }), [] ) return ( } actions={}> {() => ( )} {(tree, onChange) => ( )} {(tree) => } {(tree) => } ) } ReactDOM.render(, document.getElementById('root')) ``` ================================================ FILE: docs/guide/form-builder.zh-CN.md ================================================ # 表单设计器开发指南 ## 介绍 ![](http://img.alicdn.com/imgextra/i2/O1CN01eI9FLz22tZek2jv7E_!!6000000007178-2-tps-3683-2272.png) Formily 表单设计器是基于[designable](https://github.com/alibaba/designable)而扩展出来的扩展包,它在继承了 designable 的基础能力上,提供了 Formily 基础表单的搭建和配置能力。 ## 核心理念 Designable 的核心理念是将设计器搭建变成模块化组合,一切可替换,Designable 本身提供了一系列开箱即用的组件给用户使用,但是如果用户对组件不满意,是可以直接替换组件,从而实现最大化灵活定制,也就是 Designable 本身是不会提供任何插槽 Plugin 相关的 API ## 安装 Ant Design 用户 ```bash npm install --save @designable/formily-antd ``` Alibaba Fusion 用户 ```bash npm install --save @designable/formily-next ``` ## 快速上手 [示例源代码](https://github.com/alibaba/designable/tree/main/formily/antd/playground) ```tsx pure import 'antd/dist/antd.less' import React, { useMemo } from 'react' import ReactDOM from 'react-dom' import { Designer, //设计器根组件,主要用于下发上下文 DesignerToolsWidget, //画板工具挂件 ViewToolsWidget, //视图切换工具挂件 Workspace, //工作区组件,核心组件,用于管理工作区内的拖拽行为,树节点数据等等... OutlineTreeWidget, //大纲树组件,它会自动识别当前工作区,展示出工作区内树节点 ResourceWidget, //拖拽源挂件 HistoryWidget, //历史记录挂件 StudioPanel, //主布局面板 CompositePanel, //左侧组合布局面板 WorkspacePanel, //工作区布局面板 ToolbarPanel, //工具栏布局面板 ViewportPanel, //视口布局面板 ViewPanel, //视图布局面板 SettingsPanel, //右侧配置表单布局面板 ComponentTreeWidget, //组件树渲染器 } from '@designable/react' import { SettingsForm } from '@designable/react-settings-form' import { createDesigner, GlobalRegistry, Shortcut, KeyCode, } from '@designable/core' import { LogoWidget, ActionsWidget, PreviewWidget, SchemaEditorWidget, MarkupSchemaWidget, } from './widgets' import { saveSchema } from './service' import { Form, Field, Input, Select, TreeSelect, Cascader, Radio, Checkbox, Slider, Rate, NumberPicker, Transfer, Password, DatePicker, TimePicker, Upload, Switch, Text, Card, ArrayCards, ObjectContainer, ArrayTable, Space, FormTab, FormCollapse, FormLayout, FormGrid, } from '../src' GlobalRegistry.registerDesignerLocales({ 'zh-CN': { sources: { Inputs: '输入控件', Layouts: '布局组件', Arrays: '自增组件', Displays: '展示组件', }, }, 'en-US': { sources: { Inputs: 'Inputs', Layouts: 'Layouts', Arrays: 'Arrays', Displays: 'Displays', }, }, }) const App = () => { const engine = useMemo( () => createDesigner({ shortcuts: [ new Shortcut({ codes: [ [KeyCode.Meta, KeyCode.S], [KeyCode.Control, KeyCode.S], ], handler(ctx) { saveSchema(ctx.engine) }, }), ], rootComponentName: 'Form', }), [] ) return ( } actions={}> {() => ( )} {(tree, onChange) => ( )} {(tree) => } {(tree) => } ) } ReactDOM.render(, document.getElementById('root')) ``` ================================================ FILE: docs/guide/index.md ================================================ # Introduction ## Problem As we all know, the form scene has always been the most complex scene in the front-end and back-end fields. What is the main complexity of it? - There are a lot of fields, how can the performance not deteriorate with the increase of the number of fields? - Field association logic is complex, how to implement complex linkage logic more simply? How to ensure that the form performance is not affected when the field is associated with the field? - One-to-Many (asynchronous) - Many-to-One (asynchronous) - Many-to-Many (asynchronous) - Complex form data management - Form value conversion logic is complex (front and back formats are inconsistent) - The logic of merging synchronous and asynchronous default values is complicated - Cross-form data communication, how to keep the performance from deteriorating with the increase in the number of fields? - Complex form state management - Focusing on the self-incrementing list scenario, how to make the array data move, and the field status can follow the move during the deletion process? - Scene reuse of forms - Query list - Dialog/Drawer form - Step form - Tab form - Dynamic rendering requirements are very strong - Field configuration allows non-professional front-ends to quickly build complex forms - Cross-terminal rendering, a JSON Schema, multi-terminal adaptation - How to describe the layout in the form protocol? - Vertical layout - Horizontal layout - Grid layout - Flexible layout - Free layout - How to describe the logic in the form protocol? So many problems, how to solve them, think about it, But we still have to find a solution,Not only to solve but also to solve elegantly, The Alibaba digital supply chain team, after experiencing a lot of middle and back-office practice and exploration, finally precipitated **Formily form solution**. All the problems mentioned above, after going through UForm to Formily1.x, until Formily2.x finally achieved the degree of **elegant solution**. So how does Formily 2.x solve these problems? ## Solution In order to solve the above problems, we can further refine the problem and come up with a breakthrough direction. ### Accurate Rendering In the React scenario, to realize a form requirement, most of them use setState to realize field data collection. because form data needs to be collected and some linkage requirements are realized.This implementation is very simple and the mental cost is very low, but it also introduces performance problems, because each input will cause all fields to be rendered in full. Although there is diff at the DOM update level, diff also has a computational cost, which wastes a lot of computational resources. In terms of time complexity, the initial rendering of the form is O(n), and the field input is also O(n), which is obviously unreasonable. Historical experience is always helpful to mankind. Decades ago, humans created the MVVM design pattern. The core of this design pattern is to abstract the view model and consume it at the DSL template layer.SL uses a certain dependency collection mechanism, and then uniformly schedules in the view model to ensure that each input is accurately rendered. This is the industrial-grade GUI form! It just so happened that the github community abstracted a state management solution called Mobx for such MVVM models. The core capabilities of [Mobx](https://github.com/mobxjs/mobx) are its dependency tracking mechanism and the abstraction capabilities of responsive models. Therefore, with the help of Mobx, the O(n) problem in the form field input process can be completely solved, and it can be solved very elegantly. However, during the implementation of Formily 2.x, it was discovered that Mobx still has some problems that are not compatible with Formily's core ideas. In the end, we only can reinvent one wheel,[@formily/reactive](https://reactive.formilyjs.org) which continues the core idea of Mobx. Mention here [react-hook-form](https://github.com/react-hook-form/react-hook-form) , Very popular, known as the industry’s top performance form solution, let’s take a look at its simplest case: ```tsx pure import React from 'react' import ReactDOM from 'react-dom' import { useForm } from 'react-hook-form' function App() { const { register, handleSubmit, errors } = useForm() // initialize the hook const onSubmit = (data) => { console.log(data) } return (
{/* register an input */} {errors.lastname && 'Last name is required.'} {errors.age && 'Please enter number for age.'}
) } ReactDOM.render(, document.getElementById('root')) ``` Although the value management achieves accurate rendering, when the verification is triggered, the form will still be rendered in full. Because of the update of the errors state, the overall controlled rendering is necessary to achieve synchronization. This is only the full rendering of the verification meeting. In fact, there is linkage. To achieve linkage with react-hook-form, it also requires overall controlled rendering to achieve linkage. Therefore, if you want to truly achieve accurate rendering, it must be Reactive! ### Domain Model As mentioned in the previous question, the linkage of forms is very complicated, including various relationships between fields. Let’s imagine that most form linkages are basically linkages triggered based on the values of certain fields. However, actual business requirements may be sophisticated. It is not only necessary to trigger linkage based on certain field values, but also based on other side-effect values, such as application status, server data status, page URL, internal data of a UI component of a field, and current Other data status of the field itself, some special asynchronous events, etc. Use a picture to describe: ![image-20210202081316031](//img.alicdn.com/imgextra/i3/O1CN01LWjBSt251w5BtGHW2_!!6000000007467-55-tps-1100-432.svg) As you can see from the above figure, in order to achieve a linkage relationship, the core is to associate certain state attributes of the field with certain data. Some data here can be external data or own data. For example, the display/hide of a field is associated with certain data, the value of a field is associated with certain data, and the disabling/editing of a field is associated with certain data. Here are three examples. We have actually abstracted it. One of the simplest Field model: ```typescript interface Field { value: any visible: boolean disabled: boolean } ``` Of course, does the Field model only have these 3 attributes? Definitely not, if we want to express a field, then the path of the field must have, Because we want to describe the entire form tree structure, at the same time, we also need to manage the properties of the field corresponding to the UI component. For example, Input and Select have their properties. For example, the placeholder of Input is associated with some data, or the drop-down option of Select is associated with some data, so you can understand it. So, our Field model can look like this: ```typescript interface Field { path: string[] value: any visible: boolean disabled: boolean component: [Component, ComponentProps] } ``` We have added the component attribute, which represents the UI component and UI component attribute corresponding to the field, so that the ability to associate certain data with the field component attribute, or even the field component, is realized. Are there any more? Of course, there are also, such as the outer package container of the field, usually we call it FormItem, which is mainly responsible for the interactive style of the field, such as the field title, the style of error prompts, etc., If we want to include more linkage, such as the linkage between certain data and FormItem, then we have to add the outer package container. There are many other attributes, which are not listed here. From the above ideas, we can see that in order to solve the linkage problem, no matter how abstract we are, the field model will eventually be abstracted. It contains all the states related to the field. As long as these states are manipulated, linkage can be triggered. Regarding accurate rendering, we have determined that we can choose a Reactive solution similar to Mobx. Although it is a reinvention of a wheel, the Reactive model is still very suitable for abstract responsive models. So based on the ability of Reactive, Formily, after constant trial and error and correction, finally designed a truly elegant form model. Such a form model solves the problem of the form domain, so it is also called a domain model. With such a domain model, we can make the linkage of the form enumerable and predictable, which also lays a solid foundation for the linkage of the protocol description to be discussed later. ### Path System The field model in the form domain model was mentioned earlier. If the design is more complete, it is not only a field model, but also a form model as the top-level model. The top-level model manages all the field models, and each field has its own Path. How to find these fields? The linkage relationship mentioned earlier is more of a passive dependency, but in some scenarios, we just need to modify the state of a field based on an asynchronous event action. Here is how to find a field elegantly. The same It has also undergone a lot of trial and error and correction. Formily's original path system @formily/path solves this problem very well. It not only makes the field lookup elegant, but it can also deal with the disgusting problem of inconsistent front-end and back-end data structures through destructuring expressions. ### Life Cycle With the help of Mobx and the path system, we have created a relatively complete form scheme, but after this abstraction, our scheme is like a black box, and the outside world cannot perceive the internal state flow process of the scheme. If you want to implement some logic in a certain process stage, you cannot achieve it. So, here we need another concept, the life cycle. As long as we expose the entire form life cycle as an event hook to the outside world, we can achieve an abstract but flexible form solution. ### Protocol Driven If you want to implement a dynamically configurable form, you must make the form structure serializable. There are many ways to serialize, which can be a UI description protocol based on the UI, or a data description protocol based on the data. Because the form itself is to maintain a copy of data, it is natural that for the form scenario, the data protocol is the most suitable. To describe the data structure, [JSON-Schema](https://json-schema.org/) is now the most popular in the industry. Because the JSON Schema protocol itself has many verification-related attributes, this is naturally associated with form verification. Is the UI description protocol really not suitable for describing forms? No, the UI description protocol is suitable for more general UI expressions. Of course, the description form is not a problem, but it will be more front-end protocol. On the contrary, JSON-Schema is expressible at the back-end model layer, and is more versatile in describing data. Therefore, the two protocols have their own strengths, but in the field of pure forms, JSON-Schema will be more domain-oriented. So, if we choose JSON-Schema, how do we describe the UI and how do we describe the logic? It is not realistic to simply describe the data and output the form pages available for actual business. The solution of [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form) is that data is data and UI is UI. The advantage of this is that each protocol is a very pure protocol, but it brings a large maintenance cost and understanding cost. To develop a form, users need to constantly switch between the two protocols mentally. Therefore, if you look at such a split from a technical perspective, it is very reasonable, but from a product perspective, the split is to throw the cost to the user. Therefore, Formily's form protocol will be more inclined to expand on JSON-Schema. So, how to expand? In order not to pollute the standard JSON-Schema attributes, we uniformly express the extended attributes in the x-\* format: ```json { "type": "string", "title": "String", "description": "This is a string", "x-component": "Input", "x-component-props": { "placeholder": "please enter" } } ``` In this way, the UI protocol and the data protocol are mixed together. As long as there is a unified extension agreement, the responsibilities of the two protocols can still be guaranteed to be single. Then, what if you want to wrap a UI container on certain fields? Here, Formily defines a new schema type called `void`. No stranger to void, there is also void element in W3C specification, and void keyword in js. The former represents virtual elements, and the latter represents virtual pointers. Therefore, in JSON Schema, void is introduced to represent a virtual data node, which means that the node does not occupy the actual data structure. So, we can do this: ```json { "type": "void", "title": "card", "description": "This is a card", "x-component": "Card", "properties": { "string": { "type": "string", "title": "String", "description": "This is a string", "x-component": "Input", "x-component-props": { "placeholder": "please enter" } } } } ``` In this way, a UI container can be described. Because the UI container can be described, we can easily encapsulate a scene-based component, such as FormStep. So how do we describe the linkage between fields? For example, one field needs to control the display and hide of another field. We can do this: ```json { "type": "object", "properties": { "source": { "type": "string", "title": "Source", "x-component": "Input", "x-component-props": { "placeholder": "please enter" } }, "target": { "type": "string", "title": "Target", "x-component": "Input", "x-component-props": { "placeholder": "please enter" }, "x-reactions": [ { "dependencies": ["source"], "when": "{{$deps[0] == '123'}}", "fulfill": { "state": { "visible": true } }, "otherwise": { "state": { "visible": false } } } ] } } } ``` The target field is described with the help of `x-reactions`, which depends on the value of the source field. If the value is `'123'`, the target field is displayed, otherwise it is hidden. This linkage method is a passive linkage. What if we want to achieve active linkage ? It can be like this: ```json { "type": "object", "properties": { "source": { "type": "string", "title": "Source", "x-component": "Input", "x-component-props": { "placeholder": "please enter" }, "x-reactions": [ { "when": "{{$self.value == '123'}}", "target": "target", "fulfill": { "state": { "visible": true } }, "otherwise": { "state": { "visible": false } } } ] }, "target": { "type": "string", "title": "Target", "x-component": "Input", "x-component-props": { "placeholder": "please enter" } } } } ``` Just change the location of `x-reactions`, put it on the source field, and then specify a target. It can be seen that our linkage is actually based on: - condition - Condition-satisfied action - Unsatisfied action To achieve. Because the internal state management uses the [@formily/reactive](https://reactive.formilyjs.org) solution similar to Mobx, Formily easily realizes passive and active linkage scenarios, covering most business needs. Therefore, our form can be described by protocol, and it can be configurable no matter how complicated the layout is or the linkage is very complicated. ### Layered Architecture I talked about the solutions to various problems at the beginning, so how do we design now to make Formily more self-consistent and elegant? ![](https://img.alicdn.com/imgextra/i4/O1CN019qbf1b1ChnTfT9x3X_!!6000000000113-55-tps-1939-1199.svg) This picture mainly divides Formily into the kernel layer, UI bridge layer, extended component layer, and configuration application layer. The kernel layer is UI-independent. It ensures that the logic and state of user management are not coupled to any framework. This has several advantages: - Logic and UI framework are decoupled, and framework-level migration will be done in the future, without extensive refactoring of business code. - The learning cost is uniform. If the user uses @formily/react, the business will be migrated to @formily/vue in the future, and the user does not need to learn again. JSON Schema exists independently and is consumed by the UI bridging layer, ensuring the absolute consistency of protocol drivers under different UI frameworks, and there is no need to repeatedly implement protocol parsing logic. Extend the component layer to provide a series of form scene components to ensure that users can use it out of the box. No need to spend a lot of time for secondary development. ## Competitive Product Comparison ```tsx /** * inline: true */ import React from 'react' import { Table, Tooltip } from 'antd' import { QuestionCircleOutlined } from '@ant-design/icons' const text = (content, tooltips) => { if (tooltips) { return (
{content}
) } return content } const dataSource = [ { feature: 'Custom component access cost', antd: '4.x low access cost', fusion: 'high', formik: 'low', finalForm: 'low', schemaForm: text('high', 'Because of coupling bootstrap'), hookForm: text('high', 'Because of coupling React Ref'), 'formily1.x': 'low', 'formily2.x': 'low', }, { feature: 'performance', antd: text( '4.x performance is better', 'Only solved the value synchronization and accurate rendering' ), fusion: 'bad', formik: 'bad', finalForm: text( 'better', 'But only solved the value synchronization and accurate rendering' ), schemaForm: 'bad', hookForm: text( 'good', 'But only solved the value synchronization and accurate rendering' ), 'formily1.x': text( 'excellent', 'Can solve the precise rendering in the linkage process' ), 'formily2.x': text( 'excellent', 'Can solve the precise rendering in the linkage process' ), }, { feature: 'Whether to support dynamic rendering', antd: 'no', fusion: 'no', formik: 'no', finalForm: 'no', schemaForm: 'yes', hookForm: 'no', 'formily1.x': 'yes', 'formily2.x': 'yes', }, { feature: 'Whether to use out of the box', antd: 'yes', fusion: 'yes', formik: 'no', finalForm: 'no', schemaForm: 'yes', hookForm: 'no', 'formily1.x': 'yes', 'formily2.x': 'yes', }, { feature: 'Whether to support cross-terminal', antd: 'no', fusion: 'no', formik: 'no', finalForm: 'no', schemaForm: 'no', hookForm: 'no', 'formily1.x': 'yes', 'formily2.x': 'yes', }, { feature: 'Development efficiency', antd: 'general', fusion: 'generalv', formik: 'general', finalForm: 'general', schemaForm: text( 'low', 'Source code development requires manual maintenance of JSON' ), hookForm: 'general', 'formily1.x': 'high', 'formily2.x': 'high', }, { feature: 'Learning cost', antd: 'easy', fusion: 'easy', formik: 'easy', finalForm: 'hard', schemaForm: 'hard', hookForm: 'easy', 'formily1.x': 'very hard', 'formily2.x': text('hard', 'The concept is drastically reduced'), }, { feature: 'View code maintainability', antd: text('bad', 'Lots of conditional expressions'), fusion: text('bad', 'Lots of conditional expressions'), formik: text('bad', 'Lots of conditional expressions'), finalForm: text('bad', 'Lots of conditional expressions'), schemaForm: 'good', hookForm: text('bad', 'Lots of conditional expressions'), 'formily1.x': 'good', 'formily2.x': 'good', }, { feature: 'Scenario-based packaging capabilities', antd: 'no', fusion: 'no', formik: 'no', finalForm: 'no', schemaForm: 'yes', hookForm: 'no', 'formily1.x': 'yes', 'formily2.x': 'yes', }, { feature: 'Whether to support form preview', antd: 'no', fusion: 'yes', formik: 'no', finalForm: 'no', schemaForm: 'no', hookForm: 'no', 'formily1.x': 'yes', 'formily2.x': 'yes', }, ] export default () => { return (
) } ``` ## Core Advantages - high performance - Out of the box - Linkage logic to achieve high efficiencyv - Cross-terminal capability, logic can be cross-frame, cross-terminal reuse - Dynamic rendering capability ## Core Disadvantage - The learning cost is relatively high. Although 2.x has already converged a large number of concepts, there is still a certain learning cost. ## Who is using it? - Alibaba - Tencent - ByteDance ## Q/A Q: Now that I have Vue, why do I still need to provide @formily/vue? Answer: Vue is a UI framework. The problem it solves is a wider range of UI problems. Although its reactive ability is outstanding in form scenarios, at least it is more convenient than native React to write forms, but if it is in more complex form scenarios , We still need to do a lot of abstraction and encapsulation, so @formily/vue is to help you do these abstract encapsulation things, really let you develop super-complex form applications efficiently and conveniently. Q: What is the biggest advantage of Formily2.x compared to 1.x? Answer: The cost of learning, yes, the core is to allow users to understand Formily more quickly. We have tried our best to avoid all kinds of obscure logic and boundary problems during the 2.x design process. Q: What is the browser compatibility of Formily 2.x? Answer: IE is not supported, because the implementation of Reactive strongly relies on Proxy. ================================================ FILE: docs/guide/index.zh-CN.md ================================================ # 介绍 ## 问题 众所周知,表单场景一直都是前端中后台领域最复杂的场景,它的复杂度主要在哪里呢? - 字段数量多,如何让性能不随字段数量增加而变差? - 字段关联逻辑复杂,如何更简单的实现复杂的联动逻辑?字段与字段关联时,如何保证不影响表单性能? - 一对多(异步) - 多对一(异步) - 多对多(异步) - 表单数据管理复杂 - 表单值转换逻辑复杂(前后端格式不一致) - 同步默认值与异步默认值合并逻辑复杂 - 跨表单数据通信,如何让性能不随字段数量增加而变差? - 表单状态管理复杂 - 着重提自增列表场景,如何让数组数据在移动,删除过程中,字段状态能够做到跟随移动? - 表单的场景化复用 - 查询列表 - 弹窗/抽屉表单 - 分步表单 - 选项卡表单 - 动态渲染诉求很强烈 - 字段配置化,让非专业前端也能快速搭建复杂表单 - 跨端渲染,一份 JSON Schema,多端适配 - 如何在表单协议中描述布局? - 纵向布局 - 横向布局 - 网格布局 - 弹性布局 - 自由布局 - 如何在表单协议中描述逻辑? 这么多问题,怎么解决,想想就头大,但是我们还是得想办法解决,不仅要解决,还要优雅的解决,阿里数字供应链团队,在经历了大量的中后台实践和探索之后,总算沉淀出了 **Formily 表单解决方案** ,以上提到的所有问题,在经历了 UForm 到 Formily1.x,直到 Formily2.x 总算做到了 **优雅解决** 的程度。那 Formily2.x 是如何解决这些问题的呢? ## 解法 为了解决以上问题,我们可以对问题做进一步提炼,得出可突破的方向。 ### 精确渲染 在 React 场景下实现一个表单需求,因为要收集表单数据,实现一些联动需求,大多数都是通过 setState 来实现字段数据收集,这样实现非常简单,心智成本非常低,但是却又引入了性能问题,因为每次输入都会导致所有字段全量渲染,虽然在 DOM 更新层面是有 diff,但是 diff 也是有计算成本的,浪费了很多计算资源,如果用时间复杂度来看的话,初次渲染表单是 O(n),字段输入时也是 O(n),这样明显是不合理的。 历史的经验总是对人类有帮助的,几十年前,人类创造出了 MVVM 设计模式。这样的设计模式核心是将视图模型抽象出来,然后在 DSL 模板层消费,DSL 借助某种依赖收集机制,然后在视图模型中统一调度,保证每次输入都是精确渲染的,这就是工业级的 GUI 形态! 刚好,github 社区为这样的 MVVM 模型抽象出了一个叫 [Mobx](https://github.com/mobxjs/mobx) 的状态管理解决方案,Mobx 最核心的能力就是它的依赖追踪机制和响应式模型的抽象能力。 所以,借助 Mobx,完全可以解决表单字段输入过程中的 O(n)问题,而且是可以很优雅的解决,但是 Formily2.x 在实现的过程中发现 Mobx 还是存在一些不兼容 Formily 核心思想的问题,最终,只能重新造了一个轮子,延续 Mobx 的核心思想的 [@formily/reactive](https://reactive.formilyjs.org/zh-CN) 这里提一下 [react-hook-form](https://github.com/react-hook-form/react-hook-form) ,非常流行,号称业界性能第一的表单方案,我们看看它最简单的案例: ```tsx pure import React from 'react' import ReactDOM from 'react-dom' import { useForm } from 'react-hook-form' function App() { const { register, handleSubmit, errors } = useForm() // initialize the hook const onSubmit = (data) => { console.log(data) } return (
{/* register an input */} {errors.lastname && 'Last name is required.'} {errors.age && 'Please enter number for age.'}
) } ReactDOM.render(, document.getElementById('root')) ``` 虽然值管理做到了精确渲染,但是在触发校验的时候,还是会导致表单全量渲染,因为 errors 状态的更新,是必须要整体受控渲染才能实现同步,这仅仅只是校验会全量渲染,其实还有联动,react-hook-form 要实现联动,同样是需要整体受控渲染才能实现联动。所以,如果要真正实现精确渲染,非 Reactive 不可! ### 领域模型 前面问题中有提到表单的联动是非常复杂的,包含了字段间的各种关系,我们想象一下,大多数表单联动,基本上都是基于某些字段的值引发的联动,但是,实际业务需求可能会比较恶心,不仅要基于某些字段值引发联动,还会基于其他副作用值引发联动,比如应用状态,服务端数据状态,页面 URL,某个字段 UI 组件内部数据,当前字段自身的其他数据状态,某些特殊异步事件等等。用张图来描述: ![image-20210202081316031](//img.alicdn.com/imgextra/i3/O1CN01LWjBSt251w5BtGHW2_!!6000000007467-55-tps-1100-432.svg) 从上图可以看到,想要达成一个联动关系,核心是将字段的某些状态属性与某些数据关联起来,这里的某些数据可以是外界数据,也可以是自身数据,比如字段的显示/隐藏与某些数据的关联,又比如字段的值与某些数据关联,还比如字段的禁用/编辑与某些数据关联,就举了 3 个例子,我们其实已经抽象出了一个最简单的 Field 模型: ```typescript interface Field { value: any visible: boolean disabled: boolean } ``` 当然,Field 模型仅仅只有这 3 个属性吗?肯定不是,如果我们要表达一个字段,那么字段的路径一定要有,因为要描述整个表单树结构,同时,我们还要管理起字段对应 UI 组件的属性,比如 Input 和 Select 都有它的属性,举个例子,Input 的 placeholder 与某些数据关联,或者 Select 的下拉选项与某些数据关联,这样就能理解了吧。所以,我们的 Field 模型可以是这样: ``` interface Field { path:string[], value:any, visible:boolean, disabled:boolean, component:[Component,ComponentProps] } ``` 我们加了 component 属性,它代表了字段所对应的 UI 组件和 UI 组件属性,这样就实现了某些数据与字段组件属性关联,甚至是与字段组件关联的能力。还有吗?当然还有,比如字段的外包裹容器,通常我们都叫 FormItem,它主要负责字段的外围的交互样式,比如字段标题,错误提示的样式等等,如果我们想要囊括更多联动,比如某些数据与 FormItem 的联动,那就得把外包裹容器也加进去。还有很多很多属性,这里没法一一列举。 从上面的思路中我们可以看到,为了解决联动问题,不管我们怎么抽象,最终还是会抽象出字段模型,它包含了字段相关的所有状态,只要去操作这些状态就能引发联动。 关于精确渲染,我们已经确定可以选用类似 Mobx 的 Reactive 方案,虽然是重新造了一个轮子,但是,Reactive 这种模式始终还是很适合抽象响应式模型,所以基于 Reactive 的能力,Formily 经过不断试错与纠正,总算设计出了真正优雅的表单模型。这样的表单模型,解决的是表单领域问题,所以也称之为领域模型,有了这样的领域模型,我们就能让表单的联动变得可枚举可预测,这样也为后面要说的协议描述联动打下了坚实基础。 ### 路径系统 前面提到了表单领域模型中的字段模型,如果设计的更完备的话,其实不止是字段模型,必须还要有一个表单模型作为顶层模型,顶层模型管理着所有字段模型,每个字段都有着自己的路径,那如何查找这些字段呢?前面说到的联动关系,更多的是被动依赖关系,但是有些场景,我们就是要基于某个异步事件动作,去修改某个字段的状态,这里就涉及到如何优雅的查找某个字段,同样也是经过了大量的试错与纠正,Formily 独创的路径系统 @formily/path 很好的解决了这个问题,不仅仅是让字段查找变得优雅,它还能通过解构表达式去处理前后端数据结构不一致的恶心问题。 ### 生命周期 借助 Mobx 和路径系统,我们已经打造了一个较为完备的表单方案了,但是这样抽象了之后,我们的方案就像个黑盒,外界无法感知到方案内部状态流转过程,想要在某个过程阶段内实现一些逻辑则无法实现,所以,这里我们就需要另外一个概念了,生命周期,只要我们将整个表单生命周期作为事件钩子暴露给外界,这样就能做到了既有抽象,但又灵活的表单方案。 ### 协议驱动 如果想要实现动态可配置表单,那必然是需要将表单结构变得可序列化,序列化的方式有很多种,可以是以 UI 为思路的 UI 描述协议,也可以是以数据为思路的数据描述协议,因为表单本身就是为了维护一份数据,那自然而然,对于表单场景而言,数据协议最适合不过,想要描述数据结构,现在业界最流行的就是 [JSON-Schema](https://json-schema.org/) 了,因为 JSON Schema 协议上本身就有很多校验相关的属性,这就天然和表单校验关联上了。那 UI 描述协议就真的不适合描述表单吗?No,UI 描述协议适合更通用的 UI 表达,描述表单当然不在话下,只是它会更偏前端协议,相反,JSON-Schema,在后端模型层,都是可表达的,在描述数据上更通用,所以两种协议,各有所长,只是在单纯表单领域,JSON-Schema 会更偏领域化一些。 那么,如果选用 JSON-Schema,我们怎么描述 UI,怎么描述逻辑呢?单纯的描述数据,想要输出实际业务可用的表单页面,不太现实。 [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form)的解法是,数据是数据,UI 是 UI,这样的好处是,各个协议都是非常纯净的协议,但是却带来了较大的维护成本和理解成本,用户要开发一个表单,需要不断的在两种协议心智上做切换,所以,如果从技术视角来看这样的拆分,其实是非常合理的,但是从产品视角来看的话,拆分则是把成本抛给了用户,所以,Formily 的表单协议会更加倾向于在 JSON-Schema 上做扩展。 那么,如何扩展呢?为了不污染标准 JSON-Schema 属性,我们统一以`x-*`格式来表达扩展属性: ```json { "type": "string", "title": "字符串", "description": "这是一个字符串", "x-component": "Input", "x-component-props": { "placeholder": "请输入" } } ``` 这样看来,UI 协议与数据协议混合在一起,只要有一个统一的扩展约定,也还是能保证两种协议职责单一。 然后,如果想要在某些字段上包裹一个 UI 容器怎么办呢?这里,Formily 定义了一个新的 schema type,叫`void`。void 不陌生,W3C 规范里也有 void element,js 里也有 void 关键字,前者代表虚元素,后者代表虚指针,所以,在 JSON Schema 中,引入 void,代表一个虚数据节点,表示该节点并不占用实际数据结构。所以,我们可以这样: ```json { "type": "void", "title": "卡片", "description": "这是一个卡片", "x-component": "Card", "properties": { "string": { "type": "string", "title": "字符串", "description": "这是一个字符串", "x-component": "Input", "x-component-props": { "placeholder": "请输入" } } } } ``` 这样就可以描述了一个 UI 容器了,因为可以描述 UI 容器,我们就能轻易封装一个场景化的组件了,比如 FormStep,那么我们怎么描述字段间联动呢?比如一个字段要控制另一个字段的显示隐藏。我们可以这样: ```json { "type": "object", "properties": { "source": { "type": "string", "title": "Source", "x-component": "Input", "x-component-props": { "placeholder": "请输入" } }, "target": { "type": "string", "title": "Target", "x-component": "Input", "x-component-props": { "placeholder": "请输入" }, "x-reactions": [ { "dependencies": ["source"], "when": "{{$deps[0] == '123'}}", "fulfill": { "state": { "visible": true } }, "otherwise": { "state": { "visible": false } } } ] } } } ``` 借助`x-reactions`描述了 target 字段,依赖了 source 字段的值,如果值为`'123'`的时候则显示 target 字段,否则隐藏,这种联动方式是一种被动联动,那如果我们希望实现主动联动呢?可以这样: ```json { "type": "object", "properties": { "source": { "type": "string", "title": "Source", "x-component": "Input", "x-component-props": { "placeholder": "请输入" }, "x-reactions": [ { "when": "{{$self.value == '123'}}", "target": "target", "fulfill": { "state": { "visible": true } }, "otherwise": { "state": { "visible": false } } } ] }, "target": { "type": "string", "title": "Target", "x-component": "Input", "x-component-props": { "placeholder": "请输入" } } } } ``` 只需要将`x-reactions`换个位置,放到 source 字段上,然后再指定一个 target 即可。 可以看到,我们的联动,其实核心是基于: - 条件 - 条件满足的动作 - 条件不满足的动作 来实现的,因为内部状态管理借助了 类似 Mobx 的[@formily/reactive](https://reactive.formilyjs.org/zh-CN)方案,所以,Formily 很轻松的就实现了被动和主动联动场景,覆盖了绝大多数业务需求。 所以,我们的表单完全可以使用协议来描述了,不管是再复杂的布局,还是很复杂的联动,都能做到可配置。 ### 分层架构 前面讲了对于一开始的各种问题的解法,那么现在我们如何设计才能让 Formily 更加自洽且优雅呢? ![](https://img.alicdn.com/imgextra/i3/O1CN01iEwHrP1NUw84xTded_!!6000000001574-55-tps-1939-1199.svg) 这张图主要将 Formily 分为了内核层,UI 桥接层,扩展组件层,和配置应用层。 内核层是 UI 无关的,它保证了用户管理的逻辑和状态是不耦合任何一个框架,这样有几个好处: - 逻辑与 UI 框架解耦,未来做框架级别的迁移,业务代码无需大范围重构 - 学习成本统一,如果用户使用了@formily/react,以后业务迁移@formily/vue,用户不需要重新学习 JSON Schema 独立存在,给 UI 桥接层消费,保证了协议驱动在不同 UI 框架下的绝对一致性,不需要重复实现协议解析逻辑。 扩展组件层,提供一系列表单场景化组件,保证用户开箱即用。无需花大量时间做二次开发。 ## 竞品对比 ```tsx /** * inline: true */ import React from 'react' import { Table, Tooltip } from 'antd' import { QuestionCircleOutlined } from '@ant-design/icons' const text = (content, tooltips) => { if (tooltips) { return (
{content}
) } return content } const dataSource = [ { feature: '自定义组件接入成本', antd: '4.x接入成本低', fusion: '高', formik: '低', finalForm: '低', schemaForm: text('高', '因为耦合bootstrap'), hookForm: text('高', '因为耦合React Ref'), 'formily1.x': '低', 'formily2.x': '低', }, { feature: '性能', antd: text('4.x性能较好', '只解决了值同步精确渲染'), fusion: '差', formik: '差', finalForm: text('较好', '但只解决了值同步精确渲染'), schemaForm: '差', hookForm: text('好', '但只解决了值同步精确渲染'), 'formily1.x': text('非常好', '能解决联动过程中的精确渲染'), 'formily2.x': text('非常好', '能解决联动过程中的精确渲染'), }, { feature: '是否支持动态渲染', antd: '否', fusion: '否', formik: '否', finalForm: '否', schemaForm: '是', hookForm: '否', 'formily1.x': '是', 'formily2.x': '是', }, { feature: '是否开箱即用', antd: '是', fusion: '是', formik: '否', finalForm: '否', schemaForm: '是', hookForm: '否', 'formily1.x': '是', 'formily2.x': '是', }, { feature: '是否支持跨端', antd: '否', fusion: '否', formik: '否', finalForm: '否', schemaForm: '否', hookForm: '否', 'formily1.x': '是', 'formily2.x': '是', }, { feature: '开发效率', antd: '一般', fusion: '一般', formik: '一般', finalForm: '一般', schemaForm: text('低', '源码开发需要手工维护JSON'), hookForm: '一般', 'formily1.x': '高', 'formily2.x': '高', }, { feature: '学习成本', antd: '低', fusion: '低', formik: '低', finalForm: '高', schemaForm: '高', hookForm: '低', 'formily1.x': '很高', 'formily2.x': text('高', '概念大量减少'), }, { feature: '视图代码可维护性', antd: text('低', '大量条件表达式'), fusion: text('低', '大量条件表达式'), formik: text('低', '大量条件表达式'), finalForm: text('低', '大量条件表达式'), schemaForm: '高', hookForm: text('低', '大量条件表达式'), 'formily1.x': '高', 'formily2.x': '高', }, { feature: '场景化封装能力', antd: '无', fusion: '无', formik: '无', finalForm: '无', schemaForm: '有', hookForm: '无', 'formily1.x': '有', 'formily2.x': '有', }, { feature: '是否支持表单预览态', antd: '否', fusion: '是', formik: '否', finalForm: '否', schemaForm: '否', hookForm: '否', 'formily1.x': '是', 'formily2.x': '是', }, ] export default () => { return (
) } ``` ## 核心优势 - 高性能 - 开箱即用 - 联动逻辑实现高效 - 跨端能力,逻辑可跨框架,跨终端复用 - 动态渲染能力 ## 核心劣势 - 学习成本较高,虽然 2.x 已经在大量收敛概念,但还是存在一定的学习成本。 ## 谁在使用? - 阿里巴巴 - 数字供应链事业部 - 淘系技术部 - 飞猪 - 阿里云 - 蚂蚁 - 政务平台 - 大文娱 - 盒马 - 阿里妈妈 - 数据平台 - ICBU - 口碑 - 钉钉 - 天猫超市、天猫国际、阿里健康、农村淘宝、淘宝心选 - 腾讯 - 字节跳动 ## Q/A 问:有了 Vue 了,为什么还需要提供@formily/vue? 答:Vue 是一个 UI 框架,它解决的问题是更大范围的 UI 问题,虽然它的 reactive 能力在表单场景上表现出众,至少比原生 React 写表单要方便,但是如果在更复杂的表单场景上,我们还是需要做很多抽象和封装,所以@formily/vue 就是为了帮您做这些抽象封装的事情,真正让您高效便捷的开发出超复杂表单应用。 问:Formily2.x 相比于 1.x 最大的优势是什么? 答:学习成本的大大降低,对,核心是为了让用户更快速的理解 Formily,我们在 2.x 设计的过程中极力的避免出现各种隐晦逻辑,边界问题,同时因为移除了 rxjs/styled-components 的依赖,整体体积大大降低 问:Formily2.x 的浏览器兼容性如何? 答:不支持 IE,因为 Reactive 的实现强依赖 Proxy ================================================ FILE: docs/guide/issue-helper.md ================================================ # Issue Helper ## Before You Start... The issue list is reserved exclusively for bug reports and feature requests. That means we do not accept usage questions. If you open an issue that does not conform to the requirements, it will be closed immediately. For usage questions, please use the following resources: - Read the introduce and components documentation - Make sure you have search your question in FAQ and changelog - Look for / ask questions on [Discussions](https://github.com/alibaba/formily/discussions) Also try to search for your issue it may have already been answered or even fixed in the development branch. However, if you find that an old, closed issue still persists in the latest version, you should open a new issue using the form below instead of commenting on the old issue. ```tsx import React from 'react' import { createForm, onFieldMount, onFieldReact } from '@formily/core' import { Field, VoidField } from '@formily/react' import { Form, Input, Select, Radio, FormItem, FormButtonGroup, Submit, } from '@formily/antd' import semver from 'semver' import ReactMde from 'react-mde' import * as Showdown from 'showdown' import 'react-mde/lib/styles/css/react-mde-all.css' const converter = new Showdown.Converter({ tables: true, simplifiedAutoLink: true, strikethrough: true, tasklists: true, }) const MdInput = ({ value, onChange }) => { const [selectedTab, setSelectedTab] = React.useState('write') return (
Promise.resolve( `
${ converter.makeHtml(markdown) || '' }
` ) } />
) } const form = createForm({ validateFirst: true, effects() { onFieldMount('version', async (field) => { const { versions: unsort } = await fetch( 'https://registry.npmmirror.com/@formily/core' ).then((res) => res.json()) const versions = Object.keys(unsort).sort((v1, v2) => semver.gte(v1, v2) ? -1 : 1 ) field.dataSource = versions.map((version) => ({ label: version, value: version, })) }) onFieldMount('package', async (field) => { const packages = await fetch( 'https://formilyjs.org/.netlify/functions/npm-search?q=@formily' ).then((res) => res.json()) field.dataSource = packages.map(({ name }) => { return { label: name, value: name, } }) }) onFieldReact('bug-desc', (field) => { field.visible = field.query('type').value() === 'Bug Report' }) onFieldReact('feature-desc', (field) => { field.visible = field.query('type').value() === 'Feature Request' }) }, }) const createIssueURL = ({ type, title, version, package: pkg, reproduceLink, reproduceStep, expected, actually, comment, feature, api, }) => { const url = new URL('https://github.com/alibaba/formily/issues/new') const bugInfo = ` - [ ] I have searched the [issues](https://github.com/alibaba/formily/issues) of this repository and believe that this is not a duplicate. ### Reproduction link [![Edit on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](${ reproduceLink || '' }) ### Steps to reproduce ${reproduceStep || ''} ### What is expected? ${expected || ''} ### What is actually happening? ${actually || ''} ### Package ${pkg}@${version} --- ${comment || ''} ` const prInfo = ` - [ ] I have searched the [issues](https://github.com/alibaba/formily/issues) of this repository and believe that this is not a duplicate. ### What problem does this feature solve? ${feature || ''} ### What does the proposed API look like? ${api || ''} ` url.searchParams.set('title', `[${type}] ${title}`) url.searchParams.set('body', type === 'Bug Report' ? bugInfo : prInfo) return url.href } export default () => { return (
{ return /\/\/(codesandbox\.io|github)/.test(value) ? '' : 'Must Be Codesandbox Link or Github Repo' }, ]} description={
This is Codesandbox templates.If you are:
} />

Explain your use case, context, and rationale behind this feature request. More importantly, what is the end user experience you are trying to build that led to the need for this feature?

An important design goal of Formily is keeping the API surface small and straightforward. In general, we only consider adding new features that solve a problem that cannot be easily dealt with using existing APIs (i.e. not just an alternative way of doing things that can already be done). The problem should also be common enough to justify the addition.

} name="feature" required decorator={[FormItem]} component={[MdInput]} />
{ window.open(createIssueURL(values)) }} > Submit ) } ``` ================================================ FILE: docs/guide/issue-helper.zh-CN.md ================================================ # 问题反馈 ## 在你开始之前... Issue List 专用于跟踪错误报告和功能请求。 这意味着我们不接受使用相关的问题。 如果您创建了不符合要求的 Issue,它将立即被关闭。 如果您面临的是使用相关的问题,您可以这样: - 先阅读介绍和组件文档 - 确保您已在 FAQ 和 changelog 中搜索了您的问题 - 在[Discussions](https://github.com/alibaba/formily/discussions)中查找/询问问题 试着先尝试搜索您的问题 它可能已经在开发分支中得到了解决,甚至已经解决。 但是,如果发现旧的,已关闭的问题仍保留在最新版本中,则应使用下面的表单打开一个新的问题,而不是对旧问题进行评论。 ```tsx import React from 'react' import { createForm, onFieldMount, onFieldReact } from '@formily/core' import { Field, VoidField } from '@formily/react' import { Form, Input, Select, Radio, FormItem, FormButtonGroup, Submit, } from '@formily/antd' import semver from 'semver' import ReactMde from 'react-mde' import * as Showdown from 'showdown' import 'react-mde/lib/styles/css/react-mde-all.css' const converter = new Showdown.Converter({ tables: true, simplifiedAutoLink: true, strikethrough: true, tasklists: true, }) const MdInput = ({ value, onChange }) => { const [selectedTab, setSelectedTab] = React.useState('write') return (
Promise.resolve( `
${ converter.makeHtml(markdown) || '' }
` ) } />
) } const form = createForm({ validateFirst: true, effects() { onFieldMount('version', async (field) => { const { versions: unsort } = await fetch( 'https://registry.npmmirror.com/@formily/core' ).then((res) => res.json()) const versions = Object.keys(unsort).sort((v1, v2) => semver.gte(v1, v2) ? -1 : 1 ) field.dataSource = versions.map((version) => ({ label: version, value: version, })) }) onFieldMount('package', async (field) => { const packages = await fetch( 'https://formilyjs.org/.netlify/functions/npm-search?q=@formily' ).then((res) => res.json()) field.dataSource = packages.map(({ name }) => { return { label: name, value: name, } }) }) onFieldReact('bug-desc', (field) => { field.visible = field.query('type').value() === 'Bug Report' }) onFieldReact('feature-desc', (field) => { field.visible = field.query('type').value() === 'Feature Request' }) }, }) const createIssueURL = ({ type, title, version, package: pkg, reproduceLink, reproduceStep, expected, actually, comment, feature, api, }) => { const url = new URL('https://github.com/alibaba/formily/issues/new') const bugInfo = ` - [ ] I have searched the [issues](https://github.com/alibaba/formily/issues) of this repository and believe that this is not a duplicate. ### Reproduction link [![Edit on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](${ reproduceLink || '' }) ### Steps to reproduce ${reproduceStep || ''} ### What is expected? ${expected || ''} ### What is actually happening? ${actually || ''} ### Package ${pkg}@${version} --- ${comment || ''} ` const prInfo = ` - [ ] I have searched the [issues](https://github.com/alibaba/formily/issues) of this repository and believe that this is not a duplicate. ### What problem does this feature solve? ${feature || ''} ### What does the proposed API look like? ${api || ''} ` url.searchParams.set('title', `[${type}] ${title}`) url.searchParams.set('body', type === 'Bug Report' ? bugInfo : prInfo) return url.href } export default () => { return (
{ return /\/\/(codesandbox\.io|github)/.test(value) ? '' : '必须是 Codesandbox 链接或者 Github 仓库地址' }, ]} description={
This is Codesandbox templates.If you are:
} />

请尽可能详尽地说明这个需求的用例和场景。最重要的是:解释清楚是怎样的用户体验需求催生了这个功能上的需求。

Formily 的一个重要设计原则是保持 API 的简洁和直接。通常来说,我们只考虑添加在现有的 API 下无法轻松实现的功能。新功能的用例也应当足够常见。

} name="feature" required decorator={[FormItem]} component={[MdInput]} />
{ window.open(createIssueURL(values)) }} > 提交 ) } ``` ================================================ FILE: docs/guide/learn-formily.md ================================================ # How to learn Formily ## Study Suggestion To describe Formily in one sentence, it is an MVVM form solution that abstracts the form domain model. Therefore, if you want to use Formily in depth, you must learn and understand what Formily's domain model is like and what problems does it solve. After understanding the domain model, it is actually how to consume the view layer of this domain model. This layer only needs to look at the documentation of the specific components. ## About the documentation Because Formily’s learning costs are still relatively high, if you want to quickly understand the full picture of Formily, the most important thing is to read the documentation. It's just how to look at the document and where it will be more important. Below we give different document learning routes for different users. ### Entry-level user - Introduction, because you need to understand Formily's core ideas and whether it is suitable for your business scenario. - Quick start, learn how to use Formily in practice from the simplest example. - Component documentation/core library documentation, because Formily has already encapsulated most of the out-of-the-box components for you. If you encounter component-related problems, you can just check the component documentation just like looking up a dictionary. - Scenario case, starting from the specific scenario, see what is the best practice in this scenario. ### Advanced users - Digest the core concepts carefully and have a deeper understanding of Formily. - Advanced guide, mainly to learn more advanced usage methods, such as custom components, from simple custom components to super complex custom components. - Read component documents/core library documents at any time to deepen memory - For the details and best practices of custom component development, it is recommended to look directly at the source code of @formily/antd or @formily/next, because this is the boilerplate code and is closely related to the actual business scenario. ### Source code co-builder - Contribution guide, understand the most basic contribution posture. - Read the document, if you find that the document is defective, you can submit a PR to fix it. - Read the unit test to understand the implementation details corresponding to each test case. If you find that there are missing test cases, you can submit a PR. - Read the source code, if you find a bug in the source code, you can raise a PR. Pay attention to modify the source code, you must bring unit tests ## About the question If you encounter problems during the development process, it is recommended to use the search function at the top of the document to quickly search for the content of the document and solve it quickly. If you can’t find it, I recommend you to ask questions in the [forum](https://github.com/alibaba/formily/discussions). It is convenient to record. If you encounter a very urgent problem, you can help solve it in the Dingding group @白玄. **It is not recommended to ask various basic questions directly without reading the document, which is very inefficient** ## About the bug If you find behaviors that do not meet expectations during the development process and can be reproduced in the smallest case, you can submit an [issue](https://github.com/alibaba/formily/issues) to Formily It is strongly not recommended to record the problem in the issue, which will disrupt the information flow of Issue. At the same time, **be sure to bring the smallest reproducible link address when mentioning Issue**, so that developers can quickly locate the problem and fix it quickly, instead of Find bugs in a bunch of codes. ## About Feature Request If during the development process you find that some of Formily's designs are not good, or can be improved better, you can submit your own ideas in the [forum](https://github.com/alibaba/formily/discussions) ================================================ FILE: docs/guide/learn-formily.zh-CN.md ================================================ # 如何学习 Formily ## 学习建议 Formily 用一句话来描述,它就是一个抽象了表单领域模型的 MVVM 表单解决方案,所以,如果你想深入使用 Formily,那必须学习并了解 Formily 的领域模型到底是咋样的,它到底解决了哪些问题,了解完领域模型之后,其实就是如何消费这个领域模型的视图层了,这一层就只需要看具体组件的文档即可了。 ## 关于文档 因为 Formily 的学习成本还是比较高的,想要快速了解 Formily 的全貌,最重要的还是看文档,只是文档怎么看,从哪里看会比较重要,下面我们针对不同用户给出了不同的文档学习路线。 ### 入门级用户 - 引言介绍,因为你要了解 Formily 的核心思路,是否适合你的业务场景。 - 快速开始,从最简单的例子学习实际 Formily 使用都是怎么使用的。 - 组件文档/核心库文档,因为 Formily 为你已经封装好了大多数开箱即用的组件,遇到组件相关的问题,就像查字典一样的去查看组件文档即可。 - 场景案例,从具体的场景出发,看看什么才是这个场景下的最佳实践。 ### 进阶级用户 - 仔细消化核心概念,更深入的理解 Formily - 进阶指南,主要学习更高级的使用方式,比如自定义组件,从简单自定义组件到超复杂自定义组件 - 随时查阅组件文档/核心库文档,加深记忆 - 对于自定义组件开发上的细节问题,最佳实践,推荐直接看@formily/antd 或者@formily/next 的源码,因为这就是样板代码,跟实际业务场景息息相关。 ### 源码共建者 - 贡献指南,了解最基本的贡献姿势 - 阅读文档,如果发现文档有缺陷,可以提 PR 修复 - 阅读单元测试,了解每个测试用例所对应的实现细节,如果发现有遗漏测试用例,可以提 PR - 阅读源码,如果发现源码有 Bug,可以提 PR 注意修改源码,必须要带上单元测试 ## 关于提问 如果在开发的过程中遇到问题,推荐使用文档上方的搜索功能快速搜索文档内容,快速解决,如果搜索不到的,推荐到 [论坛](https://github.com/alibaba/formily/discussions) 中提问,这里方便记录,如果遇到非常紧急的问题,可以在钉钉群里 @白玄 帮忙解决。**非常不推荐文档都不看,就直接问各种基础问题,这样很低效** ## 关于 Bug 如果在开发过程中发现不符合预期的行为,并能够以最小案例复现的,可以给 Formily 提[Issue](https://github.com/alibaba/formily/issues) ,非常不推荐将问题记录在 issue 里,会打乱 Issue 的信息流,同时一定注意,**提 Issue 的时候要带上最小可复现的链接地址**,方便开发者快速定位问题,快速修复,而不是在一堆代码里找 Bug。 ## 关于 Feature Request 如果在开发过程中发现 Formily 的某些设计很不好,或者可以改进的更好的,则可以在 [论坛](https://github.com/alibaba/formily/discussions) 中提交自己的想法。 ================================================ FILE: docs/guide/quick-start.md ================================================ # Quick Start ## Install Dependencies ### Install the Core Library To use Formily, you must use [@formily/core](https://core.formilyjs.org), which is responsible for managing the status of the form, form verification, linkage, and so on. ```bash $ npm install --save @formily/core ``` ### Install UI Bridge Library The kernel alone is not enough. We also need a UI library to access kernel data to achieve the final form interaction effect. For users of different frameworks, we have different bridge libraries. **React users** ```bash $ npm install --save @formily/react ``` **Vue users** ```bash $ npm install --save @formily/vue ``` ### Install component library To quickly implement beautiful forms, we usually need to use industry-leading component libraries, such as [Ant Design](https://ant.design) and [Alibaba Fusion](https://fusion.design). However, these excellent component libraries are not fully covered in some scenes of the form. For example, the detailed preview state is not supported by Ant Design, and some scene-based components are not supported, so Formily is in On top of this, @formily/antd and @formily/next are encapsulated to ensure that users can use it out of the box. **Ant Design users** ```bash $ npm install --save antd moment @formily/antd ``` **Alibaba Fusion users** ```bash $ npm install --save @alifd/next moment @formily/next ``` ## Import Dependencies Use ES Module import syntax to import dependencies ```ts import React from 'react' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { FormItem, Input } from '@formily/antd' ``` ## Example ```tsx /** * defaultShowCode: true */ import React from 'react' import { createForm } from '@formily/core' import { FormProvider, FormConsumer, Field } from '@formily/react' import { FormItem, FormLayout, Input, FormButtonGroup, Submit, } from '@formily/antd' const form = createForm() export default () => { return ( {() => (
Real-time response:{form.values.input}
)}
submit
) } ``` From the above examples, we can learn a lot: - [createForm](https://core.formilyjs.org/api/entry/create-form) is used to create the core domain model of the form, which is the standard ViewModel as the [MVVM](https://core.formilyjs.org/guide/mvvm) design pattern. - The [FormProvider](https://react.formilyjs.org/api/components/form-provider) component is used as the entrance to the view layer bridge form model. It has only one parameter, which is to receive the Form instance created by createForm and pass the Form instance to the child component in the form of context. - The [FormLayout](https://antd.formilyjs.org/components/form-layout) component is a component used to control the style of [FormItem](https://antd.formilyjs.org/components/form-item) in batches. Here we specify the layout as top and bottom layout, that is, the label is on the top and the component is on the bottom. - The [Field](https://react.formilyjs.org/api/components/field) component is a component used to undertake common fields. - The name attribute identifies the path of the field in the final submitted data of the form. - Title attribute, which identifies the title of the field - If the decorator is specified as FormItem, then the title attribute will be received as the label by default in the FormItem component. - If specified as a custom component, the consumer of the title will be taken over by the custom component. - If decorator is not specified, then the title will not be displayed on the UI. - Required attribute, a shorthand for required verification, which identifies that the field is required - If the decorator is specified as FormItem, then an asterisk prompt will automatically appear, and there will be corresponding status feedback if the verification fails. These are the default processing done inside the FormItem. - If the decorator is specified as a custom component, the corresponding UI style needs to be implemented by the custom component implementer. - If decorator is not specified, then required will just block submission, and there will be no UI feedback for verification failure. - InitialValue property, which represents the default value of the field - Decorator attribute, representing the UI decorator of the field, usually we will specify it as FormItem - Note that the decorator attribute is passed in the form of an array, the first parameter represents the specified component type, and the second parameter represents the specified component attribute. - The component attribute, which represents the input control of the field, can be Input or Select, etc. - Note that the component property is passed in the form of an array, the first parameter represents the specified component type, and the second parameter represents the specified component property. - The [FormConsumer](https://react.formilyjs.org/api/components/form-consumer) component exists as a responder of a responsive model. Its core is a render props mode. In the callback function as children, all dependencies are automatically collected. If the dependencies change, it will be re-rendered. With the help of FormConsumer, we can Conveniently realize the needs of various calculations and summaries. - The [FormButtonGroup](https://antd.formilyjs.org/components/form-button-group) component exists as a form button group container and is mainly responsible for the layout of the buttons. - The [Submit](https://antd.formilyjs.org/components/submit) component exists as an action trigger for form submission. In fact, we can also directly use the form.submit method to submit. But the advantage of using Submit is that there is no need to write the onClick event handler on the Button component every time, and it also handles the loading state of the Form. If the onSubmit method returns a Promise and the Promise is pending, the button will automatically enter the loading state. ================================================ FILE: docs/guide/quick-start.zh-CN.md ================================================ # 快速开始 ## 安装依赖 ### 安装内核库 使用 Formily 必须要用到[@formily/core](https://core.formilyjs.org/zh-CN),它负责管理表单的状态,表单校验,联动等等。 ```bash $ npm install --save @formily/core ``` ### 安装 UI 桥接库 单纯有了内核还不够,我们还需要一个 UI 库来接入内核数据,用来实现最终的表单交互效果,对于不同框架的用户,我们有不同的桥接库。 **React 用户** ```bash $ npm install --save @formily/react ``` **Vue 用户** ```bash $ npm install --save @formily/vue ``` ### 安装组件库 想要快速实现漂亮的表单,通常我们都是需要使用业界优秀的组件库的,比如[Ant Design ](https://ant.design)和 [Alibaba Fusion](https://fusion.design),但是这些优秀的组件库,在表单的某些场景上覆盖的还是不够全面,比如详情预览态的支持,Ant Design 是不支持的,还有一些场景化的组件它也是不支持的,所以 Formily 在此之上又封装了@formily/antd 和@formily/next,保证用户开箱即用。 **Ant Design 用户** ```bash $ npm install --save antd moment @formily/antd ``` **Alibaba Fusion 用户** ```bash $ npm install --save @alifd/next moment @formily/next ``` ## 导入依赖 使用 ES Module import 语法导入依赖即可 ```ts import React from 'react' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { FormItem, Input } from '@formily/antd' ``` ## 具体用例 ```tsx /** * defaultShowCode: true */ import React from 'react' import { createForm } from '@formily/core' import { FormProvider, FormConsumer, Field } from '@formily/react' import { FormItem, FormLayout, Input, FormButtonGroup, Submit, } from '@formily/antd' const form = createForm() export default () => { return ( {() => (
实时响应:{form.values.input}
)}
提交
) } ``` 从以上例子中,我们可以学到很多东西: - [createForm](https://core.formilyjs.org/zh-CN/api/entry/create-form)用来创建表单核心领域模型,它是作为[MVVM](https://core.formilyjs.org/guide/mvvm)设计模式的标准 ViewModel - [FormProvider](https://react.formilyjs.org/zh-CN/api/components/form-provider)组件是作为视图层桥接表单模型的入口,它只有一个参数,就是接收 createForm 创建出来的 Form 实例,并将 Form 实例以上下文形式传递到子组件中 - [FormLayout](https://antd.formilyjs.org/zh-CN/components/form-layout)组件是用来批量控制[FormItem](https://antd.formilyjs.org/zh-CN/components/form-item)样式的组件,这里我们指定布局为上下布局,也就是标签在上,组件在下 - [Field](https://react.formilyjs.org/zh-CN/api/components/field)组件是用来承接普通字段的组件 - name 属性,标识字段在表单最终提交数据中的路径 - title 属性,标识字段的标题 - 如果 decorator 指定为 FormItem,那么在 FormItem 组件中会默认以接收 title 属性作为标签 - 如果指定为某个自定义组件,那么 title 的消费方则由自定义组件来承接 - 如果不指定 decorator,那么 title 则不会显示在 UI 上 - required 属性,必填校验的极简写法,标识该字段必填 - 如果 decorator 指定为 FormItem,那么会自动出现星号提示,同时校验失败也会有对应的状态反馈,这些都是 FormItem 内部做的默认处理 - 如果 decorator 指定为自定义组件,那么对应的 UI 样式则需要自定义组件实现方自己实现 - 如果不指定 decorator,那么 required 只是会阻塞提交,校验失败不会有任何 UI 反馈。 - initialValue 属性,代表字段的默认值 - decorator 属性,代表字段的 UI 装饰器,通常我们都会指定为 FormItem - 注意 decorator 属性传递的是数组形式,第一个参数代表指定组件类型,第二个参数代表指定组件属性 - component 属性,代表字段的输入控件,可以是 Input,也可以是 Select,等等 - 注意 component 属性传递的是数组形式,第一个参数代表指定组件类型,第二个参数代表指定组件属性 - [FormConsumer](https://react.formilyjs.org/zh-CN/api/components/form-consumer)组件是作为响应式模型的响应器而存在,它核心是一个 render props 模式,在作为 children 的回调函数中,会自动收集所有依赖,如果依赖发生变化,则会重新渲染,借助 FormConsumer 我们可以很方便的实现各种计算汇总的需求 - [FormButtonGroup](https://antd.formilyjs.org/zh-CN/components/form-button-group)组件作为表单按钮组容器而存在,主要负责按钮的布局 - [Submit](https://antd.formilyjs.org/zh-CN/components/submit)组件作为表单提交的动作触发器而存在,其实我们也可以直接使用 form.submit 方法进行提交,但是使用 Submit 的好处是不需要每次都在 Button 组件上写 onClick 事件处理器,同时它还处理了 Form 的 loading 状态,如果 onSubmit 方法返回一个 Promise,且 Promise 正在 pending 状态,那么按钮会自动进入 loading 状态 ================================================ FILE: docs/guide/scenes/VerifyCode.tsx ================================================ import React, { useState } from 'react' import { Input, Button } from 'antd' interface IVerifyCodeProps { value?: any onChange?: (value: any) => void readyPost?: boolean phoneNumber?: number style?: React.CSSProperties } export const VerifyCode: React.FC> = ({ value, onChange, readyPost, phoneNumber, ...props }) => { const [lastTime, setLastTime] = useState(0) const counting = (time = 20) => { if (time < 0) return setLastTime(time) setTimeout(() => { counting(time - 1) }, 1000) } return (
{lastTime === 0 && ( )} {lastTime > 0 && 剩余{lastTime}秒}
) } ================================================ FILE: docs/guide/scenes/dialog-drawer.md ================================================ # Dialog and Drawers Mainly use the [FormDialog](https://antd.formilyjs.org/components/form-dialog) function and [FormDrawer]() function in [@formily/antd](https://antd.formilyjs.org) or [@formily/next](https://fusion.formilyjs.org) ================================================ FILE: docs/guide/scenes/dialog-drawer.zh-CN.md ================================================ # 弹窗与抽屉 主要使用[@formily/antd](https://antd.formilyjs.org/zh-CN) 或 [@formily/next](https://fusion.formilyjs.org/zh-CN) 中的[FormDialog](https://antd.formilyjs.org/zh-CN/components/form-dialog)函数 和 [FormDrawer](https://antd.formilyjs.org/zh-CN/components/form-drawer)函数 ================================================ FILE: docs/guide/scenes/edit-detail.md ================================================ # Edit Details ## Edit #### Markup Schema Cases ```tsx import React, { useState, useEffect } from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, FormLayout, Input, Select, Cascader, DatePicker, Submit, FormGrid, Upload, ArrayItems, Editable, FormButtonGroup, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button, Spin } from 'antd' import { UploadOutlined } from '@ant-design/icons' const form = createForm({ validateFirst: true, }) const IDUpload = (props) => { return ( ) } const SchemaField = createSchemaField({ components: { FormItem, FormGrid, FormLayout, Input, DatePicker, Cascader, Select, IDUpload, ArrayItems, Editable, }, scope: { fetchAddress: (field) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }, }, }) export default () => { const [loading, setLoading] = useState(true) useEffect(() => { setTimeout(() => { form.setInitialValues({ username: 'Aston Martin', firstName: 'Aston', lastName: 'Martin', email: 'aston_martin@aston.com', gender: 1, birthday: '1836-01-03', address: ['110000', '110000', '110101'], idCard: [ { name: 'this is image', thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', uid: 'rc-upload-1615825692847-2', url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', }, ], contacts: [ { name: 'Zhang San', phone: '13245633378', email: 'zhangsan@gmail.com', }, { name: 'Li Si', phone: '16873452678', email: 'lisi@gmail.com' }, ], }) setLoading(false) }, 2000) }, []) return (
Submit
) } ``` #### JSON Schema Cases ```tsx import React, { useState, useEffect } from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, FormLayout, Input, Select, Cascader, DatePicker, Submit, FormGrid, Upload, ArrayItems, Editable, FormButtonGroup, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button, Spin } from 'antd' import { UploadOutlined } from '@ant-design/icons' const form = createForm({ validateFirst: true, }) const IDUpload = (props) => { return ( ) } const SchemaField = createSchemaField({ components: { FormItem, FormGrid, FormLayout, Input, DatePicker, Cascader, Select, IDUpload, ArrayItems, Editable, }, scope: { fetchAddress: (field) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }, }, }) const schema = { type: 'object', properties: { username: { type: 'string', title: 'Username', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, name: { type: 'void', title: 'Name', 'x-decorator': 'FormItem', 'x-decorator-props': { asterisk: true, feedbackLayout: 'none', }, 'x-component': 'FormGrid', properties: { firstName: { type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { placeholder: 'firstName', }, }, lastName: { type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { placeholder: 'lastname', }, }, }, }, email: { type: 'string', title: 'Email', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': 'email', }, gender: { type: 'string', title: 'Gender', enum: [ { label: 'male', value: 1, }, { label: 'female', value: 2, }, { label: 'third gender', value: 3, }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', }, birthday: { type: 'string', required: true, title: 'Birthday', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', }, address: { type: 'string', required: true, title: 'Address', 'x-decorator': 'FormItem', 'x-component': 'Cascader', 'x-reactions': '{{fetchAddress}}', }, idCard: { type: 'string', required: true, title: 'ID', 'x-decorator': 'FormItem', 'x-component': 'IDUpload', }, contacts: { type: 'array', required: true, title: 'Contacts', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems', items: { type: 'object', 'x-component': 'ArrayItems.Item', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, popover: { type: 'void', title: 'Contact Informations', 'x-decorator': 'Editable.Popover', 'x-component': 'FormLayout', 'x-component-props': { layout: 'vertical', }, 'x-reactions': [ { fulfill: { schema: { title: '{{$self.query(".name").value() }}', }, }, }, ], properties: { name: { type: 'string', title: 'Name', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { style: { width: 300, }, }, }, email: { type: 'string', title: 'Email', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': [{ required: true }, 'email'], 'x-component-props': { style: { width: 300, }, }, }, phone: { type: 'string', title: 'Phone Number', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': [{ required: true }, 'phone'], 'x-component-props': { style: { width: 300, }, }, }, }, }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, properties: { addition: { type: 'void', title: 'Add Contact', 'x-component': 'ArrayItems.Addition', }, }, }, }, } export default () => { const [loading, setLoading] = useState(true) useEffect(() => { setTimeout(() => { form.setInitialValues({ username: 'Aston Martin', firstName: 'Aston', lastName: 'Martin', email: 'aston_martin@aston.com', gender: 1, birthday: '1836-01-03', address: ['110000', '110000', '110101'], idCard: [ { name: 'this is image', thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', uid: 'rc-upload-1615825692847-2', url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', }, ], contacts: [ { name: 'Zhang San', phone: '13245633378', email: 'zhangsan@gmail.com', }, { name: 'Li Si', phone: '16873452678', email: 'lisi@gmail.com' }, ], }) setLoading(false) }, 2000) }, []) return (
Submit
) } ``` #### Pure JSX Cases ```tsx import React, { useState, useEffect } from 'react' import { createForm } from '@formily/core' import { Field, VoidField, ArrayField } from '@formily/react' import { Form, FormItem, FormLayout, Input, Select, Cascader, DatePicker, Submit, FormGrid, Upload, ArrayBase, Editable, FormButtonGroup, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button, Spin } from 'antd' import { UploadOutlined } from '@ant-design/icons' import './index.less' const form = createForm({ validateFirst: true, }) const IDUpload = (props) => { return ( ) } const fetchAddress = (field) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) } export default () => { const [loading, setLoading] = useState(true) useEffect(() => { setTimeout(() => { form.setInitialValues({ username: 'Aston Martin', firstName: 'Aston', lastName: 'Martin', email: 'aston_martin@aston.com', gender: 1, birthday: '1836-01-03', address: ['110000', '110000', '110101'], idCard: [ { name: 'this is image', thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', uid: 'rc-upload-1615825692847-2', url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', }, ], contacts: [ { name: 'Zhang San', phone: '13245633378', email: 'zhangsan@gmail.com', }, { name: 'Li Si', phone: '16873452678', email: 'lisi@gmail.com' }, ], }) setLoading(false) }, 2000) }, []) return (
{(field) => ( {field.value?.map((item, index) => (
{ field.title = field.query('.[].name').value() || field.title }} >
))}
)}
Submit
) } ``` ## Details #### Markup Schema Cases ```tsx import React, { useState, useEffect } from 'react' import { createForm } from '@formily/core' import { createSchemaField, useField } from '@formily/react' import { Form, FormItem, FormLayout, Input, Select, Cascader, DatePicker, FormGrid, Upload, ArrayItems, Editable, PreviewText, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button, Spin } from 'antd' import { UploadOutlined } from '@ant-design/icons' const form = createForm({ readPretty: true, validateFirst: true, }) const IDUpload = (props) => { const field = useField() return ( {field.editable && ( )} ) } const SchemaField = createSchemaField({ components: { FormItem, FormGrid, FormLayout, Input, DatePicker, Cascader, Select, IDUpload, ArrayItems, Editable, }, scope: { fetchAddress: (field) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }, }, }) export default () => { const [loading, setLoading] = useState(true) useEffect(() => { setTimeout(() => { form.setInitialValues({ username: 'Aston Martin', firstName: 'Aston', lastName: 'Martin', email: 'aston_martin@aston.com', gender: 1, birthday: '1836-01-03', address: ['110000', '110000', '110101'], idCard: [ { name: 'this is image', thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', uid: 'rc-upload-1615825692847-2', url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', }, ], contacts: [ { name: 'Zhang San', phone: '13245633378', email: 'zhangsan@gmail.com', }, { name: 'Li Si', phone: '16873452678', email: 'lisi@gmail.com' }, ], }) setLoading(false) }, 2000) }, []) return (
) } ``` #### JSON Schema Cases ```tsx import React, { useState, useEffect } from 'react' import { createForm } from '@formily/core' import { createSchemaField, useField } from '@formily/react' import { Form, FormItem, FormLayout, Input, Select, Cascader, DatePicker, FormGrid, Upload, ArrayItems, Editable, PreviewText, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button, Spin } from 'antd' import { UploadOutlined } from '@ant-design/icons' const form = createForm({ readPretty: true, validateFirst: true, }) const IDUpload = (props) => { const field = useField() return ( {field.editable && ( )} ) } const SchemaField = createSchemaField({ components: { FormItem, FormGrid, FormLayout, Input, DatePicker, Cascader, Select, IDUpload, ArrayItems, Editable, }, scope: { fetchAddress: (field) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }, }, }) const schema = { type: 'object', properties: { username: { type: 'string', title: 'Username', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, name: { type: 'void', title: 'Name', 'x-decorator': 'FormItem', 'x-decorator-props': { asterisk: true, feedbackLayout: 'none', }, 'x-component': 'FormGrid', properties: { firstName: { type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { placeholder: 'firstName', }, }, lastName: { type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { placeholder: 'lastname', }, }, }, }, email: { type: 'string', title: 'Email', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': 'email', }, gender: { type: 'string', title: 'Gender', enum: [ { label: 'male', value: 1, }, { label: 'female', value: 2, }, { label: 'third gender', value: 3, }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', }, birthday: { type: 'string', required: true, title: 'Birthday', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', }, address: { type: 'string', required: true, title: 'Address', 'x-decorator': 'FormItem', 'x-component': 'Cascader', 'x-reactions': '{{fetchAddress}}', }, idCard: { type: 'string', required: true, title: 'ID', 'x-decorator': 'FormItem', 'x-component': 'IDUpload', }, contacts: { type: 'array', required: true, title: 'Contacts', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems', items: { type: 'object', 'x-component': 'ArrayItems.Item', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, popover: { type: 'void', title: 'Contact Informations', 'x-decorator': 'Editable.Popover', 'x-component': 'FormLayout', 'x-component-props': { layout: 'vertical', }, 'x-reactions': [ { fulfill: { schema: { title: '{{$self.query(".name").value() }}', }, }, }, ], properties: { name: { type: 'string', title: 'Name', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { style: { width: 300, }, }, }, email: { type: 'string', title: 'Email', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': [{ required: true }, 'email'], 'x-component-props': { style: { width: 300, }, }, }, phone: { type: 'string', title: 'Phone Number', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': [{ required: true }, 'phone'], 'x-component-props': { style: { width: 300, }, }, }, }, }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, properties: { addition: { type: 'void', title: 'Add Contact', 'x-component': 'ArrayItems.Addition', }, }, }, }, } export default () => { const [loading, setLoading] = useState(true) useEffect(() => { setTimeout(() => { form.setInitialValues({ username: 'Aston Martin', firstName: 'Aston', lastName: 'Martin', email: 'aston_martin@aston.com', gender: 1, birthday: '1836-01-03', address: ['110000', '110000', '110101'], idCard: [ { name: 'this is image', thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', uid: 'rc-upload-1615825692847-2', url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', }, ], contacts: [ { name: 'Zhang San', phone: '13245633378', email: 'zhangsan@gmail.com', }, { name: 'Li Si', phone: '16873452678', email: 'lisi@gmail.com' }, ], }) setLoading(false) }, 2000) }, []) return (
) } ``` #### Pure JSX Cases ```tsx import React, { useState, useEffect } from 'react' import { createForm } from '@formily/core' import { Field, VoidField, ArrayField, useField } from '@formily/react' import { Form, FormItem, FormLayout, Input, Select, Cascader, DatePicker, FormGrid, ArrayBase, Upload, PreviewText, Editable, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button, Spin } from 'antd' import { UploadOutlined } from '@ant-design/icons' import './index.less' const form = createForm({ validateFirst: true, readPretty: true, }) const IDUpload = (props) => { const field = useField() return ( {field.editable && ( )} ) } const fetchAddress = (field) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) } export default () => { const [loading, setLoading] = useState(true) useEffect(() => { setTimeout(() => { form.setInitialValues({ username: 'Aston Martin', firstName: 'Aston', lastName: 'Martin', email: 'aston_martin@aston.com', gender: 1, birthday: '1836-01-03', address: ['110000', '110000', '110101'], idCard: [ { name: 'this is image', thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', uid: 'rc-upload-1615825692847-2', url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', }, ], contacts: [ { name: 'Zhang San', phone: '13245633378', email: 'zhangsan@gmail.com', }, { name: 'Li Si', phone: '16873452678', email: 'lisi@gmail.com' }, ], }) setLoading(false) }, 2000) }, []) return (
{(field) => ( {field.value?.map((item, index) => (
{ field.title = field.query('.[].name').value() || field.title }} >
))}
)}
) } ``` ================================================ FILE: docs/guide/scenes/edit-detail.zh-CN.md ================================================ # 编辑详情 ## 编辑 #### Markup Schema 案例 ```tsx import React, { useState, useEffect } from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, FormLayout, Input, Select, Cascader, DatePicker, Submit, FormGrid, Upload, ArrayItems, Editable, FormButtonGroup, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button, Spin } from 'antd' import { UploadOutlined } from '@ant-design/icons' const form = createForm({ validateFirst: true, }) const IDUpload = (props) => { return ( ) } const SchemaField = createSchemaField({ components: { FormItem, FormGrid, FormLayout, Input, DatePicker, Cascader, Select, IDUpload, ArrayItems, Editable, }, scope: { fetchAddress: (field) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }, }, }) export default () => { const [loading, setLoading] = useState(true) useEffect(() => { setTimeout(() => { form.setInitialValues({ username: 'Aston Martin', firstName: 'Aston', lastName: 'Martin', email: 'aston_martin@aston.com', gender: 1, birthday: '1836-01-03', address: ['110000', '110000', '110101'], idCard: [ { name: 'this is image', thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', uid: 'rc-upload-1615825692847-2', url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', }, ], contacts: [ { name: '张三', phone: '13245633378', email: 'zhangsan@gmail.com' }, { name: '李四', phone: '16873452678', email: 'lisi@gmail.com' }, ], }) setLoading(false) }, 2000) }, []) return (
提交
) } ``` #### JSON Schema 案例 ```tsx import React, { useState, useEffect } from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, FormLayout, Input, Select, Cascader, DatePicker, Submit, FormGrid, Upload, ArrayItems, Editable, FormButtonGroup, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button, Spin } from 'antd' import { UploadOutlined } from '@ant-design/icons' const form = createForm({ validateFirst: true, }) const IDUpload = (props) => { return ( ) } const SchemaField = createSchemaField({ components: { FormItem, FormGrid, FormLayout, Input, DatePicker, Cascader, Select, IDUpload, ArrayItems, Editable, }, scope: { fetchAddress: (field) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }, }, }) const schema = { type: 'object', properties: { username: { type: 'string', title: '用户名', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, name: { type: 'void', title: '姓名', 'x-decorator': 'FormItem', 'x-decorator-props': { asterisk: true, feedbackLayout: 'none', }, 'x-component': 'FormGrid', properties: { firstName: { type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { placeholder: '姓', }, }, lastName: { type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { placeholder: '名', }, }, }, }, email: { type: 'string', title: '邮箱', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': 'email', }, gender: { type: 'string', title: '性别', enum: [ { label: '男', value: 1, }, { label: '女', value: 2, }, { label: '第三性别', value: 3, }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', }, birthday: { type: 'string', required: true, title: '生日', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', }, address: { type: 'string', required: true, title: '地址', 'x-decorator': 'FormItem', 'x-component': 'Cascader', 'x-reactions': '{{fetchAddress}}', }, idCard: { type: 'string', required: true, title: '身份证复印件', 'x-decorator': 'FormItem', 'x-component': 'IDUpload', }, contacts: { type: 'array', required: true, title: '联系人信息', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems', items: { type: 'object', 'x-component': 'ArrayItems.Item', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, popover: { type: 'void', title: '完善联系人信息', 'x-decorator': 'Editable.Popover', 'x-component': 'FormLayout', 'x-component-props': { layout: 'vertical', }, 'x-reactions': [ { fulfill: { schema: { title: '{{$self.query(".name").value() }}', }, }, }, ], properties: { name: { type: 'string', title: '姓名', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { style: { width: 300, }, }, }, email: { type: 'string', title: '邮箱', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': [{ required: true }, 'email'], 'x-component-props': { style: { width: 300, }, }, }, phone: { type: 'string', title: '手机号', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': [{ required: true }, 'phone'], 'x-component-props': { style: { width: 300, }, }, }, }, }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, properties: { addition: { type: 'void', title: '新增联系人', 'x-component': 'ArrayItems.Addition', }, }, }, }, } export default () => { const [loading, setLoading] = useState(true) useEffect(() => { setTimeout(() => { form.setInitialValues({ username: 'Aston Martin', firstName: 'Aston', lastName: 'Martin', email: 'aston_martin@aston.com', gender: 1, birthday: '1836-01-03', address: ['110000', '110000', '110101'], idCard: [ { name: 'this is image', thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', uid: 'rc-upload-1615825692847-2', url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', }, ], contacts: [ { name: '张三', phone: '13245633378', email: 'zhangsan@gmail.com' }, { name: '李四', phone: '16873452678', email: 'lisi@gmail.com' }, ], }) setLoading(false) }, 2000) }, []) return (
提交
) } ``` #### 纯 JSX 案例 ```tsx import React, { useState, useEffect } from 'react' import { createForm } from '@formily/core' import { Field, VoidField, ArrayField } from '@formily/react' import { Form, FormItem, FormLayout, Input, Select, Cascader, DatePicker, Submit, FormGrid, Upload, ArrayBase, Editable, FormButtonGroup, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button, Spin } from 'antd' import { UploadOutlined } from '@ant-design/icons' import './index.less' const form = createForm({ validateFirst: true, }) const IDUpload = (props) => { return ( ) } const fetchAddress = (field) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) } export default () => { const [loading, setLoading] = useState(true) useEffect(() => { setTimeout(() => { form.setInitialValues({ username: 'Aston Martin', firstName: 'Aston', lastName: 'Martin', email: 'aston_martin@aston.com', gender: 1, birthday: '1836-01-03', address: ['110000', '110000', '110101'], idCard: [ { name: 'this is image', thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', uid: 'rc-upload-1615825692847-2', url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', }, ], contacts: [ { name: '张三', phone: '13245633378', email: 'zhangsan@gmail.com' }, { name: '李四', phone: '16873452678', email: 'lisi@gmail.com' }, ], }) setLoading(false) }, 2000) }, []) return (
{(field) => ( {field.value?.map((item, index) => (
{ field.title = field.query('.[].name').value() || field.title }} >
))}
)}
提交
) } ``` ## 详情 #### Markup Schema 案例 ```tsx import React, { useState, useEffect } from 'react' import { createForm } from '@formily/core' import { createSchemaField, useField } from '@formily/react' import { Form, FormItem, FormLayout, Input, Select, Cascader, DatePicker, FormGrid, Upload, ArrayItems, Editable, PreviewText, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button, Spin } from 'antd' import { UploadOutlined } from '@ant-design/icons' const form = createForm({ readPretty: true, validateFirst: true, }) const IDUpload = (props) => { const field = useField() return ( {field.editable && } ) } const SchemaField = createSchemaField({ components: { FormItem, FormGrid, FormLayout, Input, DatePicker, Cascader, Select, IDUpload, ArrayItems, Editable, }, scope: { fetchAddress: (field) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }, }, }) export default () => { const [loading, setLoading] = useState(true) useEffect(() => { setTimeout(() => { form.setInitialValues({ username: 'Aston Martin', firstName: 'Aston', lastName: 'Martin', email: 'aston_martin@aston.com', gender: 1, birthday: '1836-01-03', address: ['110000', '110000', '110101'], idCard: [ { name: 'this is image', thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', uid: 'rc-upload-1615825692847-2', url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', }, ], contacts: [ { name: '张三', phone: '13245633378', email: 'zhangsan@gmail.com' }, { name: '李四', phone: '16873452678', email: 'lisi@gmail.com' }, ], }) setLoading(false) }, 2000) }, []) return (
) } ``` #### JSON Schema 案例 ```tsx import React, { useState, useEffect } from 'react' import { createForm } from '@formily/core' import { createSchemaField, useField } from '@formily/react' import { Form, FormItem, FormLayout, Input, Select, Cascader, DatePicker, FormGrid, Upload, ArrayItems, Editable, PreviewText, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button, Spin } from 'antd' import { UploadOutlined } from '@ant-design/icons' const form = createForm({ readPretty: true, validateFirst: true, }) const IDUpload = (props) => { const field = useField() return ( {field.editable && } ) } const SchemaField = createSchemaField({ components: { FormItem, FormGrid, FormLayout, Input, DatePicker, Cascader, Select, IDUpload, ArrayItems, Editable, }, scope: { fetchAddress: (field) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }, }, }) const schema = { type: 'object', properties: { username: { type: 'string', title: '用户名', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, name: { type: 'void', title: '姓名', 'x-decorator': 'FormItem', 'x-decorator-props': { asterisk: true, feedbackLayout: 'none', }, 'x-component': 'FormGrid', properties: { firstName: { type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { placeholder: '姓', }, }, lastName: { type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { placeholder: '名', }, }, }, }, email: { type: 'string', title: '邮箱', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': 'email', }, gender: { type: 'string', title: '性别', enum: [ { label: '男', value: 1, }, { label: '女', value: 2, }, { label: '第三性别', value: 3, }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', }, birthday: { type: 'string', required: true, title: '生日', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', }, address: { type: 'string', required: true, title: '地址', 'x-decorator': 'FormItem', 'x-component': 'Cascader', 'x-reactions': '{{fetchAddress}}', }, idCard: { type: 'string', required: true, title: '身份证复印件', 'x-decorator': 'FormItem', 'x-component': 'IDUpload', }, contacts: { type: 'array', required: true, title: '联系人信息', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems', items: { type: 'object', 'x-component': 'ArrayItems.Item', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, popover: { type: 'void', title: '完善联系人信息', 'x-decorator': 'Editable.Popover', 'x-component': 'FormLayout', 'x-component-props': { layout: 'vertical', }, 'x-reactions': [ { fulfill: { schema: { title: '{{$self.query(".name").value() }}', }, }, }, ], properties: { name: { type: 'string', title: '姓名', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { style: { width: 300, }, }, }, email: { type: 'string', title: '邮箱', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': [{ required: true }, 'email'], 'x-component-props': { style: { width: 300, }, }, }, phone: { type: 'string', title: '手机号', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': [{ required: true }, 'phone'], 'x-component-props': { style: { width: 300, }, }, }, }, }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, properties: { addition: { type: 'void', title: '新增联系人', 'x-component': 'ArrayItems.Addition', }, }, }, }, } export default () => { const [loading, setLoading] = useState(true) useEffect(() => { setTimeout(() => { form.setInitialValues({ username: 'Aston Martin', firstName: 'Aston', lastName: 'Martin', email: 'aston_martin@aston.com', gender: 1, birthday: '1836-01-03', address: ['110000', '110000', '110101'], idCard: [ { name: 'this is image', thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', uid: 'rc-upload-1615825692847-2', url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', }, ], contacts: [ { name: '张三', phone: '13245633378', email: 'zhangsan@gmail.com' }, { name: '李四', phone: '16873452678', email: 'lisi@gmail.com' }, ], }) setLoading(false) }, 2000) }, []) return (
) } ``` #### 纯 JSX 案例 ```tsx import React, { useState, useEffect } from 'react' import { createForm } from '@formily/core' import { Field, VoidField, ArrayField, useField } from '@formily/react' import { Form, FormItem, FormLayout, Input, Select, Cascader, DatePicker, FormGrid, ArrayBase, Upload, PreviewText, Editable, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button, Spin } from 'antd' import { UploadOutlined } from '@ant-design/icons' import './index.less' const form = createForm({ validateFirst: true, readPretty: true, }) const IDUpload = (props) => { const field = useField() return ( {field.editable && } ) } const fetchAddress = (field) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) } export default () => { const [loading, setLoading] = useState(true) useEffect(() => { setTimeout(() => { form.setInitialValues({ username: 'Aston Martin', firstName: 'Aston', lastName: 'Martin', email: 'aston_martin@aston.com', gender: 1, birthday: '1836-01-03', address: ['110000', '110000', '110101'], idCard: [ { name: 'this is image', thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', uid: 'rc-upload-1615825692847-2', url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', }, ], contacts: [ { name: '张三', phone: '13245633378', email: 'zhangsan@gmail.com' }, { name: '李四', phone: '16873452678', email: 'lisi@gmail.com' }, ], }) setLoading(false) }, 2000) }, []) return (
{(field) => ( {field.value?.map((item, index) => (
{ field.title = field.query('.[].name').value() || field.title }} >
))}
)}
) } ``` ================================================ FILE: docs/guide/scenes/index.less ================================================ .array-items-item { border: 1px solid rgb(238, 238, 238); margin-bottom: 10px; padding: 3px 6px; display: flex; justify-content: space-around; transition: all 0.25s; .ant-formily-item { margin-bottom: 0 !important; } &:hover { border: 1px solid rgb(170, 170, 170); } } ================================================ FILE: docs/guide/scenes/login-register.md ================================================ # Log in&Sign up ## Log in #### Markup Schema Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, Password, Submit } from '@formily/antd' import { Tabs, Card } from 'antd' import * as ICONS from '@ant-design/icons' import { VerifyCode } from './VerifyCode' const normalForm = createForm({ validateFirst: true, }) const phoneForm = createForm({ validateFirst: true, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Password, VerifyCode, }, scope: { icon(name) { return React.createElement(ICONS[name]) }, }, }) export default () => { return (
Log in
Log in
) } ``` #### JSON Schema Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, Password, Submit } from '@formily/antd' import { Tabs, Card } from 'antd' import * as ICONS from '@ant-design/icons' import { VerifyCode } from './VerifyCode' const normalForm = createForm({ validateFirst: true, }) const phoneForm = createForm({ validateFirst: true, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Password, VerifyCode, }, scope: { icon(name) { return React.createElement(ICONS[name]) }, }, }) const normalSchema = { type: 'object', properties: { username: { type: 'string', title: 'Username', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { prefix: "{{icon('UserOutlined')}}", }, }, password: { type: 'string', title: 'Password', required: true, 'x-decorator': 'FormItem', 'x-component': 'Password', 'x-component-props': { prefix: "{{icon('LockOutlined')}}", }, }, }, } const phoneSchema = { type: 'object', properties: { phone: { type: 'string', title: 'Phone Number', required: true, 'x-validator': 'phone', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { prefix: "{{icon('PhoneOutlined')}}", }, }, verifyCode: { type: 'string', title: 'Verification Code', required: true, 'x-decorator': 'FormItem', 'x-component': 'VerifyCode', 'x-component-props': { prefix: "{{icon('LockOutlined')}}", }, 'x-reactions': [ { dependencies: ['.phone#value', '.phone#valid'], fulfill: { state: { 'component[1].readyPost': '{{$deps[0] && $deps[1]}}', 'component[1].phoneNumber': '{{$deps[0]}}', }, }, }, ], }, }, } export default () => { return (
Log in
Log in
) } ``` #### Pure JSX Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, Input, Password, Submit } from '@formily/antd' import { Tabs, Card } from 'antd' import { UserOutlined, LockOutlined, PhoneOutlined } from '@ant-design/icons' import { VerifyCode } from './VerifyCode' const normalForm = createForm({ validateFirst: true, }) const phoneForm = createForm({ validateFirst: true, }) export default () => { return (
, }, ]} /> , }, ]} /> Log in
, }, ]} /> { const phone = field.query('.phone') field.setComponentProps({ readyPost: phone.get('valid') && phone.get('value'), phoneNumber: phone.get('value'), }) }} decorator={[FormItem]} component={[ VerifyCode, { prefix: , }, ]} /> Log in
) } ``` ## Sign up #### Markup Schema Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, FormLayout, Input, Select, Password, Cascader, DatePicker, Submit, Space, FormGrid, Upload, ArrayItems, Editable, FormButtonGroup, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button } from 'antd' import { UploadOutlined } from '@ant-design/icons' const form = createForm({ validateFirst: true, }) const IDUpload = (props) => { return ( ) } const SchemaField = createSchemaField({ components: { FormItem, FormGrid, FormLayout, Input, DatePicker, Cascader, Select, Password, IDUpload, Space, ArrayItems, Editable, }, scope: { fetchAddress: (field) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }, }, }) export default () => { return (
Sign up
) } ``` #### JSON Schema Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, FormLayout, Input, Select, Password, Cascader, DatePicker, Submit, Space, FormGrid, Upload, ArrayItems, Editable, FormButtonGroup, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button } from 'antd' import { UploadOutlined } from '@ant-design/icons' const form = createForm({ validateFirst: true, }) const IDUpload = (props) => { return ( ) } const SchemaField = createSchemaField({ components: { FormItem, FormGrid, FormLayout, Input, DatePicker, Cascader, Select, Password, IDUpload, Space, ArrayItems, Editable, }, scope: { fetchAddress: (field) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }, }, }) const schema = { type: 'object', properties: { username: { type: 'string', title: 'Username', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, password: { type: 'string', title: 'Password', required: true, 'x-decorator': 'FormItem', 'x-component': 'Password', 'x-component-props': { checkStrength: true, }, 'x-reactions': [ { dependencies: ['.confirm_password'], fulfill: { state: { selfErrors: '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认password不匹配" : ""}}', }, }, }, ], }, confirm_password: { type: 'string', title: 'Confirm Password', required: true, 'x-decorator': 'FormItem', 'x-component': 'Password', 'x-component-props': { checkStrength: true, }, 'x-reactions': [ { dependencies: ['.password'], fulfill: { state: { selfErrors: '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "Confirm that the password does not match" : ""}}', }, }, }, ], }, name: { type: 'void', title: 'name', 'x-decorator': 'FormItem', 'x-decorator-props': { asterisk: true, feedbackLayout: 'none', }, 'x-component': 'FormGrid', properties: { firstName: { type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { placeholder: 'firstname', }, }, lastName: { type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { placeholder: 'lastname', }, }, }, }, email: { type: 'string', title: 'Email', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': 'email', }, gender: { type: 'string', title: 'Gender', enum: [ { label: 'male', value: 1, }, { label: 'female', value: 2, }, { label: 'third gender', value: 3, }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', }, birthday: { type: 'string', required: true, title: 'Birthday', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', }, address: { type: 'string', required: true, title: 'Address', 'x-decorator': 'FormItem', 'x-component': 'Cascader', 'x-reactions': '{{fetchAddress}}', }, idCard: { type: 'string', required: true, title: 'ID', 'x-decorator': 'FormItem', 'x-component': 'IDUpload', }, contacts: { type: 'array', required: true, title: 'Contacts', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems', items: { type: 'object', 'x-component': 'ArrayItems.Item', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, popover: { type: 'void', title: 'improve contact information', 'x-decorator': 'Editable.Popover', 'x-component': 'FormLayout', 'x-component-props': { layout: 'vertical', }, 'x-reactions': [ { dependencies: ['.popover.name'], fulfill: { schema: { title: '{{$deps[0]}}', }, }, }, ], properties: { name: { type: 'string', title: 'Name', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { style: { width: 300, }, }, }, email: { type: 'string', title: 'Email', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': [{ required: true }, 'email'], 'x-component-props': { style: { width: 300, }, }, }, phone: { type: 'string', title: 'Phone Number', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': [{ required: true }, 'phone'], 'x-component-props': { style: { width: 300, }, }, }, }, }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, properties: { addition: { type: 'void', title: 'Add Contact', 'x-component': 'ArrayItems.Addition', }, }, }, }, } export default () => { return (
Sign up
) } ``` #### Pure JSX Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { Field, VoidField, ArrayField } from '@formily/react' import { Form, FormItem, Input, Select, Password, Cascader, DatePicker, Submit, FormGrid, Upload, FormButtonGroup, ArrayBase, Editable, FormLayout, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button } from 'antd' import { UploadOutlined } from '@ant-design/icons' const form = createForm({ validateFirst: true, }) const IDUpload = (props) => { return ( ) } export default () => { return (
{ const confirm = field.query('.confirm_password') field.selfErrors = confirm.get('value') && field.value && field.value !== confirm.get('value') ? 'Confirm that the password does not match' : '' }} /> { const password = field.query('.password') field.selfErrors = password.get('value') && field.value && field.value !== password.get('value') ? 'Confirm that the password does not match' : '' }} /> { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }} /> {(field) => ( {field.value?.map((item, index) => (
{ field.title = field.query('.[].name').value() || field.title }} >
))}
)}
Sign up
) } ``` ## Forgot password #### Markup Schema Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, Password, Submit, FormButtonGroup, } from '@formily/antd' import { Card } from 'antd' const form = createForm({ validateFirst: true, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Password, }, }) export default () => { return (
Confirm
) } ``` #### JSON Schema Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, Password, Submit, FormButtonGroup, } from '@formily/antd' import { Card } from 'antd' const form = createForm({ validateFirst: true, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Password, }, }) const schema = { type: 'object', properties: { username: { type: 'string', title: 'Username', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, email: { type: 'string', title: 'Email', required: true, 'x-validator': 'email', 'x-decorator': 'FormItem', 'x-component': 'Input', }, oldPassword: { type: 'string', title: 'Old Password', required: true, 'x-decorator': 'FormItem', 'x-component': 'Password', }, password: { type: 'string', title: 'New Password', required: true, 'x-decorator': 'FormItem', 'x-component': 'Password', 'x-component-props': { checkStrength: true, }, 'x-reactions': [ { dependencies: ['.confirm_password'], fulfill: { state: { selfErrors: '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "Confirm that the password does not match" : ""}}', }, }, }, ], }, confirm_password: { type: 'string', title: 'Confirm Password', required: true, 'x-decorator': 'FormItem', 'x-component': 'Password', 'x-component-props': { checkStrength: true, }, 'x-reactions': [ { dependencies: ['.password'], fulfill: { state: { selfErrors: '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "Confirm that the password does not match" : ""}}', }, }, }, ], }, }, } export default () => { return (
Confirm
) } ``` #### Pure JSX Cases ```tsx import React from 'react' import { createForm } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, Input, Password, Submit, FormButtonGroup, } from '@formily/antd' import { Card } from 'antd' const form = createForm({ validateFirst: true, }) export default () => { return (
{ const confirm = field.query('.confirm_password') field.selfErrors = confirm.get('value') && field.value && field.value !== confirm.get('value') ? 'Confirm that the password does not match' : '' }} /> { const confirm = field.query('.password') field.selfErrors = confirm.get('value') && field.value && field.value !== confirm.get('value') ? 'Confirm that the password does not match' : '' }} /> Confirm change
) } ``` ================================================ FILE: docs/guide/scenes/login-register.zh-CN.md ================================================ # 登录注册 ## 登录 #### Markup Schema 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, Password, Submit } from '@formily/antd' import { Tabs, Card } from 'antd' import * as ICONS from '@ant-design/icons' import { VerifyCode } from './VerifyCode' const normalForm = createForm({ validateFirst: true, }) const phoneForm = createForm({ validateFirst: true, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Password, VerifyCode, }, scope: { icon(name) { return React.createElement(ICONS[name]) }, }, }) export default () => { return ( ) } ``` #### JSON Schema 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, Password, Submit } from '@formily/antd' import { Tabs, Card } from 'antd' import * as ICONS from '@ant-design/icons' import { VerifyCode } from './VerifyCode' const normalForm = createForm({ validateFirst: true, }) const phoneForm = createForm({ validateFirst: true, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Password, VerifyCode, }, scope: { icon(name) { return React.createElement(ICONS[name]) }, }, }) const normalSchema = { type: 'object', properties: { username: { type: 'string', title: '用户名', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { prefix: "{{icon('UserOutlined')}}", }, }, password: { type: 'string', title: '密码', required: true, 'x-decorator': 'FormItem', 'x-component': 'Password', 'x-component-props': { prefix: "{{icon('LockOutlined')}}", }, }, }, } const phoneSchema = { type: 'object', properties: { phone: { type: 'string', title: '手机号', required: true, 'x-validator': 'phone', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { prefix: "{{icon('PhoneOutlined')}}", }, }, verifyCode: { type: 'string', title: '验证码', required: true, 'x-decorator': 'FormItem', 'x-component': 'VerifyCode', 'x-component-props': { prefix: "{{icon('LockOutlined')}}", }, 'x-reactions': [ { dependencies: ['.phone#value', '.phone#valid'], fulfill: { state: { 'component[1].readyPost': '{{$deps[0] && $deps[1]}}', 'component[1].phoneNumber': '{{$deps[0]}}', }, }, }, ], }, }, } export default () => { return ( ) } ``` #### 纯 JSX 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, Input, Password, Submit } from '@formily/antd' import { Tabs, Card } from 'antd' import { UserOutlined, LockOutlined, PhoneOutlined } from '@ant-design/icons' import { VerifyCode } from './VerifyCode' const normalForm = createForm({ validateFirst: true, }) const phoneForm = createForm({ validateFirst: true, }) export default () => { return (
, }, ]} /> , }, ]} /> 登录
, }, ]} /> { const phone = field.query('.phone') field.setComponentProps({ readyPost: phone.get('valid') && phone.get('value'), phoneNumber: phone.get('value'), }) }} decorator={[FormItem]} component={[ VerifyCode, { prefix: , }, ]} /> 登录
) } ``` ## 新用户注册 #### Markup Schema 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, FormLayout, Input, Select, Password, Cascader, DatePicker, Submit, Space, FormGrid, Upload, ArrayItems, Editable, FormButtonGroup, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button } from 'antd' import { UploadOutlined } from '@ant-design/icons' const form = createForm({ validateFirst: true, }) const IDUpload = (props) => { return ( ) } const SchemaField = createSchemaField({ components: { FormItem, FormGrid, FormLayout, Input, DatePicker, Cascader, Select, Password, IDUpload, Space, ArrayItems, Editable, }, scope: { fetchAddress: (field) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }, }, }) export default () => { return (
注册
) } ``` #### JSON Schema 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, FormLayout, Input, Select, Password, Cascader, DatePicker, Submit, Space, FormGrid, Upload, ArrayItems, Editable, FormButtonGroup, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button } from 'antd' import { UploadOutlined } from '@ant-design/icons' const form = createForm({ validateFirst: true, }) const IDUpload = (props) => { return ( ) } const SchemaField = createSchemaField({ components: { FormItem, FormGrid, FormLayout, Input, DatePicker, Cascader, Select, Password, IDUpload, Space, ArrayItems, Editable, }, scope: { fetchAddress: (field) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }, }, }) const schema = { type: 'object', properties: { username: { type: 'string', title: '用户名', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, password: { type: 'string', title: '密码', required: true, 'x-decorator': 'FormItem', 'x-component': 'Password', 'x-component-props': { checkStrength: true, }, 'x-reactions': [ { dependencies: ['.confirm_password'], fulfill: { state: { selfErrors: '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}', }, }, }, ], }, confirm_password: { type: 'string', title: '确认密码', required: true, 'x-decorator': 'FormItem', 'x-component': 'Password', 'x-component-props': { checkStrength: true, }, 'x-reactions': [ { dependencies: ['.password'], fulfill: { state: { selfErrors: '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}', }, }, }, ], }, name: { type: 'void', title: '姓名', 'x-decorator': 'FormItem', 'x-decorator-props': { asterisk: true, feedbackLayout: 'none', }, 'x-component': 'FormGrid', properties: { firstName: { type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { placeholder: '姓', }, }, lastName: { type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { placeholder: '名', }, }, }, }, email: { type: 'string', title: '邮箱', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': 'email', }, gender: { type: 'string', title: '性别', enum: [ { label: '男', value: 1, }, { label: '女', value: 2, }, { label: '第三性别', value: 3, }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', }, birthday: { type: 'string', required: true, title: '生日', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', }, address: { type: 'string', required: true, title: '地址', 'x-decorator': 'FormItem', 'x-component': 'Cascader', 'x-reactions': '{{fetchAddress}}', }, idCard: { type: 'string', required: true, title: '身份证复印件', 'x-decorator': 'FormItem', 'x-component': 'IDUpload', }, contacts: { type: 'array', required: true, title: '联系人信息', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems', items: { type: 'object', 'x-component': 'ArrayItems.Item', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, popover: { type: 'void', title: '完善联系人信息', 'x-decorator': 'Editable.Popover', 'x-component': 'FormLayout', 'x-component-props': { layout: 'vertical', }, 'x-reactions': [ { dependencies: ['.popover.name'], fulfill: { schema: { title: '{{$deps[0]}}', }, }, }, ], properties: { name: { type: 'string', title: '姓名', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { style: { width: 300, }, }, }, email: { type: 'string', title: '邮箱', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': [{ required: true }, 'email'], 'x-component-props': { style: { width: 300, }, }, }, phone: { type: 'string', title: '手机号', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': [{ required: true }, 'phone'], 'x-component-props': { style: { width: 300, }, }, }, }, }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, properties: { addition: { type: 'void', title: '新增联系人', 'x-component': 'ArrayItems.Addition', }, }, }, }, } export default () => { return (
注册
) } ``` #### 纯 JSX 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { Field, VoidField, ArrayField } from '@formily/react' import { Form, FormItem, Input, Select, Password, Cascader, DatePicker, Submit, FormGrid, Upload, FormButtonGroup, ArrayBase, Editable, FormLayout, } from '@formily/antd' import { action } from '@formily/reactive' import { Card, Button } from 'antd' import { UploadOutlined } from '@ant-design/icons' const form = createForm({ validateFirst: true, }) const IDUpload = (props) => { return ( ) } export default () => { return (
{ const confirm = field.query('.confirm_password') field.selfErrors = confirm.get('value') && field.value && field.value !== confirm.get('value') ? '确认密码不匹配' : '' }} /> { const password = field.query('.password') field.selfErrors = password.get('value') && field.value && field.value !== password.get('value') ? '确认密码不匹配' : '' }} /> { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }} /> {(field) => ( {field.value?.map((item, index) => (
{ field.title = field.query('.[].name').value() || field.title }} >
))}
)}
注册
) } ``` ## 忘记密码 #### Markup Schema 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, Password, Submit, FormButtonGroup, } from '@formily/antd' import { Card } from 'antd' const form = createForm({ validateFirst: true, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Password, }, }) export default () => { return (
确认变更
) } ``` #### JSON Schema 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' import { Form, FormItem, Input, Password, Submit, FormButtonGroup, } from '@formily/antd' import { Card } from 'antd' const form = createForm({ validateFirst: true, }) const SchemaField = createSchemaField({ components: { FormItem, Input, Password, }, }) const schema = { type: 'object', properties: { username: { type: 'string', title: '用户名', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, email: { type: 'string', title: '邮箱', required: true, 'x-validator': 'email', 'x-decorator': 'FormItem', 'x-component': 'Input', }, oldPassword: { type: 'string', title: '原始密码', required: true, 'x-decorator': 'FormItem', 'x-component': 'Password', }, password: { type: 'string', title: '新密码', required: true, 'x-decorator': 'FormItem', 'x-component': 'Password', 'x-component-props': { checkStrength: true, }, 'x-reactions': [ { dependencies: ['.confirm_password'], fulfill: { state: { selfErrors: '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}', }, }, }, ], }, confirm_password: { type: 'string', title: '确认密码', required: true, 'x-decorator': 'FormItem', 'x-component': 'Password', 'x-component-props': { checkStrength: true, }, 'x-reactions': [ { dependencies: ['.password'], fulfill: { state: { selfErrors: '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}', }, }, }, ], }, }, } export default () => { return (
确认变更
) } ``` #### 纯 JSX 案例 ```tsx import React from 'react' import { createForm } from '@formily/core' import { Field } from '@formily/react' import { Form, FormItem, Input, Password, Submit, FormButtonGroup, } from '@formily/antd' import { Card } from 'antd' const form = createForm({ validateFirst: true, }) export default () => { return (
{ const confirm = field.query('.confirm_password') field.selfErrors = confirm.get('value') && field.value && field.value !== confirm.get('value') ? '确认密码不匹配' : '' }} /> { const confirm = field.query('.password') field.selfErrors = confirm.get('value') && field.value && field.value !== confirm.get('value') ? '确认密码不匹配' : '' }} /> 确认变更
) } ``` ================================================ FILE: docs/guide/scenes/more.md ================================================ # More Scenes Because Formily is a very complete solution at the form level, and it is also very flexible. It supports a lot of scenarios, but we can't list them all. Therefore, I still hope that the community can help Formily improve more scenarios! We would be very grateful!😀 ================================================ FILE: docs/guide/scenes/more.zh-CN.md ================================================ # 更多场景 因为 Formily 在表单层面上是一个非常完备的方案,而且还很灵活,支持的场景非常多,但是场景案例,我们无法一一列举。 所以,还是希望社区能帮助 Formily 完善更多场景案例!我们会不胜感激!😀 ================================================ FILE: docs/guide/scenes/query-list.md ================================================ # Query list Because Formily Schema can completely describe the UI, we can simply abstract out the QueryList/QueryForm/QueryTable components to combine to implement the query list component. The following is only the pseudo code, because the query list scenario usually involves a lot of business packaging. At present, Formily hasn't figured out how to consider both versatility and quick start of business, so it will not open up specific components for the time being. But you can take a look at the pseudo-code first. If these components are officially implemented, the usage will definitely be like this: ```tsx pure import React from 'react' import { Void, Object, Array, String } from './MySchemaField' export default () => ( fetchRecords(params), }} > ) ``` ## Ideas - QueryList - Mainly responsible for sending requests at the top level, and issuing query methods to QueryForm and QueryTable for consumption through React Context - Query parameters need to call `form.query('query')` to find the field of QueryForm, and then take out the value of the field to send the request - When you have finished querying the data, you need to call `form.query('list')` to find the QueryTable field, and then fill in the table data for the value of the field model - QueryTable - The idea is very similar to that of ArrayTable. The main thing is to parse the Schema subtree and assemble the Columns data needed by the Table by yourself. If you want to support column merging and row merging, you need to parse more complex data - Based on props.value for rendering Table structure - Rely on RecursionField to render the internal data of the Table Column - Rely on the query method passed down from the context to achieve paging query - QueryForm - There is no special logic, the main thing is to combine Form+FormGrid to realize a query form layout - Realize query form query by relying on the query method passed down from the context ================================================ FILE: docs/guide/scenes/query-list.zh-CN.md ================================================ # 查询列表 因为 Formily Schema 是可以完全描述 UI 的,所以我们可以简单的抽象出 QueryList/QueryForm/QueryTable 几个组件来组合实现查询列表组件,以下只是给出伪代码,因为查询列表场景通常都会涉及大量业务封装,目前 Formily 还没想好怎么既考虑通用性又能考虑业务快速上手,所以暂时不开放出具体组件。 不过可以先看看伪代码,如果官方实现这几个组件,那使用方式肯定会是这样: ```tsx pure import React from 'react' import { Void, Object, Array, String } from './MySchemaField' export default () => ( fetchRecords(params), }} > ) ``` ## 思路 - QueryList - 主要负责在顶层发请求,通过 React Context 下发 query 方法给 QueryForm 和 QueryTable 消费 - 查询参数需要调用`form.query('query')`找到 QueryForm 的字段,然后取出字段的 value,用于发请求 - 当查询完数据了,需要调用`form.query('list')`找到 QueryTable 的字段,然后给字段模型的 value 填 table 数据 - QueryTable - 思路跟 ArrayTable 非常相似,主要就是解析 Schema 子树,自己拼装出 Table 需要的 Columns 数据,如果想支持列合并,行合并,就需要解析更复杂的数据 - 基于 props.value 用于渲染 Table 结构 - 依赖 RecursionField 用于渲染 Table Column 内部数据 - 依赖上下文传下来的 query 方法实现分页查询 - QueryForm - 没什么特殊逻辑,主要就是组合 Form+FormGrid 实现一个查询表单布局 - 依赖上下文传下来的 query 方法实现查询表单查询 ================================================ FILE: docs/guide/scenes/step-form.md ================================================ # Step-by-Step Form Mainly use the [FormStep](https://antd.formilyjs.org/components/form-step) component in [@formily/antd](https://antd.formilyjs.org) or [@formily/next](ttps://next.formilyjs.org) ================================================ FILE: docs/guide/scenes/step-form.zh-CN.md ================================================ # 分步表单 主要使用[@formily/antd](https://antd.formilyjs.org/zh-CN) 或 [@formily/next](https://fusion.formilyjs.org/zh-CN) 中的[FormStep](https://antd.formilyjs.org/zh-CN/components/form-step)组件 ================================================ FILE: docs/guide/scenes/tab-form.md ================================================ # Tab/Accordion Form Mainly use the [FormTab](https://antd.formilyjs.org/components/form-tab) component and [FormCollapse](https://antd.formilyjs.org/components/form-collapse) component in [@formily/antd](https://antd.formilyjs.org) or [@formily/next](https://fusion.formilyjs.org) ================================================ FILE: docs/guide/scenes/tab-form.zh-CN.md ================================================ # 选项卡/手风琴表单 主要使用[@formily/antd](https://antd.formilyjs.org/zh-CN) 或 [@formily/next](https://fusion.formilyjs.org/zh-CN) 中的[FormTab](https://antd.formilyjs.org/zh-CN/components/form-tab)组件 与 [FormCollapse](https://antd.formilyjs.org/zh-CN/components/form-collapse)组件 ================================================ FILE: docs/guide/upgrade.md ================================================ # V2 Upgrade Guide It is important to mention here that Formily2 is very different from Formily1.x, and there are a lot of Break Changes. Therefore, for old users, they basically need to learn again, and V1 and V2 cannot be upgraded smoothly. 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. ## Kernel Difference > This mainly refers to the difference between @formily/core 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: **modified** - 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. - V2: Indicates whether the field is manually modified, that is, it will be set to true when the component triggers the onChange event. **inputed** - V1: Represent Whether the field has been manually modified - V2: Remove, use modified uniformly **pristine** - V1:Represent whether the field value is equal to initialValue - V2: Remove, user manual judgment, this attribute will cause a lot of dirty checks **display** - V1: Represent whether the field is displayed, if it is false, the field value will not be removed - V2: Represent the field display mode, the value is `"none" | "visible" | "hidden"` **touched** - V1: Redundant field - V2: Remove **validating** - V1: Whether the representative field is being verified - V2: Remove, use validateStatus uniformly **effectErrors/effectWarnings** - V1: Errors and warnings that represent the manual operation of the user - V2: Remove, use feedbacks uniformly **ruleErrors/ruleWarnings** - V1: Errors and warnings representing the verification operation of the validator - V2: Remove, use feedbacks uniformly **values** - V1: Represent all the parameters returned by the onChange event - V2: Remove, use inputValues uniformly **rules** - V1: Represent verification rules - V2: Remove, use validator uniformly, because rules literally means rules, but the meaning of rules is very big, not limited to verification rules **props** - 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. - V2: Remove, use decorator and component uniformly **VirtualField** - V1: Represents a virtual field - V2: Renamed and use [VoidField](https://core.formilyjs.org/api/models/void-field) uniformly ## Bridge layer differences > This mainly refers to the difference between @formily/react and @formily/react-schema-renderer. **createFormActions/createAsyncFormActions** - V1 Create a Form operator, you can call the setFieldState/setFormState method. - 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. **Form** - V1 will create a Form instance inside, which can control the transfer of values/initialValues attributes, etc. - V2 removed, unified use of [FormProvider](https://react.formilyjs.org/api/components/form-provider) **SchemaForm** - V1 will parse the json-schema protocol internally, create a Form instance, support controlled mode, and render it. - 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. **Field** - V1 supports controlled mode, which requires the use of render props for component state mapping. - V2 does not support controlled mode, you can quickly implement state mapping by passing in the decorator/component property. **VirtualField** - V1 supports controlled mode, which requires the use of render props for component state mapping. - 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. **FieldList** - V1 Represent auto-incremented field control component - V2 Renamed to [ArrayField](https://react.formilyjs.org/api/components/array-field) **FormSpy** - V1 Monitor all life cycle triggers and re-render - V2 Remove and use [FormConsumer](https://react.formilyjs.org/api/components/form-consumer) **SchemaMarkupField** - V1 Stands for Schema description label component - V2 Remove, unified use the description label component created by the [createSchemaField](https://react.formilyjs.org/api/components/schema-field) **useFormQuery** - V1 Fast Hook for realizing form query, supporting middleware mechanism - V2 Temporarily remove **useForm** - V1 Represents the creation of a Form instance - 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) **useField** - V1 Represents the creation of a Field instance - 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) **useVirtualField** - V1 Represents the creation of a VirtualField instance - V2 Remove, if you want to create, please call [form.createVoidField](https://core.formilyjs.org/api/models/form#createvoidfield) **useFormState** - V1 Form state in consumption context - V2 Remove, use [useForm](https://react.formilyjs.org/api/hooks/use-form) uniformly **useFieldState** - V1 consume Field status in context - V2 Remove, use [useField](https://react.formilyjs.org/api/hooks/use-field) **useFormSpy** - V1 Create a lifecycle listener and trigger a re-render - V2 Remove **useSchemaProps** - V1Cconsume rops of SchemaField in context - V2 Remove, use [useFieldSchema](https://react.formilyjs.org/api/hooks/use-field-schema) uniformly **connect** - V1 Standard HOC - 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 **registerFormField/registerVirtaulBox/registerFormComponent/registerFormItemComponent** - V1 Globally registered components - V2 Remove, global registration is no longer supported **FormEffectHooks** - V1 RxJS lifecycle hook - V2 Remove, export from @formily/core uniformly, and will not return RxJS Observable object **effects** - V1 Support callback function`$` selector - V2 Remove`$`selector ## Protocol layer differences > This mainly refers to the difference in the JSON Schema protocol **editable** - V1 is directly in the Schema description, indicating whether the field can be edited - V2 Renamed x-editable **visible** - V1 Indicates whether the field is displayed - V2 Renamed x-visible **display** - V1 Represent whether the field is displayed or not, if it is false, it represents the hidden behavior without deleting the value - V2 Renamed x-display, which represents the field display mode, and the value is`"none" | "visible" | "hidden"` **triggerType** - V1 Represent the field verification timing - V2 Remove, please use`x-validator:[{triggerType:"onBlur",validator:()=>...}]` **x-props** - V1 Represents the FormItem property - V2 Remove, please use x-decorator-props **x-rules** - V1 Represent field verification rules - V2 Renamed x-validator **x-linkages** - V1 Represent field linkage - V2 Remove, use x-reactions uniformly **x-mega-props** - V1 Represent the sub-component properties of the MegaLayout component - V2 Remove ## Component library differences In Formily 1.x, we mainly use @formily/antd and @formily/antd-components, or @formily/next and @formily/next-components. In V2, we have the following changes: - @formily/antd and @formily/antd-components were merged into @formily/antd, and the directory structure was changed to that of a pure component library. - The internal API of @formily/react @formily/core will no longer be exported. - Almost all components have been rewritten and cannot be smoothly upgraded. - Remove styled-components. ================================================ FILE: docs/guide/upgrade.zh-CN.md ================================================ # V2 升级指南 这里着重提一下,Formily2 相比于 Formily1.x,差别非常大,存在大量 Break Change。 所以对老用户而言,基本上是需要重新学习的,V1 和 V2 是无法做到平滑升级的。 但是 Formily2 的项目初衷就是为了降低大家的学习成本,因为老用户本身已经对 Formily 的核心思想有过一定的了解,为了帮助老用户更快速的学习 Formily2,本文会列举出 V1 和 V2 的核心差异点,并不会列举新增的能力。 ## 内核差异 > 这里主要指@formily/core 的差异 因为 Formily1.x 用户在使用内核 API 的时候,主要是使用 setFieldState/setFormState 与 getFieldState/getFormState,在 V2 中保留了这些 API,但是内部的模型属性是有语义上的差别的,差别如下: **modified** - V1: 代表字段是否已改动,其实并没有任何用处,因为字段初始化就代表已改动 - V2: 代表字段是否被手动修改,也就是组件触发 onChange 事件的时候才会设置为 true **inputed** - V1: 代表字段是否被手动修改 - V2: 移除,统一使用 modified **pristine** - V1: 代表字段 value 是否等于 initialValue - V2: 移除,用户手动判断,该属性会导致大量脏检查 **display** - V1: 代表字段是否显示,如果为 false,不会移除字段值 - V2: 代表字段展示模式,值为`"none" | "visible" | "hidden"` **touched** - V1: 冗余字段 - V2: 移除 **validating** - V1: 代表字段是否正在校验 - V2: 移除,统一使用 validateStatus **effectErrors/effectWarnings** - V1: 代表用户手动操作的 errors 和 warnings - V2: 移除,统一使用 feedbacks **ruleErrors/ruleWarnings** - V1: 代表校验器校验操作的 errors 与 warnings - V2: 移除,统一使用 feedbacks **values** - V1: 代表 onChange 事件返回的所有参数 - V2: 移除,统一使用 inputValues **rules** - V1:代表校验规则 - V2:移除,统一使用 validator,因为 rules 的字面意思是规则,但是规则的含义很大,不局限于校验规则 **props** - V1:代表组件的扩展属性,定位很不清晰,在纯 JSX 场景是代表组件属性与 FormItem 属性的集合,在 Schema 场景又是代表 Schema 字段的属性 - V2: 移除,统一使用 decorator 和 component **VirtualField** - V1: 代表虚拟字段 - V2: 改名,统一使用[VoidField](https://core.formilyjs.org/zh-CN/api/models/void-field) **unmount 行为** - V1: 字段 unmount,字段值默认会被删除 - V2: 移除,这个默认行为太隐晦,如果要删值,可以直接修改 value,同时自动删值的行为只有字段 display 为 none 时才会自动删值 ## 桥接层差异 > 这里主要指@formily/react 和@formily/react-schema-renderer 的差异 **createFormActions/createAsyncFormActions** - V1 创建一个 Form 操作器,可以调用 setFieldState/setFormState 方法 - V2 移除,统一使用@formily/core 中的[createForm](https://core.formilyjs.org/zh-CN/api/entry/create-form)创建出来的 Form 实例操作状态 **Form** - V1 内部会创建 Form 实例,可以受控传递 values/initialValues 属性等 - V2 移除,统一使用[FormProvider](https://react.formilyjs.org/zh-CN/api/components/form-provider) **SchemaForm** - V1 内部会解析 json-schema 协议,同时会创建 Form 实例,支持受控模式,并渲染 - V2 移除,统一使用[createSchemaField](https://react.formilyjs.org/zh-CN/api/components/schema-field)创建出来的 SchemaField 组件,且不支持受控模式 **Field** - V1 支持受控模式,需要使用 render props 进行组件状态映射 - V2 不支持受控模式,传入 decorator/component 属性即可快速实现状态映射 **VirtualField** - V1 支持受控模式,需要使用 render props 进行组件状态映射 - V2 不支持受控模式,改名[VoidField](https://react.formilyjs.org/zh-CN/api/components/void-field),传入 decorator/component 属性即可快速实现状态映射 **FieldList** - V1 代表自增字段控制组件 - V2 改名为[ArrayField](https://react.formilyjs.org/zh-CN/api/components/array-field) **FormSpy** - V1 监听所有生命周期触发,并重新渲染 - V2 移除,统一使用[FormConsumer](https://react.formilyjs.org/zh-CN/api/components/form-consumer) **SchemaMarkupField** - V1 代表 Schema 描述标签组件 - V2 移除,统一使用[createSchemaField](https://react.formilyjs.org/zh-CN/api/components/schema-field)工厂函数创建出来的描述标签组件 **useFormQuery** - V1 用于实现表单查询的快捷 Hook,支持中间件机制 - V2 暂时移除 **useForm** - V1 代表创建 Form 实例 - V2 代表消费上下文中的 Form 实例,如果要创建,请使用[createForm](https://core.formilyjs.org/zh-CN/api/entry/create-form) **useField** - V1 代表创建 Field 实例 - V2 代表消费上下文中的 Field 实例,如果要创建,请调用[form.createField](https://core.formilyjs.org/zh-CN/api/models/form#createfield) **useVirtualField** - V1 代表创建 VirtualField 实例 - V2 移除,如果要创建,请调用[form.createVoidField](https://core.formilyjs.org/zh-CN/api/models/form#createvoidfield) **useFormState** - V1 消费上下文中的 Form 状态 - V2 移除,统一使用[useForm](https://react.formilyjs.org/zh-CN/api/hooks/use-form) **useFieldState** - V1 消费上下文中的 Field 状态 - V2 移除,统一使用[useField](https://react.formilyjs.org/zh-CN/api/hooks/use-field) **useFormSpy** - V1 创建生命周期监听器,并触发重新渲染 - V2 移除 **useSchemaProps** - V1 消费上下文中的 SchemaField 的 Props - V2 移除,统一使用[useFieldSchema](https://react.formilyjs.org/zh-CN/api/hooks/use-field-schema) **connect** - V1 标准 HOC - V2 高阶函数改为 1 阶,属性有巨大变化,具体看[connect 文档](https://react.formilyjs.org/zh-CN/api/shared/connect) **registerFormField/registerVirtaulBox/registerFormComponent/registerFormItemComponent** - V1 全局注册组件 - V2 移除,不再支持全局注册 **FormEffectHooks** - V1 RxJS 生命周期钩子 - V2 移除,统一从@formily/core 中导出,且不会返回 RxJS Observable 对象 **effects** - V1 支持回调函数`$`选择器 - V2 移除`$`选择器 ## 协议层差异 > 这里主要指 JSON Schema 协议上的差异 **editable** - V1 直接在 Schema 描述中,代表字段是否可编辑 - V2 改名 x-editable **visible** - V1 代表字段是否显示 - V2 改名 x-visible **display** - V1 代表字段是否显示,如果为 false,代表不删值的隐藏行为 - V2 改名 x-display,代表字段展示模式,值为`"none" | "visible" | "hidden"` **triggerType** - V1 代表字段校验时机 - V2 移除,请使用`x-validator:[{triggerType:"onBlur",validator:()=>...}]` **x-props** - V1 代表 FormItem 属性 - V2 移除,请使用 x-decorator-props **x-rules** - V1 代表字段校验规则 - V2 改名 x-validator **x-linkages** - V1 代表字段联动 - V2 移除,统一使用 x-reactions **x-mega-props** - V1 代表 MegaLayout 组件的子组件属性 - V2 移除 ## 组件库差异 在 Formily1.x 中,我们主要使用@formily/antd 和@formily/antd-components,或者@formily/next 和@formily/next-components, 在 V2 中,我们有以下几个改变: - @formily/antd 与@formily/antd-components 合并成@formily/antd,同时目录结构全部改成纯组件库的目录结构了。 - 不会再导出@formily/react @formily/core 的内部 API - 所有组件几乎都做了重写,无法平滑升级 - 移除 styled-components ================================================ FILE: docs/index.md ================================================ --- title: Formily - Alibaba unified front-end form solution order: 10 hero: title: Alibaba Formily desc: Alibaba Unified Front-end Form Solution actions: - text: Introduction link: /guide - text: Quick start link: /guide/quick-start features: - icon: https://img.alicdn.com/imgextra/i2/O1CN016i72sH1c5wh1kyy9U_!!6000000003550-55-tps-800-800.svg title: Easier to Use desc: Out of the box, rich cases - icon: https://img.alicdn.com/imgextra/i1/O1CN01bHdrZJ1rEOESvXEi5_!!6000000005599-55-tps-800-800.svg title: More Efficient desc: Fool writing, ultra-high performance - icon: https://img.alicdn.com/imgextra/i3/O1CN01xlETZk1G0WSQT6Xii_!!6000000000560-55-tps-800-800.svg title: More Professional desc: Complete, flexible and elegant footer: Open-source MIT Licensed | Copyright © 2019-present
Powered by self --- ```tsx /** * inline: true */ import React from 'react' import { Section } from './site/Section' import './site/styles.less' export default () => (
) ``` ```tsx /** * inline: true */ import React from 'react' import { Section } from './site/Section' import './site/styles.less' export default () => (
) ``` ```tsx /** * inline: true */ import React from 'react' import { Section } from './site/Section' import './site/styles.less' export default () => (
) ``` ```tsx /** * inline: true */ import React from 'react' import { Section } from './site/Section' import { Contributors } from './site/Contributors' import './site/styles.less' export default () => (
) ``` ```tsx /** * inline: true */ import React from 'react' import { Section } from './site/Section' import { QrCode, QrCodeGroup } from './site/QrCode' import './site/styles.less' export default () => (
) ``` ================================================ FILE: docs/index.zh-CN.md ================================================ --- title: Formily - 阿里巴巴统一前端表单解决方案 order: 10 hero: title: Alibaba Formily desc: 阿里巴巴统一前端表单解决方案 actions: - text: 查看文档 link: /zh-CN/guide - text: 快速开始 link: /zh-CN/guide/quick-start features: - icon: https://img.alicdn.com/imgextra/i2/O1CN016i72sH1c5wh1kyy9U_!!6000000003550-55-tps-800-800.svg title: 更易用 desc: 开箱即用,案例丰富 - icon: https://img.alicdn.com/imgextra/i1/O1CN01bHdrZJ1rEOESvXEi5_!!6000000005599-55-tps-800-800.svg title: 更高效 desc: 傻瓜写法,超高性能 - icon: https://img.alicdn.com/imgextra/i3/O1CN01xlETZk1G0WSQT6Xii_!!6000000000560-55-tps-800-800.svg title: 更专业 desc: 完备,灵活,优雅 footer: Open-source MIT Licensed | Copyright © 2019-present
Powered by self --- ```tsx /** * inline: true */ import React from 'react' import { Section } from './site/Section' import './site/styles.less' export default () => (
) ``` ```tsx /** * inline: true */ import React from 'react' import { Section } from './site/Section' import './site/styles.less' export default () => (
) ``` ```tsx /** * inline: true */ import React from 'react' import { Section } from './site/Section' import './site/styles.less' export default () => (
) ``` ```tsx /** * inline: true */ import React from 'react' import { Section } from './site/Section' import { Contributors } from './site/Contributors' import './site/styles.less' export default () => (
) ``` ```tsx /** * inline: true */ import React from 'react' import { Section } from './site/Section' import { QrCode, QrCodeGroup } from './site/QrCode' import './site/styles.less' export default () => (
) ``` ================================================ FILE: docs/site/Contributors.less ================================================ .contri-list { display: flex; flex-wrap: wrap; .contri-user { display: flex; flex-direction: column; width: 120px; height: 120px; align-items: center; justify-content: center; &-avatar { display: block; width: 60px; height: 60px; border-radius: 60px; overflow: hidden; transition: all 0.15s ease-in-out; &:hover { opacity: 0.8; } } &-info { text-align: center; } } } ================================================ FILE: docs/site/Contributors.tsx ================================================ import React, { useEffect, useState } from 'react' import './Contributors.less' export const Contributors: React.FC = () => { const [contributors, setContributors] = useState([]) useEffect(() => { fetch('//formilyjs.org/.netlify/functions/contributors') .then((res) => res.json()) .then(({ data }) => { setContributors(data) }) }, []) return (
{contributors.map((user, key) => ( ))}
) } ================================================ FILE: docs/site/QrCode.less ================================================ .qrcode-group { display: flex; justify-content: center; .qrcode { width: 400px; margin: 20px; display: flex; flex-direction: column; justify-content: center; align-items: center; &-title { font-size: 20px; position: relative; &-content { position: absolute; left: 50%; bottom: 0; white-space: nowrap; transform: translateX(-50%); } } } } ================================================ FILE: docs/site/QrCode.tsx ================================================ import React from 'react' import './QrCode.less' export interface IQrCodeProps { title?: React.ReactNode link?: string } export const QrCode: React.FC> = ( props ) => { return (
{props.title}
) } export const QrCodeGroup: React.FC = (props) => (
{props.children}
) ================================================ FILE: docs/site/Section.less ================================================ .site-section { &-title { font-size: 50px; text-align: center; padding-bottom: 200px; position: relative; color: #45124e; @media (max-width: 480px) { font-size: 30px; } } } ================================================ FILE: docs/site/Section.tsx ================================================ import React from 'react' import './Section.less' export interface ISectionProps { title?: React.ReactNode style?: React.CSSProperties titleStyle?: React.CSSProperties scale?: number } export const Section: React.FC> = ( props ) => { return (
{props.title}
{props.children}
) } ================================================ FILE: docs/site/styles.less ================================================ .site-section { .codesandbox { width: 100%; height: 500px; border: 0; border-radius: 4px; overflow: hidden; box-shadow: 0 10px 30px #555; } img { width: 100%; border-radius: 4px; } } #root { overflow: hidden; } ================================================ FILE: global.config.ts ================================================ import prettyFormat from 'pretty-format' global['prettyFormat'] = prettyFormat global['sleep'] = (time: number) => { return new Promise((resolve) => setTimeout(resolve, time)) } global['requestAnimationFrame'] = (fn: () => void) => setTimeout(fn, 0) global['document'].documentElement.style['grid-column-gap'] = true ================================================ FILE: jest.config.js ================================================ module.exports = { collectCoverage: true, verbose: true, testEnvironment: 'jsdom', preset: 'ts-jest', testMatch: ['**/__tests__/**/*.spec.[jt]s?(x)'], setupFilesAfterEnv: [ require.resolve('jest-dom/extend-expect'), './global.config.ts', ], // moduleNameMapper: process.env.TEST_ENV === 'production' ? undefined : alias, globals: { 'ts-jest': { babelConfig: false, tsconfig: './tsconfig.jest.json', diagnostics: false, }, }, coveragePathIgnorePatterns: [ '/node_modules/', '/__tests__/', '/esm/', '/lib/', 'package.json', '/demo/', '/packages/builder/src/__tests__/', '/packages/builder/src/components/', '/packages/builder/src/configs/', 'package-lock.json', ], } ================================================ FILE: lerna.json ================================================ { "version": "2.3.7", "npmClient": "yarn", "useWorkspaces": true, "npmClientArgs": [ "--ignore-engines" ], "command": { "version": { "forcePublish": true, "exact": true, "message": "chore(release): 😊 publish %s" } } } ================================================ FILE: package.json ================================================ { "name": "root", "private": true, "devEngines": { "node": "8.x || 9.x || 10.x || 11.x" }, "workspaces": [ "packages/*", "devtools/*" ], "scripts": { "build": "rimraf -rf packages/*/{lib,dist,esm} && lerna run build", "build:docs": "dumi build", "start": "dumi dev", "test": "jest --coverage", "test:reactive": "jest packages/reactive/", "test:validator": "jest packages/validator/", "test:core": "jest packages/core/", "test:core:watch": "npm run test:core --- --watch", "test:schema": "jest packages/json-schema/", "test:schema:watch": "npm run test:schema --- --watch", "test:react": "jest packages/react/", "test:shared": "jest packages/shared/", "test:path": "jest packages/path/", "test:react:watch": "npm run test:react --- --watch", "test:vue": "jest packages/vue/", "test:vue:watch": "npm run test:vue --- --watch", "test:reactive-vue": "jest packages/reactive-vue/", "test:reactive-vue:watch": "npm run test:reactive-vue --- --watch", "test:antd": "jest packages/antd/", "test:next": "jest packages/next/", "test:watch": "jest --watch", "test:prod": "jest --coverage --silent", "preversion": "yarn install --ignore-engines && git add -A && npm run build && npm run lint && npm run test", "version:alpha": "lerna version prerelease --preid alpha", "version:beta": "lerna version prerelease --preid beta", "version:rc": "lerna version prerelease --preid rc", "version:patch": "lerna version patch", "version:minor": "lerna version minor", "version:preminor": "lerna version preminor --preid beta", "version:major": "lerna version major", "release:force": "lerna publish from-package --yes", "lint": "eslint ." }, "resolutions": { "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@mapbox/hast-util-to-jsx": "~1.0.0", "yargs": "^16.x", "commander": "^6.x", "ttypescript": "1.5.15" }, "devDependencies": { "@alifd/next": "^1.19.1", "@commitlint/cli": "^14.1.0", "@commitlint/config-conventional": "^14.1.0", "@commitlint/prompt-cli": "^14.1.0", "@netlify/functions": "^0.7.2", "@rollup/plugin-commonjs": "^17.0.0", "@testing-library/jest-dom": "^5.0.0", "@testing-library/react": "^11.2.3", "@testing-library/vue": "^5.6.2", "@types/fs-extra": "^8.1.0", "@types/hoist-non-react-statics": "^3.3.1", "@types/jest": "^24.0.18", "@types/node": "^12.6.8", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/react-is": "^18.3.0", "@typescript-eslint/eslint-plugin": "^4.9.1", "@typescript-eslint/parser": "^4.8.2", "@umijs/plugin-sass": "^1.1.1", "@vue/test-utils": "1.0.0-beta.22", "antd": "^4.0.0", "axios": "^1.6.0", "chalk": "^2.4.2", "chokidar": "^2.1.2", "concurrently": "^4.1.0", "conventional-commit-types": "^2.2.0", "cool-path": "^1.0.6", "cross-env": "^5.2.1", "css-loader": "^5.0.0", "cz-conventional-changelog": "^2.1.0", "dumi": "^1.1.53", "escape-string-regexp": "^4.0.0", "eslint": "^7.14.0", "eslint-config-prettier": "^7.0.0", "eslint-plugin-import": "^2.13.0", "eslint-plugin-markdown": "^2.0.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^3.1.0", "eslint-plugin-promise": "^4.0.0", "eslint-plugin-react": "^7.14.2", "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-vue": "^7.0.1", "execa": "^5.0.0", "file-loader": "^5.0.2", "findup": "^0.1.5", "fs-extra": "^7.0.1", "ghooks": "^2.0.4", "glob": "^7.1.3", "html-webpack-plugin": "^3.2.0", "immutable": "^4.0.0-rc.12", "istanbul-api": "^2.1.1", "istanbul-lib-coverage": "^2.0.3", "jest": "^26.0.0", "jest-codemods": "^0.19.1", "jest-dom": "^3.1.2", "jest-localstorage-mock": "^2.3.0", "jest-styled-components": "6.3.3", "jest-watch-lerna-packages": "^1.1.0", "lerna": "^4.0.0", "less": "^4.1.1", "less-loader": "^5.0.0", "less-plugin-npm-import": "^2.1.0", "lint-staged": "^8.2.1", "mfetch": "^0.2.27", "mobx": "^6.0.4", "mobx-react-lite": "^3.1.6", "onchange": "^5.2.0", "opencollective": "^1.0.3", "opencollective-postinstall": "^2.0.2", "param-case": "^3.0.4", "postcss": "^8.0.0", "prettier": "^2.2.1", "pretty-format": "^24.0.0", "pretty-quick": "^3.1.0", "querystring": "^0.2.1", "raw-loader": "^4.0.0", "react": "^18.0.0", "react-dom": "^18.0.0", "react-mde": "^11.5.0", "react-test-renderer": "^16.11.0", "rimraf": "^3.0.0", "rollup": "^2.37.1", "rollup-plugin-dts": "^2.0.0", "rollup-plugin-external-globals": "^0.6.1", "rollup-plugin-inject-process-env": "^1.3.1", "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-postcss": "^4.0.0", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.35.0", "semver": "^7.3.5", "semver-regex": "^3.1.3", "showdown": "^1.9.1", "staged-git-files": "^1.1.2", "string-similarity": "^4.0.4", "style-loader": "^1.1.3", "styled-components": "^5.0.0", "ts-import-plugin": "1.6.1", "ts-jest": "^26.0.0", "ts-loader": "^7.0.4", "ts-node": "^9.1.1", "typescript": "^4.1.5", "vue-eslint-parser": "^7.1.1", "webpack": "^4.41.5", "webpack-cli": "^3.3.10", "webpack-dev-server": "^3.10.1", "yup": "^1.4.0" }, "repository": { "type": "git", "url": "git+https://github.com/alibaba/formily.git" }, "config": { "ghooks": { "pre-commit": "lint-staged", "commit-msg": "commitlint --edit" } }, "lint-staged": { "*.{ts,tsx,js}": [ "eslint --ext .ts,.tsx,.js", "pretty-quick --staged", "git add" ], "*.md": [ "pretty-quick --staged", "git add" ] }, "collective": { "type": "opencollective", "url": "https://opencollective.com/formily" }, "dependencies": { "@ant-design/icons": "^4.0.2" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } ================================================ FILE: packages/.eslintrc ================================================ { "parser": "@typescript-eslint/parser", "extends": [ "plugin:react/recommended", "plugin:@typescript-eslint/recommended", "prettier/@typescript-eslint" ], "env": { "node": true }, "plugins": ["@typescript-eslint", "react", "prettier", "markdown"], "parserOptions": { "sourceType": "module", "ecmaVersion": 10, "ecmaFeatures": { "jsx": true } }, "settings": { "react": { "version": "detect" } }, "rules": { "prettier/prettier": 0, // don't force es6 functions to include space before paren "space-before-function-paren": 0, "react/prop-types": 0, "react/no-find-dom-node": 0, "react/display-name": 0, // allow specifying true explicitly for boolean props "react/jsx-boolean-value": 0, "react/no-did-update-set-state": 0, "react/no-unescaped-entities": "off", // maybe we should no-public "@typescript-eslint/explicit-member-accessibility": 0, "@typescript-eslint/interface-name-prefix": 0, "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/explicit-function-return-type": 0, "@typescript-eslint/no-parameter-properties": 0, "@typescript-eslint/array-type": 0, "@typescript-eslint/no-object-literal-type-assertion": 0, "@typescript-eslint/no-use-before-define": 0, "@typescript-eslint/no-unused-vars": 1, "@typescript-eslint/no-namespace": 0, "@typescript-eslint/ban-ts-comment": 0, "@typescript-eslint/ban-types": 0, "@typescript-eslint/adjacent-overload-signatures": 0, "@typescript-eslint/explicit-module-boundary-types": 0, "@typescript-eslint/triple-slash-reference": 0, "@typescript-eslint/no-empty-function": 0, "no-console": [ "error", { "allow": ["warn", "error", "info"] } ], "prefer-const": 0, "no-var": 1, "prefer-rest-params": 0 }, "overrides": [ { "files": ["**/*.md.{jsx,tsx}"], "processor": "markdown/markdown" }, { "files": ["**/*.md/*.{jsx,tsx}"], "rules": { "@typescript-eslint/no-unused-vars": "error", "no-unused-vars": "error", "no-console": "off", "react/display-name": "off", "react/prop-types": "off" } }, { "files": ["**/*.md/*.{js,ts}"], "rules": { "@typescript-eslint/no-unused-vars": "off", "no-unused-vars": "off", "no-console": "off", "react/display-name": "off", "react/prop-types": "off" } } ] } ================================================ FILE: packages/antd/.npmignore ================================================ node_modules *.log build docs doc-site __tests__ .eslintrc jest.config.js tsconfig.json .umi src ================================================ FILE: packages/antd/.umirc.js ================================================ import { resolve } from 'path' export default { mode: 'site', logo: '//img.alicdn.com/imgextra/i2/O1CN01Kq3OHU1fph6LGqjIz_!!6000000004056-55-tps-1141-150.svg', title: 'Ant Design', hash: true, favicon: '//img.alicdn.com/imgextra/i3/O1CN01XtT3Tv1Wd1b5hNVKy_!!6000000002810-55-tps-360-360.svg', outputPath: './doc-site', locales: [ ['en-US', 'English'], ['zh-CN', '中文'], ], navs: { 'zh-CN': [ { title: 'Ant Design', path: '/zh-CN/components', }, { title: '主站', path: 'https://formilyjs.org', }, { title: 'GITHUB', path: 'https://github.com/alibaba/formily', }, ], 'en-US': [ { title: 'Ant Design', path: '/components', }, { title: 'Home Site', path: 'https://formilyjs.org', }, { title: 'GITHUB', path: 'https://github.com/alibaba/formily', }, ], }, links: [ { rel: 'stylesheet', href: 'https://esm.sh/antd@4.x/dist/antd.css', }, ], headScripts: [ ` function loadAd(){ var header = document.querySelector('.__dumi-default-layout-content .markdown h1') if(header && !header.querySelector('#_carbonads_js')){ var script = document.createElement('script') script.src = '//cdn.carbonads.com/carbon.js?serve=CEAICK3M&placement=formilyjsorg' script.id = '_carbonads_js' script.classList.add('head-ad') header.appendChild(script) } } var request = null var observer = new MutationObserver(function(){ cancelIdleCallback(request) request = requestIdleCallback(loadAd) }) document.addEventListener('DOMContentLoaded',function(){ loadAd() observer.observe( document.body, { childList:true, subtree:true } ) }) `, ], styles: [ `.__dumi-default-navbar-logo{ height: 60px !important; width: 150px !important; padding-left:0 !important; color: transparent !important; } .__dumi-default-navbar{ padding: 0 28px !important; } .__dumi-default-layout-hero{ background-image: url(//img.alicdn.com/imgextra/i4/O1CN01ZcvS4e26XMsdsCkf9_!!6000000007671-2-tps-6001-4001.png); background-size: cover; background-repeat: no-repeat; padding: 120px 0 !important; } .__dumi-default-layout-hero h1{ color:#45124e !important; font-size:80px !important; padding-bottom: 30px !important; } .__dumi-default-dark-switch { display:none } nav a{ text-decoration: none !important; } #carbonads * { margin: initial; padding: initial; } #carbonads { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Helvetica, Arial, sans-serif; } #carbonads { display: flex; max-width: 330px; background-color: hsl(0, 0%, 98%); box-shadow: 0 1px 4px 1px hsla(0, 0%, 0%, 0.1); z-index: 100; float:right; } #carbonads a { color: inherit; text-decoration: none; } #carbonads a:hover { color: inherit; } #carbonads span { position: relative; display: block; overflow: hidden; } #carbonads .carbon-wrap { display: flex; } #carbonads .carbon-img { display: block; margin: 0; line-height: 1; } #carbonads .carbon-img img { display: block; } #carbonads .carbon-text { font-size: 13px; padding: 10px; margin-bottom: 16px; line-height: 1.5; text-align: left; } #carbonads .carbon-poweredby { display: block; padding: 6px 8px; background: #f1f1f2; text-align: center; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; font-size: 8px; line-height: 1; border-top-left-radius: 3px; position: absolute; bottom: 0; right: 0; } `, ], } ================================================ FILE: packages/antd/LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2015-present, Alibaba Group Holding Limited. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/antd/README.md ================================================ # @formily/antd ### Install ```bash npm install --save @formily/antd ``` ================================================ FILE: packages/antd/__tests__/moment.spec.ts ================================================ import moment from 'moment' import { formatMomentValue, momentable } from '../src/__builtins__/moment' test('momentable is usable', () => { expect(moment.isMoment(momentable('2021-09-08'))).toBe(true) expect( momentable(['2021-09-08', '2021-12-29']).every((item) => moment.isMoment(item) ) ).toBe(true) expect(momentable(0)).toBe(0) }) test('formatMomentValue is usable', () => { expect(formatMomentValue('', 'YYYY-MM-DD', '~')).toBe('~') expect(formatMomentValue('2021-12-21 15:47:00', 'YYYY-MM-DD')).toBe( '2021-12-21' ) expect(formatMomentValue('2021-12-21 15:47:00', undefined)).toBe( '2021-12-21 15:47:00' ) expect(formatMomentValue('2021-12-21 15:47:00', (date: string) => date)).toBe( '2021-12-21 15:47:00' ) expect(formatMomentValue('12:11', 'HH:mm')).toBe('12:11') expect(formatMomentValue('12:11:11', 'HH:mm:ss')).toBe('12:11:11') expect(formatMomentValue(['12:11'], ['HH:mm'])).toEqual(['12:11']) expect(formatMomentValue(['12:11:11'], ['HH:mm:ss'])).toEqual(['12:11:11']) expect(formatMomentValue(1663155911097, 'YYYY-MM-DD HH:mm:ss')).toBe( moment(1663155911097).format('YYYY-MM-DD HH:mm:ss') ) expect(formatMomentValue([1663155911097], ['YYYY-MM-DD HH:mm:ss'])).toEqual([ moment(1663155911097).format('YYYY-MM-DD HH:mm:ss'), ]) expect( formatMomentValue('2022-09-15T09:56:26.000Z', 'YYYY-MM-DD HH:mm:ss') ).toBe(moment('2022-09-15T09:56:26.000Z').format('YYYY-MM-DD HH:mm:ss')) expect( formatMomentValue(['2022-09-15T09:56:26.000Z'], ['YYYY-MM-DD HH:mm:ss']) ).toEqual([moment('2022-09-15T09:56:26.000Z').format('YYYY-MM-DD HH:mm:ss')]) expect(formatMomentValue('2022-09-15 09:56:26', 'HH:mm:ss')).toBe('09:56:26') expect(formatMomentValue(['2022-09-15 09:56:26'], ['HH:mm:ss'])).toEqual([ '09:56:26', ]) expect( formatMomentValue( ['2021-12-21 15:47:00', '2021-12-29 15:47:00'], 'YYYY-MM-DD' ) ).toEqual(['2021-12-21', '2021-12-29']) expect( formatMomentValue( ['2021-12-21 16:47:00', '2021-12-29 18:47:00'], (date: string) => date ) ).toEqual(['2021-12-21 16:47:00', '2021-12-29 18:47:00']) expect( formatMomentValue( ['2021-12-21 16:47:00', '2021-12-29 18:47:00'], ['YYYY-MM-DD', (date: string) => date] ) ).toEqual(['2021-12-21', '2021-12-29 18:47:00']) expect( formatMomentValue( ['2021-12-21 16:47:00', '2021-12-29 18:47:00'], ['YYYY-MM-DD', undefined] ) ).toEqual(['2021-12-21', '2021-12-29 18:47:00']) expect(formatMomentValue([undefined], 'YYYY-MM-DD', 'placeholder')).toEqual([ 'placeholder', ]) }) ================================================ FILE: packages/antd/__tests__/sideEffects.spec.ts ================================================ import SideEffectsFlagPlugin from 'webpack/lib/optimize/SideEffectsFlagPlugin' // eslint-disable-next-line @typescript-eslint/no-var-requires const { sideEffects, name: baseName } = require('../package.json') test('sideEffects should be controlled manually', () => { // if config in pkg.json changed, please ensure it is covered by jest. expect(sideEffects).toStrictEqual([ 'dist/*', 'esm/*.js', 'lib/*.js', 'src/*.ts', '*.less', '**/*/style.js', ]) }) test('dist/*', () => { // eg. import "@formily/antd/dist/antd.css" expect( SideEffectsFlagPlugin.moduleHasSideEffects('dist/antd.css', 'dist/*') ).toBeTruthy() expect( SideEffectsFlagPlugin.moduleHasSideEffects( 'dist/formily.antd.umd.development.js', 'dist/*' ) ).toBeTruthy() expect( SideEffectsFlagPlugin.moduleHasSideEffects( 'dist/formily.antd.umd.production.js', 'dist/*' ) ).toBeTruthy() }) test('esm/*.js & lib/*.js', () => { // expected to be truthy // eg. import FormilyAntd from "@formily/antd/esm/index" expect( SideEffectsFlagPlugin.moduleHasSideEffects('esm/index.js', 'esm/*.js') ).toBeTruthy() expect( SideEffectsFlagPlugin.moduleHasSideEffects('lib/index.js', 'lib/*.js') ).toBeTruthy() // expected to be falsy // eg. import Input from "@formily/antd/esm/input/index" => will be compiled to __webpack_require__("./node_modules/@formily/antd/esm/input/index.js") // It should be removed by webpack if not used after imported. expect( SideEffectsFlagPlugin.moduleHasSideEffects('esm/input/index.js', 'esm/*.js') ).toBeFalsy() expect( SideEffectsFlagPlugin.moduleHasSideEffects( 'esm/array-base/index.js', 'esm/*.js' ) ).toBeFalsy() expect( SideEffectsFlagPlugin.moduleHasSideEffects('lib/input/index.js', 'lib/*.js') ).toBeFalsy() }) test('*.less', () => { // eg. import "@formily/antd/lib/input/style.less" expect( SideEffectsFlagPlugin.moduleHasSideEffects( `${baseName}/lib/input/style.less`, '*.less' ) ).toBeTruthy() }) test('**/*/style.js', () => { // eg. import "@formily/antd/lib/input/style" will be compiled to __webpack_require__("./node_modules/@formily/antd/lib/input/style.js") // so we can match the `*style.js` only, not `**/*/style*` may be cause someting mismatch like `@formily/antd/lib/xxx-style/index.js` const modulePathArr = [ 'lib/input/style.js', `${baseName}/lib/input/style.js`, `./node_modules/${baseName}/style.js`, ] modulePathArr.forEach((modulePath) => { const hasSideEffects = SideEffectsFlagPlugin.moduleHasSideEffects( modulePath, '**/*/style.js' ) expect(hasSideEffects).toBeTruthy() }) }) ================================================ FILE: packages/antd/build-style.ts ================================================ import { build } from '../../scripts/build-style' build({ esStr: 'antd/es/', libStr: 'antd/lib/', allStylesOutputFile: 'dist/antd.css', }) ================================================ FILE: packages/antd/create-style.ts ================================================ import glob from 'glob' import path from 'path' import fs from 'fs-extra' glob( './*/style.less', { cwd: path.resolve(__dirname, './src') }, (err, files) => { if (err) return console.error(err) fs.writeFile( path.resolve(__dirname, './src/style.ts'), `// auto generated code ${files .map((path) => { return `import '${path}'\n` }) .join('')}`, 'utf8' ) fs.writeFile( path.resolve(__dirname, './src/style.less'), `// auto generated code ${files .map((path) => { return `@import '${path}';\n` }) .join('')}`, 'utf8' ) } ) ================================================ FILE: packages/antd/docs/components/ArrayCards.md ================================================ # ArrayCards > Card list, it is more suitable to use ArrayCards for scenarios with a large number of fields in each row and more linkages > > Note: This component is only applicable to Schema scenarios ## Markup Schema example ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() export default () => { return ( Submit ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, 'x-decorator': 'FormItem', 'x-component-props': { title: 'String array', }, items: { type: 'void', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCards.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, 'x-decorator': 'FormItem', 'x-component-props': { title: 'Object array', }, items: { type: 'object', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCards.Addition', }, }, }, }, } export default () => { return ( Submit ) } ``` ## Effects linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/antd' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm({ effects: () => { //Active linkage mode onFieldChange('array.*.aa', ['value'], (field, form) => { form.setFieldState(field.query('.bb'), (state) => { state.visible = field.value != '123' }) }) //Passive linkage mode onFieldReact('array.*.dd', (field) => { field.visible = field.query('.cc').get('value') != '123' }) }, }) export default () => { return ( Submit ) } ``` ## JSON Schema linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, title: 'Object array', items: { type: 'object', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, aa: { type: 'string', 'x-decorator': 'FormItem', title: 'AA', required: true, 'x-component': 'Input', description: 'Enter 123', }, bb: { type: 'string', title: 'BB', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-reactions': [ { dependencies: ['.aa'], when: "{{$deps[0] != '123'}}", fulfill: { schema: { title: 'BB', 'x-disabled': true, }, }, otherwise: { schema: { title: 'Changed', 'x-disabled': false, }, }, }, ], }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCards.Addition', }, }, }, }, } export default () => { return ( Submit ) } ``` ## API ### ArrayCards Extended attributes | Property name | Type | Description | Default value | | ------------- | ------------------------- | --------------- | ------------- | | onAdd | `(index: number) => void` | add method | | | onRemove | `(index: number) => void` | remove method | | | onCopy | `(index: number) => void` | copy method | | | onMoveUp | `(index: number) => void` | moveUp method | | | onMoveDown | `(index: number) => void` | moveDown method | | Other Reference https://ant.design/components/card-cn/ ### ArrayCards.Addition > Add button Extended attributes | Property name | Type | Description | Default value | | ------------- | -------------------- | ------------- | ------------- | | title | ReactText | Copywriting | | | method | `'push' \|'unshift'` | add method | `'push'` | | defaultValue | `any` | Default value | | Other references https://ant.design/components/button-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayCards.Copy > Copy button Extended attributes | Property name | Type | Description | Default value | | ------------- | -------------------- | ----------- | ------------- | | title | ReactText | Copywriting | | | method | `'push' \|'unshift'` | Copy method | `'push'` | Other references https://ant.design/components/button-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayCards.Remove > Delete button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayCards.MoveDown > Move down button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayCards.MoveUp > Move up button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayCards.Index > Index Renderer No attributes ### ArrayCards.useIndex > Read the React Hook of the current rendering row index ### ArrayCards.useRecord > Read the React Hook of the current rendering row ================================================ FILE: packages/antd/docs/components/ArrayCards.zh-CN.md ================================================ # ArrayCards > 卡片列表,对于每行字段数量较多,联动较多的场景比较适合使用 ArrayCards > > 注意:该组件只适用于 Schema 场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() export default () => { return ( 提交 ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, 'x-decorator': 'FormItem', 'x-component-props': { title: '字符串数组', }, items: { type: 'void', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCards.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, 'x-decorator': 'FormItem', 'x-component-props': { title: '对象数组', }, items: { type: 'object', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCards.Addition', }, }, }, }, } export default () => { return ( 提交 ) } ``` ## Effects 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/antd' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm({ effects: () => { //主动联动模式 onFieldChange('array.*.aa', ['value'], (field, form) => { form.setFieldState(field.query('.bb'), (state) => { state.visible = field.value != '123' }) }) //被动联动模式 onFieldReact('array.*.dd', (field) => { field.visible = field.query('.cc').get('value') != '123' }) }, }) export default () => { return ( 提交 ) } ``` ## JSON Schema 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, title: '对象数组', items: { type: 'object', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, aa: { type: 'string', 'x-decorator': 'FormItem', title: 'AA', required: true, 'x-component': 'Input', description: '输入123', }, bb: { type: 'string', title: 'BB', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-reactions': [ { dependencies: ['.aa'], when: "{{$deps[0] != '123'}}", fulfill: { schema: { title: 'BB', 'x-disabled': true, }, }, otherwise: { schema: { title: 'Changed', 'x-disabled': false, }, }, }, ], }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCards.Addition', }, }, }, }, } export default () => { return ( 提交 ) } ``` ## API ### ArrayCards 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ---------- | ------------------------- | ------------ | ------ | | onAdd | `(index: number) => void` | 增加方法 | | | onRemove | `(index: number) => void` | 删除方法 | | | onCopy | `(index: number) => void` | 复制方法 | | | onMoveUp | `(index: number) => void` | 向上移动方法 | | | onMoveDown | `(index: number) => void` | 向下移动方法 | | 其余参考 https://ant.design/components/card-cn/ ### ArrayCards.Addition > 添加按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | | defaultValue | `any` | 默认值 | | 其余参考 https://ant.design/components/button-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCards.Copy > 复制按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | 其余参考 https://ant.design/components/button-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCards.Remove > 删除按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCards.MoveDown > 下移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCards.MoveUp > 上移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCards.Index > 索引渲染器 无属性 ### ArrayCards.useIndex > 读取当前渲染行索引的 React Hook ### ArrayCards.useRecord > 读取当前渲染记录的 React Hook ================================================ FILE: packages/antd/docs/components/ArrayCollapse.md ================================================ # ArrayCollapse > Folding panel, it is more suitable to use ArrayCollapse for scenes with more fields in each row and more linkage > > Note: This component is only applicable to Schema scenarios ## Markup Schema example ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() export default () => { return ( Submit ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'void', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { header: 'String array', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCollapse.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { header: 'Object array', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCollapse.Addition', }, }, }, array_unshift: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { header: 'Object array', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry (unshift)', 'x-component': 'ArrayCollapse.Addition', 'x-component-props': { method: 'unshift', }, }, }, }, }, } export default () => { return ( Submit ) } ``` ## Effects linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/antd' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm({ effects: () => { //Active linkage mode onFieldChange('array.*.aa', ['value'], (field, form) => { form.setFieldState(field.query('.bb'), (state) => { state.visible = field.value != '123' }) }) //Passive linkage mode onFieldReact('array.*.dd', (field) => { field.visible = field.query('.cc').get('value') != '123' }) }, }) export default () => { return ( Submit ) } ``` ## JSON Schema linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, title: 'Object array', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { header: 'Object array', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, aa: { type: 'string', 'x-decorator': 'FormItem', title: 'AA', required: true, 'x-component': 'Input', description: 'Enter 123', }, bb: { type: 'string', title: 'BB', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-reactions': [ { dependencies: ['.aa'], when: "{{$deps[0] != '123'}}", fulfill: { schema: { title: 'BB', 'x-disabled': true, }, }, otherwise: { schema: { title: 'Changed', 'x-disabled': false, }, }, }, ], }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCollapse.Addition', }, }, }, }, } export default () => { return ( Submit ) } ``` ## API ### ArrayCollapse Reference https://ant.design/components/collapse-cn/ ### ArrayCollapse.CollapsePanel Reference https://ant.design/components/collapse-cn/ ### ArrayCollapse.Addition > Add button Extended attributes | Property name | Type | Description | Default value | | ------------- | -------------------- | ------------- | ------------- | | title | ReactText | Copywriting | | | method | `'push' \|'unshift'` | add method | `'push'` | | defaultValue | `any` | Default value | | Other references https://ant.design/components/button-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayCollapse.Remove > Delete button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayCollapse.MoveDown > Move down button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayCollapse.MoveUp > Move up button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayCollapse.Index > Index Renderer No attributes ### ArrayCollapse.useIndex > Read the React Hook of the current rendering row index ### ArrayCollapse.useRecord > Read the React Hook of the current rendering row ================================================ FILE: packages/antd/docs/components/ArrayCollapse.zh-CN.md ================================================ # ArrayCollapse > 折叠面板,对于每行字段数量较多,联动较多的场景比较适合使用 ArrayCollapse > > 注意:该组件只适用于 Schema 场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() export default () => { return ( 提交 ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayCollapse', 'x-component-props': { onAdd: (index: number) => { console.log('Adding ' + index + ' item') }, }, maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'void', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { header: '字符串数组', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCollapse.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { header: '对象数组', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCollapse.Addition', }, }, }, array_unshift: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { header: '对象数组', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目(unshift)', 'x-component': 'ArrayCollapse.Addition', 'x-component-props': { method: 'unshift', }, }, }, }, }, } export default () => { return ( 提交 ) } ``` ## Effects 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/antd' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm({ effects: () => { //主动联动模式 onFieldChange('array.*.aa', ['value'], (field, form) => { form.setFieldState(field.query('.bb'), (state) => { state.visible = field.value != '123' }) }) //被动联动模式 onFieldReact('array.*.dd', (field) => { field.visible = field.query('.cc').get('value') != '123' }) }, }) export default () => { return ( 提交 ) } ``` ## JSON Schema 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, title: '对象数组', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { header: '对象数组', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, aa: { type: 'string', 'x-decorator': 'FormItem', title: 'AA', required: true, 'x-component': 'Input', description: '输入123', }, bb: { type: 'string', title: 'BB', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-reactions': [ { dependencies: ['.aa'], when: "{{$deps[0] != '123'}}", fulfill: { schema: { title: 'BB', 'x-disabled': true, }, }, otherwise: { schema: { title: 'Changed', 'x-disabled': false, }, }, }, ], }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCollapse.Addition', }, }, }, }, } export default () => { return ( 提交 ) } ``` ## API ### ArrayCollapse 参考 https://ant.design/components/collapse-cn/ ### ArrayCollapse.CollapsePanel 参考 https://ant.design/components/collapse-cn/ ### ArrayCollapse.Addition > 添加按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | | defaultValue | `any` | 默认值 | | 其余参考 https://ant.design/components/button-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCollapse.Remove > 删除按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCollapse.MoveDown > 下移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCollapse.MoveUp > 上移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCollapse.Index > 索引渲染器 无属性 ### ArrayCollapse.useIndex > 读取当前渲染行索引的 React Hook ### ArrayCollapse.useRecord > 读取当前渲染记录的 React Hook ================================================ FILE: packages/antd/docs/components/ArrayItems.md ================================================ # ArrayItems > Self-increment list, suitable for simple self-increment editing scenes, or for scenes with high space requirements > > Note: This component is only applicable to Schema scenarios ## Markup Schema example ```tsx import React from 'react' import { FormItem, Input, Editable, Select, DatePicker, ArrayItems, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, DatePicker, Editable, Space, Input, Select, ArrayItems, }, }) const form = createForm() export default () => { return ( { field.title = field.value?.input || field.title }} > Submit ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormItem, Editable, Input, Select, Radio, DatePicker, ArrayItems, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Editable, DatePicker, Space, Radio, Input, Select, ArrayItems, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', title: 'String array', items: { type: 'void', 'x-component': 'Space', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, input: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, properties: { add: { type: 'void', title: 'Add entry', 'x-component': 'ArrayItems.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', title: 'Object array', items: { type: 'object', properties: { space: { type: 'void', 'x-component': 'Space', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, date: { type: 'string', title: 'Date', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { style: { width: 160, }, }, }, input: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Input', }, select: { type: 'string', title: 'drop-down box', enum: [ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 160, }, }, }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, }, }, properties: { add: { type: 'void', title: 'Add entry', 'x-component': 'ArrayItems.Addition', }, }, }, array2: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', 'x-component-props': { style: { width: 300 } }, title: 'Object array', items: { type: 'object', 'x-decorator': 'ArrayItems.Item', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, input: { type: 'string', title: 'input box', 'x-decorator': 'Editable', 'x-component': 'Input', 'x-component-props': { bordered: false, }, }, config: { type: 'object', title: 'Configure complex data', 'x-component': 'Editable.Popover', 'x-reactions': '{{(field)=>field.title = field.value && field.value.input || field.title}}', properties: { date: { type: 'string', title: 'Date', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { style: { width: 160, }, }, }, input: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Input', }, select: { type: 'string', title: 'drop-down box', enum: [ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 160, }, }, }, }, }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, properties: { add: { type: 'void', title: 'Add entry', 'x-component': 'ArrayItems.Addition', }, }, }, }, } export default () => { return ( Submit ) } ``` ## Effects linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayItems, Editable, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Space, Editable, FormItem, Input, ArrayItems, }, }) const form = createForm({ effects: () => { //Active linkage mode onFieldChange('array.*.aa', ['value'], (field, form) => { form.setFieldState(field.query('.bb'), (state) => { state.visible = field.value != '123' }) }) //Passive linkage mode onFieldReact('array.*.dd', (field) => { field.visible = field.query('.cc').get('value') != '123' }) }, }) export default () => { return ( Submit ) } ``` ## JSON Schema linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayItems, Editable, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Space, Editable, FormItem, Input, ArrayItems, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', maxItems: 3, title: 'Object array', 'x-component-props': { style: { width: 300 } }, items: { type: 'object', 'x-decorator': 'ArrayItems.Item', properties: { left: { type: 'void', 'x-component': 'Space', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, index: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Index', }, }, }, edit: { type: 'void', 'x-component': 'Editable.Popover', title: 'Configuration data', properties: { aa: { type: 'string', 'x-decorator': 'FormItem', title: 'AA', required: true, 'x-component': 'Input', description: 'Enter 123', }, bb: { type: 'string', title: 'BB', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-reactions': [ { dependencies: ['.aa'], when: "{{$deps[0] != '123'}}", fulfill: { schema: { title: 'BB', 'x-disabled': true, }, }, otherwise: { schema: { title: 'Changed', 'x-disabled': false, }, }, }, ], }, }, }, right: { type: 'void', 'x-component': 'Space', properties: { remove: { type: 'void', 'x-component': 'ArrayItems.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayItems.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayItems.MoveDown', }, }, }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayItems.Addition', }, }, }, }, } export default () => { return ( Submit ) } ``` ## API ### ArrayItems Extended attributes | Property name | Type | Description | Default value | | ------------- | ------------------------- | --------------- | ------------- | | onAdd | `(index: number) => void` | add method | | | onRemove | `(index: number) => void` | remove method | | | onCopy | `(index: number) => void` | copy method | | | onMoveUp | `(index: number) => void` | moveUp method | | | onMoveDown | `(index: number) => void` | moveDown method | | Other Inherit HTMLDivElement Props ### ArrayItems.Item > List block Inherit HTMLDivElement Props Extended attributes | Property name | Type | Description | Default value | | ------------- | ------------------- | --------------------- | ------------- | | type | `'card' \|'divide'` | card or dividing line | | ### ArrayItems.SortHandle > Drag handle Reference https://ant.design/components/icon-cn/ ### ArrayItems.Addition > Add button Extended attributes | Property name | Type | Description | Default value | | ------------- | -------------------- | ------------- | ------------- | | title | ReactText | Copywriting | | | method | `'push' \|'unshift'` | add method | `'push'` | | defaultValue | `any` | Default value | | Other references https://ant.design/components/button-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayItems.Copy > Copy button Extended attributes | Property name | Type | Description | Default value | | ------------- | -------------------- | ----------- | ------------- | | title | ReactText | Copywriting | | | method | `'push' \|'unshift'` | Copy method | `'push'` | Other references https://ant.design/components/button-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayItems.Remove > Delete button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayItems.MoveDown > Move down button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayItems.MoveUp > Move up button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayItems.Index > Index Renderer No attributes ### ArrayItems.useIndex > Read the React Hook of the current rendering row index ### ArrayItems.useRecord > Read the React Hook of the current rendering row ================================================ FILE: packages/antd/docs/components/ArrayItems.zh-CN.md ================================================ # ArrayItems > 自增列表,对于简单的自增编辑场景比较适合,或者对于空间要求高的场景比较适合 > > 注意:该组件只适用于 Schema 场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormItem, Input, Editable, Select, DatePicker, ArrayItems, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, DatePicker, Editable, Space, Input, Select, ArrayItems, }, }) const form = createForm() export default () => { return ( { field.title = field.value?.input || field.title }} > 提交 ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormItem, Editable, Input, Select, Radio, DatePicker, ArrayItems, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Editable, DatePicker, Space, Radio, Input, Select, ArrayItems, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', title: '字符串数组', items: { type: 'void', 'x-component': 'Space', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, input: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, properties: { add: { type: 'void', title: '添加条目', 'x-component': 'ArrayItems.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', title: '对象数组', items: { type: 'object', properties: { space: { type: 'void', 'x-component': 'Space', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, date: { type: 'string', title: '日期', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { style: { width: 160, }, }, }, input: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'Input', }, select: { type: 'string', title: '下拉框', enum: [ { label: '选项1', value: 1 }, { label: '选项2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 160, }, }, }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, }, }, properties: { add: { type: 'void', title: '添加条目', 'x-component': 'ArrayItems.Addition', }, }, }, array2: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', 'x-component-props': { style: { width: 300 } }, title: '对象数组', items: { type: 'object', 'x-decorator': 'ArrayItems.Item', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, input: { type: 'string', title: '输入框', 'x-decorator': 'Editable', 'x-component': 'Input', 'x-component-props': { bordered: false, }, }, config: { type: 'object', title: '配置复杂数据', 'x-component': 'Editable.Popover', 'x-reactions': '{{(field)=>field.title = field.value && field.value.input || field.title}}', properties: { date: { type: 'string', title: '日期', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { style: { width: 160, }, }, }, input: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'Input', }, select: { type: 'string', title: '下拉框', enum: [ { label: '选项1', value: 1 }, { label: '选项2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 160, }, }, }, }, }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, properties: { add: { type: 'void', title: '添加条目', 'x-component': 'ArrayItems.Addition', }, }, }, }, } export default () => { return ( 提交 ) } ``` ## Effects 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayItems, Editable, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Space, Editable, FormItem, Input, ArrayItems, }, }) const form = createForm({ effects: () => { //主动联动模式 onFieldChange('array.*.aa', ['value'], (field, form) => { form.setFieldState(field.query('.bb'), (state) => { state.visible = field.value != '123' }) }) //被动联动模式 onFieldReact('array.*.dd', (field) => { field.visible = field.query('.cc').get('value') != '123' }) }, }) export default () => { return ( 提交 ) } ``` ## JSON Schema 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayItems, Editable, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Space, Editable, FormItem, Input, ArrayItems, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', maxItems: 3, title: '对象数组', 'x-component-props': { style: { width: 300 } }, items: { type: 'object', 'x-decorator': 'ArrayItems.Item', properties: { left: { type: 'void', 'x-component': 'Space', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, index: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Index', }, }, }, edit: { type: 'void', 'x-component': 'Editable.Popover', title: '配置数据', properties: { aa: { type: 'string', 'x-decorator': 'FormItem', title: 'AA', required: true, 'x-component': 'Input', description: '输入123', }, bb: { type: 'string', title: 'BB', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-reactions': [ { dependencies: ['.aa'], when: "{{$deps[0] != '123'}}", fulfill: { schema: { title: 'BB', 'x-disabled': true, }, }, otherwise: { schema: { title: 'Changed', 'x-disabled': false, }, }, }, ], }, }, }, right: { type: 'void', 'x-component': 'Space', properties: { remove: { type: 'void', 'x-component': 'ArrayItems.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayItems.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayItems.MoveDown', }, }, }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayItems.Addition', }, }, }, }, } export default () => { return ( 提交 ) } ``` ## API ### ArrayItems 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ---------- | ------------------------- | ------------ | ------ | | onAdd | `(index: number) => void` | 增加方法 | | | onRemove | `(index: number) => void` | 删除方法 | | | onCopy | `(index: number) => void` | 复制方法 | | | onMoveUp | `(index: number) => void` | 向上移动方法 | | | onMoveDown | `(index: number) => void` | 向下移动方法 | | 其余继承 HTMLDivElement Props ### ArrayItems.Item > 列表区块 继承 HTMLDivElement Props 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------ | -------------------- | -------------- | ------ | | type | `'card' \| 'divide'` | 卡片或者分割线 | | ### ArrayItems.SortHandle > 拖拽手柄 参考 https://ant.design/components/icon-cn/ ### ArrayItems.Addition > 添加按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | | defaultValue | `any` | 默认值 | | 其余参考 https://ant.design/components/button-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayItems.Copy > 复制按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | 其余参考 https://ant.design/components/button-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayItems.Remove > 删除按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayItems.MoveDown > 下移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayItems.MoveUp > 上移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayItems.Index > 索引渲染器 无属性 ### ArrayItems.useIndex > 读取当前渲染行索引的 React Hook ### ArrayItems.useRecord > 读取当前渲染记录的 React Hook ================================================ FILE: packages/antd/docs/components/ArrayTable.md ================================================ # ArrayTable > Self-increasing table, it is more suitable to use this component for scenes with a large amount of data. Although the amount of data is large to a certain extent, it will be a little bit stuck, but it will not affect the basic operation > > Note: This component is only applicable to Schema scenarios and can only be an array of objects ## Markup Schema example ```tsx import React from 'react' import { FormItem, Input, ArrayTable, Editable, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button, Alert } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Editable, Input, ArrayTable, }, }) const form = createForm() const range = (count: number) => Array.from(new Array(count)).map((_, key) => ({ aaa: key, })) export default () => { return ( Submit ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormItem, Input, ArrayTable, Editable, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Editable, Input, ArrayTable, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'ArrayTable', 'x-component-props': { pagination: { pageSize: 10 }, scroll: { x: '100%' }, }, items: { type: 'object', properties: { column1: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 50, title: 'Sort', align: 'center' }, properties: { sort: { type: 'void', 'x-component': 'ArrayTable.SortHandle', }, }, }, column2: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 80, title: 'Index', align: 'center' }, properties: { index: { type: 'void', 'x-component': 'ArrayTable.Index', }, }, }, column3: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A1' }, properties: { a1: { type: 'string', 'x-decorator': 'Editable', 'x-component': 'Input', }, }, }, column4: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A2' }, properties: { a2: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, column5: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A3' }, properties: { a3: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, column6: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Operations', dataIndex: 'operations', width: 200, fixed: 'right', }, properties: { item: { type: 'void', 'x-component': 'FormItem', properties: { remove: { type: 'void', 'x-component': 'ArrayTable.Remove', }, moveDown: { type: 'void', 'x-component': 'ArrayTable.MoveDown', }, moveUp: { type: 'void', 'x-component': 'ArrayTable.MoveUp', }, }, }, }, }, }, }, properties: { add: { type: 'void', 'x-component': 'ArrayTable.Addition', title: 'Add entry', }, }, }, }, } export default () => { return ( Submit ) } ``` ## Effects linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayTable, Switch, FormButtonGroup, Submit, } from '@formily/antd' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Switch, Input, Button, ArrayTable, }, }) const form = createForm({ effects: () => { //Active linkage mode onFieldChange('hideFirstColumn', ['value'], (field) => { field.query('array.column4').take((target) => { target.visible = !field.value }) field.query('array.*.a2').take((target) => { target.visible = !field.value }) }) //Passive linkage mode onFieldReact('array.*.a2', (field) => { field.visible = !field.query('.a1').get('value') }) }, }) export default () => { return ( A2', dataIndex: 'a1', width: 100, }} > Submit ) } ``` ## JSON Schema linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayTable, Switch, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Switch, Input, ArrayTable, }, }) const form = createForm() const schema = { type: 'object', properties: { hideFirstColumn: { type: 'boolean', title: 'Hide A2', 'x-decorator': 'FormItem', 'x-component': 'Switch', }, array: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'ArrayTable', 'x-component-props': { pagination: { pageSize: 10 }, scroll: { x: '100%' }, }, items: { type: 'object', properties: { column1: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 50, title: 'Sort', align: 'center' }, properties: { sort: { type: 'void', 'x-component': 'ArrayTable.SortHandle', }, }, }, column2: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 80, title: 'Index', align: 'center' }, properties: { index: { type: 'void', 'x-component': 'ArrayTable.Index', }, }, }, column3: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 100, title: 'Explicitly hidden->A2' }, properties: { a1: { type: 'boolean', 'x-decorator': 'FormItem', 'x-component': 'Switch', }, }, }, column4: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A2' }, 'x-reactions': [ { dependencies: ['hideFirstColumn'], when: '{{$deps[0]}}', fulfill: { schema: { 'x-visible': false, }, }, otherwise: { schema: { 'x-visible': true, }, }, }, ], properties: { a2: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', required: true, 'x-reactions': [ { dependencies: ['.a1', 'hideFirstColumn'], when: '{{$deps[1] || $deps[0]}}', fulfill: { schema: { 'x-visible': false, }, }, otherwise: { schema: { 'x-visible': true, }, }, }, ], }, }, }, column5: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A3' }, properties: { a3: { type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, column6: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Operations', dataIndex: 'operations', width: 200, fixed: 'right', }, properties: { item: { type: 'void', 'x-component': 'FormItem', properties: { remove: { type: 'void', 'x-component': 'ArrayTable.Remove', }, moveDown: { type: 'void', 'x-component': 'ArrayTable.MoveDown', }, moveUp: { type: 'void', 'x-component': 'ArrayTable.MoveUp', }, }, }, }, }, }, }, properties: { add: { type: 'void', 'x-component': 'ArrayTable.Addition', title: 'Add entry', }, }, }, }, } export default () => { return ( Submit ) } ``` ## Overwrite default behavior of build-in operations ```tsx /** * title: Markup Schema */ import React from 'react' import { FormItem, Input, ArrayTable, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { message } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayTable, }, }) const form = createForm() export default () => { return ( { e.preventDefault() message.info('remove is disabled!') }, }} /> { e.preventDefault() message.info('copy is disabled!') }, }} /> { e.preventDefault() message.info('moveDown is disabled!') }, }} /> { e.preventDefault() message.info('moveUp is disabled!') }, }} /> { e.preventDefault() const base = form.values.array.length form.values.array.push( { a1: base + 1 }, { a1: base + 2, a2: base + 2 } ) }, }} title="Add two entries" /> Submit ) } ``` ```tsx /** * title: JSON Schema */ import React from 'react' import { FormItem, Input, ArrayTable, FormButtonGroup, Submit, } from '@formily/antd' import { createForm, onFieldMount } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { message } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayTable, }, }) const form = createForm({ effects() { onFieldMount('array.add', (field, form) => { field.componentProps.onClick = (e) => { e.preventDefault() const base = form.values.array.length form.values.array.push({ a1: base + 1 }, { a1: base + 2, a2: base + 2 }) } }) onFieldMount('array.*[0:].item.*', (field) => { field.componentProps.onClick = (e) => { e.preventDefault() message.info(`${field.address.segments.slice(-1)[0]} is disabled!`) } }) }, }) const schema = { type: 'object', properties: { array: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'ArrayTable', 'x-component-props': { pagination: { pageSize: 10 }, scroll: { x: '100%' }, }, items: { type: 'object', properties: { column1: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 80, title: 'Index', align: 'center' }, properties: { index: { type: 'void', 'x-component': 'ArrayTable.Index', }, }, }, column2: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A1' }, properties: { a1: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, column3: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A2' }, properties: { a2: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, column4: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Operations', dataIndex: 'operations', width: 200, fixed: 'right', }, properties: { item: { type: 'void', 'x-component': 'FormItem', properties: { remove: { type: 'void', 'x-component': 'ArrayTable.Remove', }, moveDown: { type: 'void', 'x-component': 'ArrayTable.MoveDown', }, moveUp: { type: 'void', 'x-component': 'ArrayTable.MoveUp', }, }, }, }, }, }, }, properties: { add: { type: 'void', 'x-component': 'ArrayTable.Addition', title: 'Add two entries', }, }, }, }, } export default () => { return ( Submit ) } ``` ## API ### ArrayTable > Form Components Extended attributes | Property name | Type | Description | Default value | | ------------- | ------------------------- | --------------- | ------------- | | onAdd | `(index: number) => void` | add method | | | onRemove | `(index: number) => void` | remove method | | | onCopy | `(index: number) => void` | copy method | | | onMoveUp | `(index: number) => void` | moveUp method | | | onMoveDown | `(index: number) => void` | moveDown method | | Other Reference https://ant.design/components/table-cn/ ### ArrayTable.Column > Table Column Reference https://ant.design/components/table-cn/ ### ArrayTable.SortHandle > Drag handle Reference https://ant.design/components/icon-cn/ ### ArrayTable.Addition > Add button Extended attributes | Property name | Type | Description | Default value | | ------------- | -------------------- | ------------- | ------------- | | title | ReactText | Copywriting | | | method | `'push' \|'unshift'` | add method | `'push'` | | defaultValue | `any` | Default value | | Other references https://ant.design/components/button-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayTable.Remove > Delete button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayTable.Copy > Copy button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayTable.MoveDown > Move down button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayTable.MoveUp > Move up button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayTable.Index > Index Renderer No attributes ### ArrayTable.useIndex > Read the React Hook of the current rendering row index ### ArrayTable.useRecord > Read the React Hook of the current rendering row ================================================ FILE: packages/antd/docs/components/ArrayTable.zh-CN.md ================================================ # ArrayTable > 自增表格,对于数据量超大的场景比较适合使用该组件,虽然数据量大到一定程度会有些许卡顿,但是不会影响基本操作 > > 注意:该组件只适用于 Schema 场景,且只能是对象数组 ## Markup Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayTable, Editable, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button, Alert } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Editable, Input, ArrayTable, }, }) const form = createForm() const range = (count: number) => Array.from(new Array(count)).map((_, key) => ({ aaa: key, })) export default () => { return ( 提交 ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayTable, Editable, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Editable, Input, ArrayTable, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'ArrayTable', 'x-component-props': { pagination: { pageSize: 10 }, scroll: { x: '100%' }, }, items: { type: 'object', properties: { column1: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 50, title: 'Sort', align: 'center' }, properties: { sort: { type: 'void', 'x-component': 'ArrayTable.SortHandle', }, }, }, column2: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 80, title: 'Index', align: 'center' }, properties: { index: { type: 'void', 'x-component': 'ArrayTable.Index', }, }, }, column3: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A1' }, properties: { a1: { type: 'string', 'x-decorator': 'Editable', 'x-component': 'Input', }, }, }, column4: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A2' }, properties: { a2: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, column5: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A3' }, properties: { a3: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, column6: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Operations', dataIndex: 'operations', width: 200, fixed: 'right', }, properties: { item: { type: 'void', 'x-component': 'FormItem', properties: { remove: { type: 'void', 'x-component': 'ArrayTable.Remove', }, moveDown: { type: 'void', 'x-component': 'ArrayTable.MoveDown', }, moveUp: { type: 'void', 'x-component': 'ArrayTable.MoveUp', }, }, }, }, }, }, }, properties: { add: { type: 'void', 'x-component': 'ArrayTable.Addition', title: '添加条目', }, }, }, }, } export default () => { return ( 提交 ) } ``` ## Effects 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayTable, Switch, FormButtonGroup, Submit, } from '@formily/antd' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Switch, Input, Button, ArrayTable, }, }) const form = createForm({ effects: () => { //主动联动模式 onFieldChange('hideFirstColumn', ['value'], (field) => { field.query('array.column4').take((target) => { target.visible = !field.value }) field.query('array.*.a2').take((target) => { target.visible = !field.value }) }) //被动联动模式 onFieldReact('array.*.a2', (field) => { field.visible = !field.query('.a1').get('value') }) }, }) export default () => { return ( A2', dataIndex: 'a1', width: 100, }} > 提交 ) } ``` ## JSON Schema 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayTable, Switch, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Switch, Input, ArrayTable, }, }) const form = createForm() const schema = { type: 'object', properties: { hideFirstColumn: { type: 'boolean', title: '隐藏A2', 'x-decorator': 'FormItem', 'x-component': 'Switch', }, array: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'ArrayTable', 'x-component-props': { pagination: { pageSize: 10 }, scroll: { x: '100%' }, }, items: { type: 'object', properties: { column1: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 50, title: 'Sort', align: 'center' }, properties: { sort: { type: 'void', 'x-component': 'ArrayTable.SortHandle', }, }, }, column2: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 80, title: 'Index', align: 'center' }, properties: { index: { type: 'void', 'x-component': 'ArrayTable.Index', }, }, }, column3: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 100, title: '显隐->A2' }, properties: { a1: { type: 'boolean', 'x-decorator': 'FormItem', 'x-component': 'Switch', }, }, }, column4: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A2' }, 'x-reactions': [ { dependencies: ['hideFirstColumn'], when: '{{$deps[0]}}', fulfill: { schema: { 'x-visible': false, }, }, otherwise: { schema: { 'x-visible': true, }, }, }, ], properties: { a2: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', required: true, 'x-reactions': [ { dependencies: ['.a1', 'hideFirstColumn'], when: '{{$deps[1] || $deps[0]}}', fulfill: { schema: { 'x-visible': false, }, }, otherwise: { schema: { 'x-visible': true, }, }, }, ], }, }, }, column5: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A3' }, properties: { a3: { type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, column6: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Operations', dataIndex: 'operations', width: 200, fixed: 'right', }, properties: { item: { type: 'void', 'x-component': 'FormItem', properties: { remove: { type: 'void', 'x-component': 'ArrayTable.Remove', }, moveDown: { type: 'void', 'x-component': 'ArrayTable.MoveDown', }, moveUp: { type: 'void', 'x-component': 'ArrayTable.MoveUp', }, }, }, }, }, }, }, properties: { add: { type: 'void', 'x-component': 'ArrayTable.Addition', title: '添加条目', }, }, }, }, } export default () => { return ( 提交 ) } ``` ## 重写内置操作项的默认行为 ```tsx import React from 'react' import { FormItem, Input, ArrayTable, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { message } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayTable, }, }) const form = createForm() export default () => { return ( { e.preventDefault() message.info('remove 已被禁用!') }, }} /> { e.preventDefault() message.info('copy 已被禁用!') }, }} /> { e.preventDefault() message.info('moveDown 已被禁用!') }, }} /> { e.preventDefault() message.info('moveUp 已被禁用!') }, }} /> { e.preventDefault() const base = form.values.array.length form.values.array.push( { a1: base + 1 }, { a1: base + 2, a2: base + 2 } ) }, }} title="添加2个条目" /> 提交 ) } ``` ```tsx /** * title: JSON Schema */ import React from 'react' import { FormItem, Input, ArrayTable, FormButtonGroup, Submit, } from '@formily/antd' import { createForm, onFieldMount } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { message } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayTable, }, }) const form = createForm({ effects() { onFieldMount('array.add', (field, form) => { field.componentProps.onClick = (e) => { e.preventDefault() const base = form.values.array.length form.values.array.push({ a1: base + 1 }, { a1: base + 2, a2: base + 2 }) } }) onFieldMount('array.*[0:].item.*', (field) => { field.componentProps.onClick = (e) => { e.preventDefault() message.info(`${field.address.segments.slice(-1)[0]} 已被禁用!`) } }) }, }) const schema = { type: 'object', properties: { array: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'ArrayTable', 'x-component-props': { pagination: { pageSize: 10 }, scroll: { x: '100%' }, }, items: { type: 'object', properties: { column1: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 80, title: 'Index', align: 'center' }, properties: { index: { type: 'void', 'x-component': 'ArrayTable.Index', }, }, }, column2: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A1' }, properties: { a1: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, column3: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A2' }, properties: { a2: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, column4: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Operations', dataIndex: 'operations', width: 200, fixed: 'right', }, properties: { item: { type: 'void', 'x-component': 'FormItem', properties: { remove: { type: 'void', 'x-component': 'ArrayTable.Remove', }, copy: { type: 'void', 'x-component': 'ArrayTable.Copy', }, moveDown: { type: 'void', 'x-component': 'ArrayTable.MoveDown', }, moveUp: { type: 'void', 'x-component': 'ArrayTable.MoveUp', }, }, }, }, }, }, }, properties: { add: { type: 'void', 'x-component': 'ArrayTable.Addition', title: '添加2个条目', }, }, }, }, } export default () => { return ( Submit ) } ``` ## API ### ArrayTable > 表格组件 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ---------- | ------------------------- | ------------ | ------ | | onAdd | `(index: number) => void` | 增加方法 | | | onRemove | `(index: number) => void` | 删除方法 | | | onCopy | `(index: number) => void` | 复制方法 | | | onMoveUp | `(index: number) => void` | 向上移动方法 | | | onMoveDown | `(index: number) => void` | 向下移动方法 | | 其余参考 https://ant.design/components/table-cn/ ### ArrayTable.Column > 表格列 参考 https://ant.design/components/table-cn/ ### ArrayTable.SortHandle > 拖拽手柄 参考 https://ant.design/components/icon-cn/ ### ArrayTable.Addition > 添加按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | | defaultValue | `any` | 默认值 | | 其余参考 https://ant.design/components/button-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayTable.Remove > 删除按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayTable.Copy > 复制按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayTable.MoveDown > 下移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayTable.MoveUp > 上移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayTable.Index > 索引渲染器 无属性 ### ArrayTable.useIndex > 读取当前渲染行索引的 React Hook ### ArrayTable.useRecord > 读取当前渲染记录的 React Hook ================================================ FILE: packages/antd/docs/components/ArrayTabs.md ================================================ # ArrayTabs > Self-increasing tab, you can consider using this component for scenarios with high vertical space requirements > > Note: This component is only applicable to Schema scenarios, please avoid cross-tab linkage in interaction ## Markup Schema example ```tsx import React from 'react' import { FormItem, Input, ArrayTabs, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayTabs, }, }) const form = createForm() export default () => { return ( Submit ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormItem, Input, ArrayTabs, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayTabs, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', title: 'String array', 'x-decorator': 'FormItem', maxItems: 3, 'x-component': 'ArrayTabs', items: { type: 'string', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, array: { type: 'array', title: 'Object array', 'x-decorator': 'FormItem', maxItems: 3, 'x-component': 'ArrayTabs', items: { type: 'object', properties: { aaa: { type: 'string', 'x-decorator': 'FormItem', title: 'AAA', required: true, 'x-component': 'Input', }, bbb: { type: 'string', 'x-decorator': 'FormItem', title: 'BBB', required: true, 'x-component': 'Input', }, }, }, }, }, } export default () => { return ( Submit ) } ``` ## API ### ArrayTabs Reference https://ant.design/components/tabs-cn/ ================================================ FILE: packages/antd/docs/components/ArrayTabs.zh-CN.md ================================================ # ArrayTabs > 自增选项卡,对于纵向空间要求较高的场景可以考虑使用该组件 > > 注意:该组件只适用于 Schema 场景,交互上请避免跨 Tab 联动 ## Markup Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayTabs, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayTabs, }, }) const form = createForm() export default () => { return ( 提交 ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayTabs, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayTabs, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', title: '字符串数组', 'x-decorator': 'FormItem', maxItems: 3, 'x-component': 'ArrayTabs', items: { type: 'string', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, array: { type: 'array', title: '对象数组', 'x-decorator': 'FormItem', maxItems: 3, 'x-component': 'ArrayTabs', items: { type: 'object', properties: { aaa: { type: 'string', 'x-decorator': 'FormItem', title: 'AAA', required: true, 'x-component': 'Input', }, bbb: { type: 'string', 'x-decorator': 'FormItem', title: 'BBB', required: true, 'x-component': 'Input', }, }, }, }, }, } export default () => { return ( 提交 ) } ``` ## API ### ArrayTabs 参考 https://ant.design/components/tabs-cn/ ================================================ FILE: packages/antd/docs/components/Cascader.md ================================================ # Cascader > Cascade selector ## Markup Schema example ```tsx import React from 'react' import { Cascader, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm, onFieldReact, FormPathPattern } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Cascader, FormItem, }, }) const useAddress = (pattern: FormPathPattern) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } onFieldReact(pattern, (field) => { field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }) } const form = createForm({ effects: () => { useAddress('address') }, }) export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Cascader, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Cascader, FormItem, }, }) const transformAddress = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transformAddress(cities) const _districts = transformAddress(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } const useAsyncDataSource = (url: string, transform: (data: any) => any) => (field) => { field.loading = true fetch(url) .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) } const form = createForm() const schema = { type: 'object', properties: { address: { type: 'string', title: 'Address Selection', 'x-decorator': 'FormItem', 'x-component': 'Cascader', 'x-component-props': { style: { width: 240, }, }, 'x-reactions': [ '{{useAsyncDataSource("//unpkg.com/china-location/dist/location.json",transformAddress)}}', ], }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Cascader, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm, onFieldReact, FormPathPattern } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { action } from '@formily/reactive' const useAddress = (pattern: FormPathPattern) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } onFieldReact(pattern, (field) => { field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }) } const form = createForm({ effects: () => { useAddress('address') }, }) export default () => ( Submit ) ``` ## API Reference https://ant.design/components/cascader-cn/ ================================================ FILE: packages/antd/docs/components/Cascader.zh-CN.md ================================================ # Cascader > 联级选择器 ## Markup Schema 案例 ```tsx import React from 'react' import { Cascader, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm, onFieldReact, FormPathPattern } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Cascader, FormItem, }, }) const useAddress = (pattern: FormPathPattern) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } onFieldReact(pattern, (field) => { field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }) } const form = createForm({ effects: () => { useAddress('address') }, }) export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Cascader, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Cascader, FormItem, }, }) const transformAddress = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transformAddress(cities) const _districts = transformAddress(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } const useAsyncDataSource = (url: string, transform: (data: any) => any) => (field) => { field.loading = true fetch(url) .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) } const form = createForm() const schema = { type: 'object', properties: { address: { type: 'string', title: '地址选择', 'x-decorator': 'FormItem', 'x-component': 'Cascader', 'x-component-props': { style: { width: 240, }, }, 'x-reactions': [ '{{useAsyncDataSource("//unpkg.com/china-location/dist/location.json",transformAddress)}}', ], }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Cascader, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm, onFieldReact, FormPathPattern } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { action } from '@formily/reactive' const useAddress = (pattern: FormPathPattern) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } onFieldReact(pattern, (field) => { field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }) } const form = createForm({ effects: () => { useAddress('address') }, }) export default () => ( 提交 ) ``` ## API 参考 https://ant.design/components/cascader-cn/ ================================================ FILE: packages/antd/docs/components/Checkbox.md ================================================ # Checkbox > Checkbox ## Markup Schema example ```tsx import React from 'react' import { Checkbox, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Checkbox, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Checkbox, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Checkbox, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { single: { type: 'boolean', title: 'Are you sure?', 'x-decorator': 'FormItem', 'x-component': 'Checkbox', }, multiple: { type: 'array', title: 'Check', enum: [ { label: 'Option 1', value: 1, }, { label: 'Option 2', value: 2, }, ], 'x-decorator': 'FormItem', 'x-component': 'Checkbox.Group', }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Checkbox, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://ant.design/components/checkbox-cn/ ================================================ FILE: packages/antd/docs/components/Checkbox.zh-CN.md ================================================ # Checkbox > 复选框 ## Markup Schema 案例 ```tsx import React from 'react' import { Checkbox, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Checkbox, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Checkbox, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Checkbox, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { single: { type: 'boolean', title: '是否确认', 'x-decorator': 'FormItem', 'x-component': 'Checkbox', }, multiple: { type: 'array', title: '复选', enum: [ { label: '选项1', value: 1, }, { label: '选项2', value: 2, }, ], 'x-decorator': 'FormItem', 'x-component': 'Checkbox.Group', }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Checkbox, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://ant.design/components/checkbox-cn/ ================================================ FILE: packages/antd/docs/components/DatePicker.md ================================================ # DatePicker > Date Picker ## Markup Schema example ```tsx import React from 'react' import { DatePicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { DatePicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { date: { title: 'Normal date', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', type: 'string', }, week: { title: 'Week Selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', 'x-component-props': { picker: 'week', }, type: 'string', }, month: { title: 'Month Selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', 'x-component-props': { picker: 'month', }, type: 'string', }, quarter: { title: 'Fiscal Year Selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', 'x-component-props': { picker: 'quarter', }, type: 'string', }, year: { title: 'Year selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', 'x-component-props': { picker: 'year', }, type: 'string', }, '[startDate,endDate]': { title: 'Date range', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { showTime: true, }, type: 'string', }, range_week: { title: 'Week range selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { picker: 'week', }, type: 'string', }, range_month: { title: 'Month Range Selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { picker: 'month', }, type: 'string', }, range_quarter: { title: 'Financial year range selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { picker: 'quarter', }, type: 'string', }, range_year: { name: 'range_year', title: 'Year range selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { picker: 'year', }, type: 'string', }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { DatePicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://ant.design/components/date-picker-cn/ ================================================ FILE: packages/antd/docs/components/DatePicker.zh-CN.md ================================================ # DatePicker > 日期选择器 ## Markup Schema 案例 ```tsx import React from 'react' import { DatePicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { DatePicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { date: { title: '普通日期', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', type: 'string', }, week: { title: '周选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', 'x-component-props': { picker: 'week', }, type: 'string', }, month: { title: '月选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', 'x-component-props': { picker: 'month', }, type: 'string', }, quarter: { title: '财年选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', 'x-component-props': { picker: 'quarter', }, type: 'string', }, year: { title: '年选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', 'x-component-props': { picker: 'year', }, type: 'string', }, '[startDate,endDate]': { title: '日期范围', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { showTime: true, }, type: 'string', }, range_week: { title: '周范围选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { picker: 'week', }, type: 'string', }, range_month: { title: '月范围选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { picker: 'month', }, type: 'string', }, range_quarter: { title: '财年范围选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { picker: 'quarter', }, type: 'string', }, range_year: { name: 'range_year', title: '年范围选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { picker: 'year', }, type: 'string', }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { DatePicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://ant.design/components/date-picker-cn/ ================================================ FILE: packages/antd/docs/components/Editable.md ================================================ # Editable > Partial editor, you can use this component for some form areas with high space requirements > > Editable component is equivalent to a variant of FormItem component, so it is usually placed in decorator ## Markup Schema example ```tsx import React from 'react' import { Input, DatePicker, Editable, FormItem, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, Editable, Input, FormItem, }, }) const form = createForm() export default () => ( { field.title = field.query('.void.date2').get('value') || field.title }} > { field.title = field.value?.date || field.title }} > Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Input, DatePicker, Editable, FormItem, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, Editable, Input, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { date: { type: 'string', title: 'Date', 'x-decorator': 'Editable', 'x-component': 'DatePicker', }, input: { type: 'string', title: 'input box', 'x-decorator': 'Editable', 'x-component': 'Input', }, void: { type: 'void', title: 'Virtual Node Container', 'x-component': 'Editable.Popover', 'x-reactions': "{{(field) => field.title = field.query('.void.date2').get('value') || field.title}}", properties: { date2: { type: 'string', title: 'Date', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', }, input2: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, iobject: { type: 'object', title: 'Object node container', 'x-component': 'Editable.Popover', 'x-reactions': '{{(field) => field.title = field.value && field.value.date || field.title}}', properties: { date: { type: 'string', title: 'Date', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', }, input: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Input, DatePicker, Editable, FormItem, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field, VoidField, ObjectField } from '@formily/react' const form = createForm() export default () => ( { field.title = field.query('.void.date2').get('value') || field.title }} component={[Editable.Popover]} > { field.title = field.value?.date || field.title }} component={[Editable.Popover]} > Submit ) ``` ## API ### Editable > Inline editing Refer to the FormItem property in https://ant.design/components/form-cn/ ### Editable.Popover > Floating layer editing Reference https://ant.design/components/popover-cn/ ================================================ FILE: packages/antd/docs/components/Editable.zh-CN.md ================================================ # Editable > 局部编辑器,对于一些空间要求较高的表单区域可以使用该组件 > > Editable 组件相当于是 FormItem 组件的变体,所以通常放在 decorator 中 ## Markup Schema 案例 ```tsx import React from 'react' import { Input, DatePicker, Editable, FormItem, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, Editable, Input, FormItem, }, }) const form = createForm() export default () => ( { field.title = field.query('.void.date2').get('value') || field.title }} > { field.title = field.value?.date || field.title }} > 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Input, DatePicker, Editable, FormItem, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, Editable, Input, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { date: { type: 'string', title: '日期', 'x-decorator': 'Editable', 'x-component': 'DatePicker', }, input: { type: 'string', title: '输入框', 'x-decorator': 'Editable', 'x-component': 'Input', }, void: { type: 'void', title: '虚拟节点容器', 'x-component': 'Editable.Popover', 'x-reactions': "{{(field) => field.title = field.query('.void.date2').get('value') || field.title}}", properties: { date2: { type: 'string', title: '日期', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', }, input2: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, iobject: { type: 'object', title: '对象节点容器', 'x-component': 'Editable.Popover', 'x-reactions': '{{(field) => field.title = field.value && field.value.date || field.title}}', properties: { date: { type: 'string', title: '日期', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', }, input: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Input, DatePicker, Editable, FormItem, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field, VoidField, ObjectField } from '@formily/react' const form = createForm() export default () => ( { field.title = field.query('.void.date2').get('value') || field.title }} component={[Editable.Popover]} > { field.title = field.value?.date || field.title }} component={[Editable.Popover]} > 提交 ) ``` ## API ### Editable > 内联编辑 参考 https://ant.design/components/form-cn/ 中的 FormItem 属性 ### Editable.Popover > 浮层编辑 参考 https://ant.design/components/popover-cn/ ================================================ FILE: packages/antd/docs/components/Form.md ================================================ # Form > The combination of FormProvider + FormLayout + form tags can help us quickly implement forms that are submitted with carriage return and can be laid out in batches ## Use Cases ```tsx import React from 'react' import { Input, Select, Form, FormItem, FormGrid, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { Field } from '@formily/react' const form = createForm() export default () => (
Query
) ``` Note: To realize the carriage return submission, we cannot pass the onSubmit event to it when using the Submit component, otherwise the carriage return submission will become invalid. The purpose of this is to prevent users from writing onSubmit event listeners in multiple places at the same time, and processing logic If they are inconsistent, it is difficult to locate the problem when submitting. ## API For layout-related API properties, we can refer to [FormLayout](./form-layout), and the rest are the unique API properties of the Form component | Property name | Type | Description | Default value | | ---------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | ------------- | | form | [Form](https://core.formilyjs.org/api/models/form) | Form example | - | | component | string | Rendering component, can be specified as custom component rendering | `form` | | previewTextPlaceholder | ReactNode | Preview State Placeholder | `N/A` | | onAutoSubmit | `(values:any)=>any` | Carriage return submit event callback | - | | onAutoSubmitFailed | (feedbacks: [IFormFeedback](https://core.formilyjs.org/api/models/form#iformfeedback)[]) => void | Carriage return submission verification failure event callback | - | ================================================ FILE: packages/antd/docs/components/Form.zh-CN.md ================================================ # Form > FormProvider + FormLayout + form 标签的组合组件,可以帮助我们快速实现带回车提交的且能批量布局的表单 ## 使用案例 ```tsx import React from 'react' import { Input, Select, Form, FormItem, FormGrid, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { Field } from '@formily/react' const form = createForm() export default () => (
查询
) ``` 注意:想要实现回车提交,我们在使用Submit组件的时候不能给其传onSubmit事件,否则回车提交会失效,这样做的目的是为了防止用户同时在多处写onSubmit事件监听器,处理逻辑不一致的话,提交时很难定位问题。 ## API 布局相关的 API 属性,我们参考 [FormLayout](./form-layout)即可,剩下是 Form 组件独有的 API 属性 | 属性名 | 类型 | 描述 | 默认值 | | ---------------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------- | ------ | | form | [Form](https://core.formilyjs.org/zh-CN/api/models/form) | Form 实例 | - | | component | string | 渲染组件,可以指定为自定义组件渲染 | `form` | | previewTextPlaceholder | ReactNode | 预览态占位符 | `N/A` | | onAutoSubmit | `(values:any)=>any` | 回车提交事件回调 | - | | onAutoSubmitFailed | (feedbacks: [IFormFeedback](https://core.formilyjs.org/zh-CN/api/models/form#iformfeedback)[]) => void | 回车提交校验失败事件回调 | - | ================================================ FILE: packages/antd/docs/components/FormButtonGroup.md ================================================ # FormButtonGroup > Form button group layout component ## Common case ```tsx import React from 'react' import { FormButtonGroup, Submit, Reset, FormItem, Input, FormLayout, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const form = createForm() export default () => { return ( Submit Reset ) } ``` ## Suction bottom case ```tsx import React from 'react' import { FormButtonGroup, Submit, Reset, FormItem, FormLayout, Input, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const form = createForm() export default () => { return ( Submit Reset ) } ``` ## Suction bottom centering case ```tsx import React from 'react' import { FormButtonGroup, Submit, Reset, FormItem, FormLayout, Input, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const form = createForm() export default () => { return ( Submit Reset ) } ``` ## API ### FormButtonGroup > This component is mainly used to handle the button group gap | Property name | Type | Description | Default value | | ------------- | --------------------------- | ----------- | ------------- | | gutter | number | Gap size | 8px | | align | `'left'\|'center'\|'right'` | Alignment | `'left'` | ### FormButtonGroup.FormItem > This component is mainly used to deal with the alignment of the button group and the main form FormItem Refer to [FormItem](/components/form-item) property ### FormButtonGroup.Sticky > This component is mainly used to deal with the floating positioning problem of the button group | Property name | Type | Description | Default value | | ------------- | --------------------------- | ----------- | ------------- | | align | `'left'\|'center'\|'right'` | Alignment | `'left'` | ================================================ FILE: packages/antd/docs/components/FormButtonGroup.zh-CN.md ================================================ # FormButtonGroup > 表单按钮组布局组件 ## 普通案例 ```tsx import React from 'react' import { FormButtonGroup, Submit, Reset, FormItem, Input, FormLayout, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const form = createForm() export default () => { return ( 提交 重置 ) } ``` ## 吸底案例 ```tsx import React from 'react' import { FormButtonGroup, Submit, Reset, FormItem, FormLayout, Input, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const form = createForm() export default () => { return ( 提交 重置 ) } ``` ## 吸底居中案例 ```tsx import React from 'react' import { FormButtonGroup, Submit, Reset, FormItem, FormLayout, Input, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const form = createForm() export default () => { return ( 提交 重置 ) } ``` ## API ### FormButtonGroup > 该组件主要用来处理按钮组间隙 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------------------------- | -------- | -------- | | gutter | number | 间隙大小 | 8px | | align | `'left'\|'center'\|'right'` | 对齐方式 | `'left'` | ### FormButtonGroup.FormItem > 该组件主要用来处理按钮组与主表单 FormItem 对齐问题 参考 [FormItem](/components/form-item) 属性 ### FormButtonGroup.Sticky > 该组件主要用来处理按钮组浮动定位问题 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------------------------- | -------- | -------- | | align | `'left'\|'center'\|'right'` | 对齐方式 | `'left'` | ================================================ FILE: packages/antd/docs/components/FormCollapse.md ================================================ # FormCollapse > Folding panel, usually used in form scenes with high layout space requirements > > Note: Can only be used in Schema scenarios ## Markup Schema example ```tsx import React from 'react' import { FormCollapse, FormLayout, FormItem, Input, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, FormCollapse, Input, }, }) const form = createForm() const formCollapse = FormCollapse.createFormCollapse() export default () => { return ( Submit ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormCollapse, FormItem, FormLayout, Input, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, FormCollapse, Input, }, }) const form = createForm() const formCollapse = FormCollapse.createFormCollapse() const schema = { type: 'object', properties: { collapse: { type: 'void', title: 'Folding Panel', 'x-decorator': 'FormItem', 'x-component': 'FormCollapse', 'x-component-props': { formCollapse: '{{formCollapse}}', }, properties: { panel1: { type: 'void', 'x-component': 'FormCollapse.CollapsePanel', 'x-component-props': { header: 'A1', }, properties: { aaa: { type: 'string', title: 'AAA', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, panel2: { type: 'void', 'x-component': 'FormCollapse.CollapsePanel', 'x-component-props': { header: 'A2', }, properties: { bbb: { type: 'string', title: 'BBB', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, panel3: { type: 'void', 'x-component': 'FormCollapse.CollapsePanel', 'x-component-props': { header: 'A3', }, properties: { ccc: { type: 'string', title: 'CCC', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, }, }, }, } export default () => { return ( Submit ) } ``` ## API ### FormCollapse | Property name | Type | Description | Default value | | ------------- | ------------- | --------------------------------------------------------------- | ------------- | | formCollapse | IFormCollapse | Pass in the model created by createFormCollapse/useFormCollapse | | Other references https://ant.design/components/collapse-cn/ ### FormCollapse.CollapsePanel Reference https://ant.design/components/collapse-cn/ ### FormCollapse.createFormCollapse ```ts pure type ActiveKey = string | number type ActiveKeys = string | number | Array interface createFormCollapse { (defaultActiveKeys?: ActiveKeys): IFormCollpase } interface IFormCollapse { //Activate the primary key list activeKeys: ActiveKeys //Does the activation key exist? hasActiveKey(key: ActiveKey): boolean //Set the list of active primary keys setActiveKeys(keys: ActiveKeys): void //Add activation key addActiveKey(key: ActiveKey): void //Delete the active primary key removeActiveKey(key: ActiveKey): void //Switch to activate the main key toggleActiveKey(key: ActiveKey): void } ``` ================================================ FILE: packages/antd/docs/components/FormCollapse.zh-CN.md ================================================ # FormCollapse > 折叠面板,通常用在布局空间要求较高的表单场景 > > 注意:只能用在 Schema 场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormCollapse, FormLayout, FormItem, Input, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, FormCollapse, Input, }, }) const form = createForm() const formCollapse = FormCollapse.createFormCollapse() export default () => { return ( 提交 ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormCollapse, FormItem, FormLayout, Input, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, FormCollapse, Input, }, }) const form = createForm() const formCollapse = FormCollapse.createFormCollapse() const schema = { type: 'object', properties: { collapse: { type: 'void', title: '折叠面板', 'x-decorator': 'FormItem', 'x-component': 'FormCollapse', 'x-component-props': { formCollapse: '{{formCollapse}}', }, properties: { panel1: { type: 'void', 'x-component': 'FormCollapse.CollapsePanel', 'x-component-props': { header: 'A1', }, properties: { aaa: { type: 'string', title: 'AAA', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, panel2: { type: 'void', 'x-component': 'FormCollapse.CollapsePanel', 'x-component-props': { header: 'A2', }, properties: { bbb: { type: 'string', title: 'BBB', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, panel3: { type: 'void', 'x-component': 'FormCollapse.CollapsePanel', 'x-component-props': { header: 'A3', }, properties: { ccc: { type: 'string', title: 'CCC', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, }, }, }, } export default () => { return ( 提交 ) } ``` ## API ### FormCollapse | 属性名 | 类型 | 描述 | 默认值 | | ------------ | ------------- | ---------------------------------------------------------- | ------ | | formCollapse | IFormCollapse | 传入通过 createFormCollapse/useFormCollapse 创建出来的模型 | | 其余参考 https://ant.design/components/collapse-cn/ ### FormCollapse.CollapsePanel 参考 https://ant.design/components/collapse-cn/ ### FormCollapse.createFormCollapse ```ts pure type ActiveKey = string | number type ActiveKeys = string | number | Array interface createFormCollapse { (defaultActiveKeys?: ActiveKeys): IFormCollpase } interface IFormCollapse { //激活主键列表 activeKeys: ActiveKeys //是否存在该激活主键 hasActiveKey(key: ActiveKey): boolean //设置激活主键列表 setActiveKeys(keys: ActiveKeys): void //添加激活主键 addActiveKey(key: ActiveKey): void //删除激活主键 removeActiveKey(key: ActiveKey): void //开关切换激活主键 toggleActiveKey(key: ActiveKey): void } ``` ================================================ FILE: packages/antd/docs/components/FormDialog.md ================================================ # FormDialog > Pop-up form, mainly used in simple event to open the form scene ## Markup Schema example ```tsx import React from 'react' import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd' import { createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) export default () => { return ( ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd' import { createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const schema = { type: 'object', properties: { aaa: { type: 'string', title: 'input box 1', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, bbb: { type: 'string', title: 'input box 2', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, ccc: { type: 'string', title: 'input box 3', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, ddd: { type: 'string', title: 'input box 4', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, } export default () => { return ( ) } ``` ## Pure JSX case ```tsx import React from 'react' import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd' import { Field } from '@formily/react' import { Button } from 'antd' export default () => { return ( ) } ``` ## API ### FormDialog ```ts pure import { IFormProps, Form } from '@formily/core' type FormDialogRenderer = | React.ReactElement | ((form: Form) => React.ReactElement) type ModalTitle = string | number | React.ReactElement interface IFormDialog { forOpen( middleware: ( props: IFormProps, next: (props?: IFormProps) => Promise ) => any ): any //Middleware interceptor, can intercept Dialog to open forConfirm( middleware: (props: Form, next: (props?: Form) => Promise) => any ): any //Middleware interceptor, which can intercept Dialog confirmation forCancel( middleware: (props: Form, next: (props?: Form) => Promise) => any ): any //Middleware interceptor, can intercept Dialog to cancel //Open the pop-up window to receive form attributes, you can pass in initialValues/values/effects etc. open(props: IFormProps): Promise //return form data //Close the pop-up window close(): void } interface IModalProps extends ModalProps { onOk?: (event: React.MouseEvent) => void | boolean // return false can prevent onOk onCancel?: (event: React.MouseEvent) => void | boolean // return false can prevent onCancel loadingText?: React.ReactNode } interface FormDialog { (title: IModalProps, id: string, renderer: FormDialogRenderer): IFormDialog (title: IModalProps, renderer: FormDialogRenderer): IFormDialog (title: ModalTitle, id: string, renderer: FormDialogRenderer): IFormDialog (title: ModalTitle, renderer: FormDialogRenderer): IFormDialog } ``` `ModalProps` type definition reference ant design [Modal API](https://ant.design/components/modal-cn/#API) ### FormDialog.Footer No attributes, only child nodes are received ### FormDialog.Portal 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 ================================================ FILE: packages/antd/docs/components/FormDialog.zh-CN.md ================================================ # FormDialog > 弹窗表单,主要用在简单的事件打开表单场景 ## Markup Schema 案例 以下例子演示了 FormDialog 的几个能力: - 快速打开,关闭能力 - 中间件能力,自动出现加载态 - 渲染函数内可以响应式能力 - 上下文共享能力 ```tsx import React, { createContext, useContext } from 'react' import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd' import { createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const Context = createContext() const PortalId = '可以传,也可以不传的ID,默认是form-dialog' export default () => { return ( ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd' import { createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const schema = { type: 'object', properties: { aaa: { type: 'string', title: '输入框1', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, bbb: { type: 'string', title: '输入框2', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, ccc: { type: 'string', title: '输入框3', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, ddd: { type: 'string', title: '输入框4', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, } export default () => { return ( ) } ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd' import { Field } from '@formily/react' import { Button } from 'antd' export default () => { return ( ) } ``` ## API ### FormDialog ```ts pure import { IFormProps, Form } from '@formily/core' type FormDialogRenderer = | React.ReactElement | ((form: Form) => React.ReactElement) type ModalTitle = string | number | React.ReactElement interface IFormDialog { forOpen( middleware: ( props: IFormProps, next: (props?: IFormProps) => Promise ) => any ): any //中间件拦截器,可以拦截Dialog打开 forConfirm( middleware: (props: Form, next: (props?: Form) => Promise) => any ): any //中间件拦截器,可以拦截Dialog确认 forCancel( middleware: (props: Form, next: (props?: Form) => Promise) => any ): any //中间件拦截器,可以拦截Dialog取消 //打开弹窗,接收表单属性,可以传入initialValues/values/effects etc. open(props: IFormProps): Promise //返回表单数据 //关闭弹窗 close(): void } interface IModalProps extends ModalProps { onOk?: (event: React.MouseEvent) => void | boolean // return false can prevent onOk onCancel?: (event: React.MouseEvent) => void | boolean // return false can prevent onCancel loadingText?: React.ReactNode } interface FormDialog { (title: IModalProps, id: string, renderer: FormDialogRenderer): IFormDialog (title: IModalProps, renderer: FormDialogRenderer): IFormDialog (title: ModalTitle, id: string, renderer: FormDialogRenderer): IFormDialog (title: ModalTitle, renderer: FormDialogRenderer): IFormDialog } ``` `ModalProps`类型定义参考 ant design [Modal API](https://ant.design/components/modal-cn/#API) ### FormDialog.Footer 无属性,只接收子节点 ### FormDialog.Portal 接收可选的 id 属性,默认值为`form-dialog`,如果一个应用存在多个 prefixCls,不同区域的弹窗内部 prefixCls 不一样,那推荐指定 id 为区域级 id ================================================ FILE: packages/antd/docs/components/FormDrawer.md ================================================ # FormDrawer > Drawer form, mainly used in simple event to open form scene ## Markup Schema example ```tsx import React from 'react' import { FormDrawer, FormItem, FormLayout, Input, Submit, Reset, FormButtonGroup, } from '@formily/antd' import { createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) export default () => { return ( ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormDrawer, FormItem, FormLayout, Input, Submit, Reset, FormButtonGroup, } from '@formily/antd' import { createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const schema = { type: 'object', properties: { aaa: { type: 'string', title: 'input box 1', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, bbb: { type: 'string', title: 'input box 2', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, ccc: { type: 'string', title: 'input box 3', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, ddd: { type: 'string', title: 'input box 4', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, } export default () => { return ( ) } ``` ## Pure JSX case ```tsx import React from 'react' import { FormDrawer, FormItem, FormLayout, Input, Submit, Reset, FormButtonGroup, } from '@formily/antd' import { Field } from '@formily/react' import { Button } from 'antd' export default () => { return ( ) } ``` ## API ### FormDrawer ```ts pure import { IFormProps, Form } from '@formily/core' type FormDrawerRenderer = | React.ReactElement | ((form: Form) => React.ReactElement) interface IFormDrawer { forOpen( middleware: ( props: IFormProps, next: (props?: IFormProps) => Promise ) => any ): any //Middleware interceptor, can intercept Drawer to open //Open the pop-up window to receive form attributes, you can pass in initialValues/values/effects etc. open(props: IFormProps): Promise //return form data //Close the pop-up window close(): void } export interface IDrawerProps extends DrawerProps { onClose?: (e: EventType) => void | boolean // return false can prevent onClose loadingText?: React.ReactNode } interface FormDrawer { (title: IDrawerProps, id: string, renderer: FormDrawerRenderer): IFormDrawer (title: IDrawerProps, renderer: FormDrawerRenderer): IFormDrawer (title: ModalTitle, id: string, renderer: FormDrawerRenderer): IFormDrawer (title: ModalTitle, renderer: FormDrawerRenderer): IFormDrawer } ``` `DrawerProps` type definition reference ant design [Drawer API](https://ant.design/components/drawer-cn/#API) ### FormDrawer.Extra No attributes, only child nodes are received ### FormDrawer.Footer No attributes, only child nodes are received ### FormDrawer.Portal Receive an optional id attribute, the default value is `form-drawer`, 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 ================================================ FILE: packages/antd/docs/components/FormDrawer.zh-CN.md ================================================ # FormDrawer > 抽屉表单,主要用在简单的事件打开表单场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormDrawer, FormItem, FormLayout, Input, Submit, Reset, FormButtonGroup, } from '@formily/antd' import { createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) export default () => { return ( ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormDrawer, FormItem, FormLayout, Input, Submit, Reset, FormButtonGroup, } from '@formily/antd' import { createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const schema = { type: 'object', properties: { aaa: { type: 'string', title: '输入框1', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, bbb: { type: 'string', title: '输入框2', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, ccc: { type: 'string', title: '输入框3', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, ddd: { type: 'string', title: '输入框4', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, } export default () => { return ( ) } ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { FormDrawer, FormItem, FormLayout, Input, Submit, Reset, FormButtonGroup, } from '@formily/antd' import { Field } from '@formily/react' import { Button } from 'antd' export default () => { return ( ) } ``` ## API ### FormDrawer ```ts pure import { IFormProps, Form } from '@formily/core' type FormDrawerRenderer = | React.ReactElement | ((form: Form) => React.ReactElement) interface IFormDrawer { forOpen( middleware: ( props: IFormProps, next: (props?: IFormProps) => Promise ) => any ): any //中间件拦截器,可以拦截Drawer打开 //打开弹窗,接收表单属性,可以传入initialValues/values/effects etc. open(props: IFormProps): Promise //返回表单数据 //关闭弹窗 close(): void } export interface IDrawerProps extends DrawerProps { onClose?: (e: EventType) => void | boolean // return false can prevent onClose loadingText?: React.ReactNode } interface FormDrawer { (title: IDrawerProps, id: string, renderer: FormDrawerRenderer): IFormDrawer (title: IDrawerProps, renderer: FormDrawerRenderer): IFormDrawer (title: ModalTitle, id: string, renderer: FormDrawerRenderer): IFormDrawer (title: ModalTitle, renderer: FormDrawerRenderer): IFormDrawer } ``` `DrawerProps`类型定义参考 ant design [Drawer API](https://ant.design/components/drawer-cn/#API) ### FormDrawer.Extra 无属性,只接收子节点 ### FormDrawer.Footer 无属性,只接收子节点 ### FormDrawer.Portal 接收可选的 id 属性,默认值为`form-drawer`,如果一个应用存在多个 prefixCls,不同区域的弹窗内部 prefixCls 不一样,那推荐指定 id 为区域级 id ================================================ FILE: packages/antd/docs/components/FormGrid.md ================================================ # FormGrid > FormGrid component ## Markup Schema example ```tsx import React from 'react' import { FormItem, Input, FormGrid } from '@formily/antd' import { FormProvider, createSchemaField } from '@formily/react' import { createForm } from '@formily/core' const SchemaField = createSchemaField({ components: { FormItem, Input, FormGrid, }, }) const form = createForm() export default () => { return ( ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormItem, Input, FormGrid } from '@formily/antd' import { FormProvider, createSchemaField } from '@formily/react' import { createForm } from '@formily/core' const SchemaField = createSchemaField({ components: { FormItem, Input, FormGrid, }, }) const form = createForm() const schema = { type: 'object', properties: { grid: { type: 'void', 'x-component': 'FormGrid', 'x-component-props': { minColumns: [4, 6, 10], }, properties: { aaa: { type: 'string', title: 'AAA', 'x-decorator': 'FormItem', 'x-component': 'Input', }, bbb: { type: 'string', title: 'BBB', 'x-decorator': 'FormItem', 'x-component': 'Input', }, ccc: { type: 'string', title: 'CCC', 'x-decorator': 'FormItem', 'x-component': 'Input', }, ddd: { type: 'string', title: 'DDD', 'x-decorator': 'FormItem', 'x-component': 'Input', }, eee: { type: 'string', title: 'EEE', 'x-decorator': 'FormItem', 'x-component': 'Input', }, fff: { type: 'string', title: 'FFF', 'x-decorator': 'FormItem', 'x-component': 'Input', }, ggg: { type: 'string', title: 'GGG', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, }, } export default () => { return ( ) } ``` ## Native case ```tsx import React from 'react' import { FormGrid } from '@formily/antd' const { GridColumn } = FormGrid const Cell = ({ children }) => { return (
{children}
) } export default () => { return (

maxColumns 3 + minColumns 2

1 2 3 4 5 6

maxColumns 3

1 2 3 4 5 6

minColumns 2

1 2 3 4 5 6

Null

1 2 3 4 5 6

minWidth 150 +maxColumns 3

1 2 3 4 5 6

maxWidth 120+minColumns 2

1 2 3 4 5 6

maxWidth 120 + gridSpan -1

1 2 3
) } ``` ## Query Form case ```tsx import React, { useMemo, Fragment } from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormProvider, observer } from '@formily/react' import { Form, Input, Select, DatePicker, FormItem, FormGrid, Submit, Reset, FormButtonGroup, } from '@formily/antd' const useCollapseGrid = (maxRows: number) => { const grid = useMemo( () => FormGrid.createFormGrid({ maxColumns: 4, maxWidth: 240, maxRows: maxRows, shouldVisible: (node, grid) => { if (node.index === grid.childSize - 1) return true if (grid.maxRows === Infinity) return true return node.shadowRow < maxRows + 1 }, }), [] ) const expanded = grid.maxRows === Infinity const realRows = grid.shadowRows const computeRows = grid.fullnessLastColumn ? grid.shadowRows - 1 : grid.shadowRows const toggle = () => { if (grid.maxRows === Infinity) { grid.maxRows = maxRows } else { grid.maxRows = Infinity } } const takeType = () => { if (realRows < maxRows + 1) return 'incomplete-wrap' if (computeRows > maxRows) return 'collapsible' return 'complete-wrap' } return { grid, expanded, toggle, type: takeType(), } } const QueryForm: React.FC = observer((props) => { const { grid, expanded, toggle, type } = useCollapseGrid(1) const renderActions = () => { return ( Query Reset ) } const renderButtonGroup = () => { if (type === 'incomplete-wrap') { return ( {renderActions()} ) } if (type === 'collapsible') { return ( { e.preventDefault() toggle() }} > {expanded ? 'Fold' : 'UnFold'} {renderActions()} ) } return ( {renderActions()} ) } return (
{props.children} {renderButtonGroup()}
) }) const SchemaField = createSchemaField({ components: { QueryForm, Input, Select, DatePicker, FormItem, }, }) export default () => { const form = useMemo(() => createForm(), []) return ( ) } ``` ## API ### FormGrid | Property name | Type | Description | Default value | | ------------- | ---------------------- | --------------------------------------------------------------------------------- | ----------------- | | minWidth | `number \| number[]` | Minimum element width | 100 | | maxWidth | `number \| number[]` | Maximum element width | - | | minColumns | `number \| number[]` | Minimum number of columns | 0 | | maxColumns | `number \| number[]` | Maximum number of columns | - | | breakpoints | number[] | Container size breakpoints | `[720,1280,1920]` | | columnGap | number | Column spacing | 8 | | rowGap | number | Row spacing | 4 | | colWrap | boolean | Wrap | true | | strictAutoFit | boolean | Is width strictly limited by maxWidth | false | | shouldVisible | `(node,grid)=>boolean` | Whether to show the current node | `()=>true` | | grid | `Grid` | Grid instance passed in from outside, used to implement more complex layout logic | - | note: - minWidth takes priority over minColumn - maxWidth has priority over maxColumn - The array format of minWidth/maxWidth/minColumns/maxColumns represents the mapping with the breakpoint array ### FormGrid.GridColumn | Property name | Type | Description | Default value | | ------------- | ------ | ------------------------------------------------------------------------------------------------------------------------ | ------------- | | gridSpan | number | The number of columns spanned by the element, if it is -1, it will automatically fill the cell across columns in reverse | 1 | ### FormGrid.createFormGrid Read the Grid instance from the context ```ts interface createFormGrid { (props: IGridProps): Grid } ``` - IGridProps reference FormGrid properties - Grid instance attribute method reference https://github.com/alibaba/formily/tree/formily_next/packages/grid ### FormGrid.useFormGrid Read the Grid instance from the context ```ts interface useFormGrid { (): Grid } ``` - Grid instance attribute method reference https://github.com/alibaba/formily/tree/formily_next/packages/grid ================================================ FILE: packages/antd/docs/components/FormGrid.zh-CN.md ================================================ # FormGrid > FormGrid 组件 ## Markup Schema 案例 ```tsx import React from 'react' import { FormItem, Input, FormGrid } from '@formily/antd' import { FormProvider, createSchemaField } from '@formily/react' import { createForm } from '@formily/core' const SchemaField = createSchemaField({ components: { FormItem, Input, FormGrid, }, }) const form = createForm() export default () => { return ( ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormItem, Input, FormGrid } from '@formily/antd' import { FormProvider, createSchemaField } from '@formily/react' import { createForm } from '@formily/core' const SchemaField = createSchemaField({ components: { FormItem, Input, FormGrid, }, }) const form = createForm() const schema = { type: 'object', properties: { grid: { type: 'void', 'x-component': 'FormGrid', 'x-component-props': { minColumns: [4, 6, 10], }, properties: { aaa: { type: 'string', title: 'AAA', 'x-decorator': 'FormItem', 'x-component': 'Input', }, bbb: { type: 'string', title: 'BBB', 'x-decorator': 'FormItem', 'x-component': 'Input', }, ccc: { type: 'string', title: 'CCC', 'x-decorator': 'FormItem', 'x-component': 'Input', }, ddd: { type: 'string', title: 'DDD', 'x-decorator': 'FormItem', 'x-component': 'Input', }, eee: { type: 'string', title: 'EEE', 'x-decorator': 'FormItem', 'x-component': 'Input', }, fff: { type: 'string', title: 'FFF', 'x-decorator': 'FormItem', 'x-component': 'Input', }, ggg: { type: 'string', title: 'GGG', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, }, } export default () => { return ( ) } ``` ## 原生 案例 ```tsx import React from 'react' import { FormGrid } from '@formily/antd' const { GridColumn } = FormGrid const Cell = ({ children }) => { return (
{children}
) } export default () => { return (

maxColumns 3 + minColumns 2

1 2 3 4 5 6

maxColumns 3

1 2 3 4 5 6

minColumns 2

1 2 3 4 5 6

Null

1 2 3 4 5 6

minWidth 150 +maxColumns 3

1 2 3 4 5 6

maxWidth 120+minColumns 2

1 2 3 4 5 6

maxWidth 120 + gridSpan -1

1 2 3
) } ``` ## 查询表单实现案例 ```tsx import React, { useMemo, Fragment } from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormProvider, observer } from '@formily/react' import { Form, Input, Select, DatePicker, FormItem, FormGrid, Submit, Reset, FormButtonGroup, } from '@formily/antd' const useCollapseGrid = (maxRows: number) => { const grid = useMemo( () => FormGrid.createFormGrid({ maxColumns: 4, maxWidth: 240, maxRows: maxRows, shouldVisible: (node, grid) => { if (node.index === grid.childSize - 1) return true if (grid.maxRows === Infinity) return true return node.shadowRow < maxRows + 1 }, }), [] ) const expanded = grid.maxRows === Infinity const realRows = grid.shadowRows const computeRows = grid.fullnessLastColumn ? grid.shadowRows - 1 : grid.shadowRows const toggle = () => { if (grid.maxRows === Infinity) { grid.maxRows = maxRows } else { grid.maxRows = Infinity } } const takeType = () => { if (realRows < maxRows + 1) return 'incomplete-wrap' if (computeRows > maxRows) return 'collapsible' return 'complete-wrap' } return { grid, expanded, toggle, type: takeType(), } } const QueryForm: React.FC = observer((props) => { const { grid, expanded, toggle, type } = useCollapseGrid(1) const renderActions = () => { return ( 查询 重置 ) } const renderButtonGroup = () => { if (type === 'incomplete-wrap') { return ( {renderActions()} ) } if (type === 'collapsible') { return ( { e.preventDefault() toggle() }} > {expanded ? '收起' : '展开'} {renderActions()} ) } return ( {renderActions()} ) } return (
{props.children} {renderButtonGroup()}
) }) const SchemaField = createSchemaField({ components: { QueryForm, Input, Select, DatePicker, FormItem, }, }) export default () => { const form = useMemo(() => createForm(), []) return ( ) } ``` ## API ### FormGrid | 属性名 | 类型 | 描述 | 默认值 | | ------------- | ---------------------- | -------------------------------------------------------------- | ----------------- | | minWidth | `number \| number[]` | 元素最小宽度 | 100 | | maxWidth | `number \| number[]` | 元素最大宽度 | - | | minColumns | `number \| number[]` | 最小列数 | 0 | | maxColumns | `number \| number[]` | 最大列数 | - | | breakpoints | number[] | 容器尺寸断点 | `[720,1280,1920]` | | columnGap | number | 列间距 | 8 | | rowGap | number | 行间距 | 4 | | colWrap | boolean | 自动换行 | true | | strictAutoFit | boolean | GridItem 宽度是否严格受限于 maxWidth,不受限的话会自动占满容器 | false | | shouldVisible | `(node,grid)=>boolean` | 是否需要显示当前节点 | `()=>true` | | grid | `Grid` | 外部传入 Grid 实例,用于实现更复杂的布局逻辑 | - | 注意: - minWidth 生效优先级高于 minColumn - maxWidth 优先级高于 maxColumn - minWidth/maxWidth/minColumns/maxColumns 的数组格式代表与断点数组映射 ### FormGrid.GridColumn | 属性名 | 类型 | 描述 | 默认值 | | -------- | ------ | ---------------------------------------------------- | ------ | | gridSpan | number | 元素所跨列数,如果为-1,那么会自动反向跨列填补单元格 | 1 | ### FormGrid.createFormGrid 从上下文中读取 Grid 实例 ```ts interface createFormGrid { (props: IGridProps): Grid } ``` - IGridProps 参考 FormGrid 属性 - Grid 实例属性方法参考 https://github.com/alibaba/formily/tree/formily_next/packages/grid ### FormGrid.useFormGrid 从上下文中读取 Grid 实例 ```ts interface useFormGrid { (): Grid } ``` - Grid 实例属性方法参考 https://github.com/alibaba/formily/tree/formily_next/packages/grid ================================================ FILE: packages/antd/docs/components/FormItem.md ================================================ # FormItem > The brand-new FormItem component, compared to Antd's FormItem, it supports more functions. At the same time, it is positioned as a pure style component and does not manage the state of the form, so it will be lighter and more convenient for customization ## Markup Schema example ```tsx import React from 'react' import { Input, Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, Select, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { input: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { style: { width: 240, }, }, }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## Commonly used attribute cases ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, FormItem, NumberPicker, Switch, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const Title = (props) =>

{props.text}

const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, NumberPicker, Switch, Radio, FormItem, Title, }, }) const form = createForm() export default () => { return ( ) } ``` ## Required style ```tsx import React, { useState } from 'react' import { Input, FormItem, FormLayout } from '@formily/antd' import { Radio } from 'antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => { const [requiredMark, setRequiredMark] = useState(true) return (

Required Mark: setRequiredMark(e.target.value)} > optional true false

) } ``` ## Borderless case Set to remove the component border ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, FormItem, NumberPicker, Switch, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const Title = (props) =>

{props.text}

const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, NumberPicker, Switch, Radio, FormItem, Title, }, }) const form = createForm() export default () => { return ( ) } ``` ## Embedded mode case Set the form component to inline mode ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, FormItem, NumberPicker, Switch, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const Title = (props) =>

{props.text}

const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, NumberPicker, Switch, Radio, FormItem, Title, }, }) const form = createForm() export default () => { return ( ) } ``` ## Feedback Customization Case The button for specifying feedback can be passed in through `feedbackIcon` ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, TimePicker, FormItem, FormLayout, NumberPicker, Switch, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { CheckCircleFilled, LoadingOutlined } from '@ant-design/icons' const Title = (props) =>

{props.text}

const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, TimePicker, NumberPicker, Switch, Radio, FormItem, Title, FormLayout, }, }) const form = createForm() export default () => { return ( , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> ) } ``` ## Size control case ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, FormItem, NumberPicker, Switch, } from '@formily/antd' import { createForm, onFieldChange } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const Div = (props) =>
const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, NumberPicker, Switch, Radio, FormItem, Div, }, }) const form = createForm({ values: { size: 'default', }, effects: () => { onFieldChange('size', ['value'], (field, form) => { form.setFieldState('sizeWrap.*', (state) => { if (state.decorator[1]) { state.decorator[1].size = field.value } }) }) }, }) export default () => { return ( ) } ``` ## API ### FormItem | Property name | Type | Description | Default value | | --------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | label | ReactNode | label | - | | style | CSSProperties | Style | - | | labelStyle | CSSProperties | Label style | - | | wrapperStyle | CSSProperties | Component container style | - | | className | string | Component style class name | - | | colon | boolean | colon | true | | tooltip | ReactNode | Question mark prompt | - | | tooltipLayout | `"icon" \| "text"` | Ask the prompt layout | `"icon"` | | tooltipIcon | ReactNode | Ask the prompt icon | `?` | | labelAlign | `"left"` \| `"right"` | Label text alignment | `"right"` | | labelWrap | boolean | Label change, otherwise an ellipsis appears, hover has tooltip | false | | labelWidth | `number \| string` | Label fixed width | - | | wrapperWidth | `number \| string` | Content fixed width | - | | labelCol | number | The number of columns occupied by the label grid, and the number of content columns add up to 24 | - | | wrapperCol | number | The number of columns occupied by the content grid, and the number of label columns add up to 24 | - | | wrapperAlign | `"left"` \| `"right"` | Content text alignment | `"left"` | | wrapperWrap | boolean | Change the content, otherwise an ellipsis appears, and hover has tooltip | false | | fullness | boolean | fullness | false | | addonBefore | ReactNode | Prefix content | - | | addonAfter | ReactNode | Suffix content | - | | size | `"small"` \| `"default"` \| `"large"` | size | - | | inset | boolean | Is it an inline layout | false | | extra | ReactNode | Extended description script | - | | feedbackText | ReactNode | Feedback Case | - | | feedbackLayout | `"loose"` \| `"terse"` \| `"popover" \| "none"` | Feedback layout | - | | feedbackStatus | `"error"` \| `"warning"` \| `"success"` \| `"pending"` | Feedback layout | - | | feedbackIcon | ReactNode | Feedback icon | - | | enableOutlineFeedback | boolean | Enable the border color style of the abnormal state, it is recommended to turn off this item when there is a sub-form in the custom component | true | | getPopupContainer | function(triggerNode) | when `feedbackLayout` is popover, The DOM container of the tip, the default behavior is to create a div element in body | () => document.body | | asterisk | boolean | Asterisk reminder | - | | gridSpan | number | Grid layout occupies width | - | | bordered | boolean | Is there a border | - | ### FormItem.BaseItem Pure style components, the properties are the same as FormItem, and Formily Core does not do state bridging. It is mainly used for scenarios that need to rely on the style layout capabilities of FormItem but do not want to access the Field state. ================================================ FILE: packages/antd/docs/components/FormItem.zh-CN.md ================================================ # FormItem > 全新的 FormItem 组件,相比于 Antd 的 FormItem,它支持的功能更多,同时它的定位是纯样式组件,不管理表单状态,所以也会更轻量,更方便定制 ## Markup Schema 案例 ```tsx import React from 'react' import { Input, Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, Select, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { input: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { style: { width: 240, }, }, }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## 常用属性案例 ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, FormItem, NumberPicker, Switch, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const Title = (props) =>

{props.text}

const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, NumberPicker, Switch, Radio, FormItem, Title, }, }) const form = createForm() export default () => { return ( ) } ``` ## 必填样式 ```tsx import React, { useState } from 'react' import { Input, FormItem, FormLayout } from '@formily/antd' import { Radio, ConfigProvider } from 'antd' import zhCN from 'antd/es/locale/zh_CN' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => { const [requiredMark, setRequiredMark] = useState(true) return (

Required Mark: setRequiredMark(e.target.value)} > optional true false

) } ``` ## 无边框案例 设置去除组件边框 ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, FormItem, NumberPicker, Switch, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const Title = (props) =>

{props.text}

const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, NumberPicker, Switch, Radio, FormItem, Title, }, }) const form = createForm() export default () => { return ( ) } ``` ## 内嵌模式案例 设置表单组件为内嵌模式 ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, FormItem, NumberPicker, Switch, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const Title = (props) =>

{props.text}

const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, NumberPicker, Switch, Radio, FormItem, Title, }, }) const form = createForm() export default () => { return ( ) } ``` ## 反馈信息定制案例 可通过 `feedbackIcon` 传入指定反馈的按钮 ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, TimePicker, FormItem, FormLayout, NumberPicker, Switch, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { CheckCircleFilled, LoadingOutlined } from '@ant-design/icons' const Title = (props) =>

{props.text}

const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, TimePicker, NumberPicker, Switch, Radio, FormItem, Title, FormLayout, }, }) const form = createForm() export default () => { return ( , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> ) } ``` ## 尺寸控制案例 ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, FormItem, NumberPicker, Switch, } from '@formily/antd' import { createForm, onFieldChange } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const Div = (props) =>
const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, NumberPicker, Switch, Radio, FormItem, Div, }, }) const form = createForm({ values: { size: 'default', }, effects: () => { onFieldChange('size', ['value'], (field, form) => { form.setFieldState('sizeWrap.*', (state) => { if (state.decorator[1]) { state.decorator[1].size = field.value } }) }) }, }) export default () => { return ( ) } ``` ## API ### FormItem | 属性名 | 类型 | 描述 | 默认值 | | --------------------- | ------------------------------------------------------ | ------------------------------------------------------------------- | ------------------- | | label | ReactNode | 标签 | - | | style | CSSProperties | 样式 | - | | labelStyle | CSSProperties | 标签样式 | - | | wrapperStyle | CSSProperties | 组件容器样式 | - | | className | string | 组件样式类名 | - | | colon | boolean | 冒号 | true | | tooltip | ReactNode | 问号提示 | - | | tooltipLayout | `"icon" \| "text"` | 问号提示布局 | `"icon"` | | tooltipIcon | ReactNode | 问号提示图标 | `?` | | labelAlign | `"left"` \| `"right"` | 标签文本对齐方式 | `"right"` | | labelWrap | boolean | 标签换⾏,否则出现省略号,hover 有 tooltip | false | | labelWidth | `number \| string` | 标签固定宽度 | - | | wrapperWidth | `number \| string` | 内容固定宽度 | - | | labelCol | number | 标签⽹格所占列数,和内容列数加起来总和为 24 | - | | wrapperCol | number | 内容⽹格所占列数,和标签列数加起来总和为 24 | - | | wrapperAlign | `"left"` \| `"right"` | 内容文本对齐方式⻬ | `"left"` | | wrapperWrap | boolean | 内容换⾏,否则出现省略号,hover 有 tooltip | false | | fullness | boolean | 内容撑满 | false | | addonBefore | ReactNode | 前缀内容 | - | | addonAfter | ReactNode | 后缀内容 | - | | size | `"small"` \| `"default"` \| `"large"` | 尺⼨ | - | | inset | boolean | 是否是内嵌布局 | false | | extra | ReactNode | 扩展描述⽂案 | - | | feedbackText | ReactNode | 反馈⽂案 | - | | feedbackLayout | `"loose"` \| `"terse"` \| `"popover" \| "none"` | 反馈布局 | - | | feedbackStatus | `"error"` \| `"warning"` \| `"success"` \| `"pending"` | 反馈布局 | - | | feedbackIcon | ReactNode | 反馈图标 | - | | enableOutlineFeedback | boolean | 开启异常状态的边框颜色样式,当自定义组件内存在子表单时建议关闭此项 | true | | getPopupContainer | function(triggerNode) | 当 feedbackLayout 为 popover 时,浮层渲染父节点,默认渲染到 body 上 | () => document.body | | asterisk | boolean | 星号提醒 | - | | gridSpan | number | ⽹格布局占宽 | - | | bordered | boolean | 是否有边框 | - | ### FormItem.BaseItem 纯样式组件,属性与 FormItem 一样,与 Formily Core 不做状态桥接,主要用于一些需要依赖 FormItem 的样式布局能力,但不希望接入 Field 状态的场景 ================================================ FILE: packages/antd/docs/components/FormLayout.md ================================================ # FormLayout > Block-level layout batch control component, with the help of this component, we can easily control the layout mode of all FormItem components enclosed by FormLayout ## Markup Schema example ```tsx import React from 'react' import { Input, Select, FormItem, FormLayout } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, Select, FormItem, FormLayout, }, }) const form = createForm() export default () => ( 123
, }} x-component="Input" required /> ) ``` ## JSON Schema case ```tsx import React from 'react' import { Input, Select, FormItem, FormLayout } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, Select, FormItem, FormLayout, }, }) const schema = { type: 'object', properties: { layout: { type: 'void', 'x-component': 'FormLayout', 'x-component-props': { labelCol: 6, wrapperCol: 10, layout: 'vertical', }, properties: { input: { type: 'string', title: 'input box', required: true, 'x-decorator': 'FormItem', 'x-decorator-props': { tooltip:
123
, }, 'x-component': 'Input', }, select: { type: 'string', title: 'Select box', required: true, 'x-decorator': 'FormItem', 'x-component': 'Select', }, }, }, }, } const form = createForm() export default () => ( ) ``` ## Pure JSX case ```tsx import React from 'react' import { Input, Select, FormItem, FormButtonGroup, Submit, FormLayout, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API | Property name | Type | Description | Default value | | -------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------- | | style | CSSProperties | Style | - | | className | string | class name | - | | colon | boolean | Is there a colon | true | | requiredMark | boolean \| `"optional"` | Required mark style. Can use required mark or optional mark | true | | labelAlign | `'right' \| 'left' \| ('right' \| 'left')[]` | Label content alignment | - | | wrapperAlign | `'right' \| 'left' \| ('right' \| 'left')[]` | Component container content alignment | - | | labelWrap | boolean | Wrap label content | false | | labelWidth | number | Label width (px) | - | | wrapperWidth | number | Component container width (px) | - | | wrapperWrap | boolean | Component container wrap | false | | labelCol | `number \| number[]` | Label width (24 column) | - | | wrapperCol | `number \| number[]` | Component container width (24 column) | - | | fullness | boolean | Component container width 100% | false | | size | `'small' \|'default' \|'large'` | component size | default | | layout | `'vertical' \| 'horizontal' \| 'inline' \| ('vertical' \| 'horizontal' \| 'inline')[]` | layout mode | horizontal | | direction | `'rtl' \|'ltr'` | direction (not supported yet) | ltr | | inset | boolean | Inline layout | false | | shallow | boolean | shallow context transfer | true | | feedbackLayout | `'loose' \|'terse' \|'popover' \|'none'` | feedback layout | true | | tooltipLayout | `"icon" \| "text"` | Ask the prompt layout | `"icon"` | | tooltipIcon | ReactNode | Ask the prompt icon | - | | bordered | boolean | Is there a border | true | | breakpoints | number[] | Container size breakpoints | - | | gridColumnGap | number | Grid Column Gap | 8 | | gridRowGap | number | Grid Row Gap | 4 | | spaceGap | number | Space Gap | 8 | ================================================ FILE: packages/antd/docs/components/FormLayout.zh-CN.md ================================================ # FormLayout > 区块级布局批量控制组件,借助该组件,我们可以轻松的控制被 FormLayout 圈住的所有 FormItem 组件的布局模式 ## Markup Schema 案例 ```tsx import React from 'react' import { Input, Select, FormItem, FormLayout } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, Select, FormItem, FormLayout, }, }) const form = createForm() export default () => ( 123
, }} x-component="Input" required />
) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Input, Select, FormItem, FormLayout } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, Select, FormItem, FormLayout, }, }) const schema = { type: 'object', properties: { layout: { type: 'void', 'x-component': 'FormLayout', 'x-component-props': { labelCol: 6, wrapperCol: 10, layout: 'vertical', }, properties: { input: { type: 'string', title: '输入框', required: true, 'x-decorator': 'FormItem', 'x-decorator-props': { tooltip:
123
, }, 'x-component': 'Input', }, select: { type: 'string', title: '选择框', required: true, 'x-decorator': 'FormItem', 'x-component': 'Select', }, }, }, }, } const form = createForm() export default () => ( ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Input, Select, FormItem, FormButtonGroup, Submit, FormLayout, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API | 属性名 | 类型 | 描述 | 默认值 | | -------------- | -------------------------------------------------------------------------------------- | ---------------------------------------- | ---------- | | style | CSSProperties | 样式 | - | | className | string | 类名 | - | | colon | boolean | 是否有冒号 | true | | requiredMark | boolean \| `"optional"` | 必选样式,可以切换为必选或者可选展示样式 | true | | labelAlign | `'right' \| 'left' \| ('right' \| 'left')[]` | 标签内容对齐 | - | | wrapperAlign | `'right' \| 'left' \| ('right' \| 'left')[]` | 组件容器内容对齐 | - | | labelWrap | boolean | 标签内容换行 | false | | labelWidth | number | 标签宽度(px) | - | | wrapperWidth | number | 组件容器宽度(px) | - | | wrapperWrap | boolean | 组件容器换行 | false | | labelCol | `number \| number[]` | 标签宽度(24 column) | - | | wrapperCol | `number \| number[]` | 组件容器宽度(24 column) | - | | fullness | boolean | 组件容器宽度 100% | false | | size | `'small' \| 'default' \| 'large'` | 组件尺寸 | default | | layout | `'vertical' \| 'horizontal' \| 'inline' \| ('vertical' \| 'horizontal' \| 'inline')[]` | 布局模式 | horizontal | | direction | `'rtl' \| 'ltr'` | 方向(暂不支持) | ltr | | inset | boolean | 内联布局 | false | | shallow | boolean | 上下文浅层传递 | true | | feedbackLayout | `'loose' \| 'terse' \| 'popover' \| 'none'` | 反馈布局 | true | | tooltipLayout | `"icon" \| "text"` | 问号提示布局 | `"icon"` | | tooltipIcon | ReactNode | 问号提示图标 | - | | bordered | boolean | 是否有边框 | true | | breakpoints | number[] | 容器尺寸断点 | - | | gridColumnGap | number | 网格布局列间距 | 8 | | gridRowGap | number | 网格布局行间距 | 4 | | spaceGap | number | 弹性间距 | 8 | ================================================ FILE: packages/antd/docs/components/FormStep.md ================================================ # FormStep > Step-by-step form components > > Note: This component can only be used in Schema scenarios ## Markup Schema example ```tsx import React from 'react' import { FormStep, FormItem, Input, FormButtonGroup } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, FormConsumer, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, FormStep, Input, }, }) const form = createForm() const formStep = FormStep.createFormStep() export default () => { return ( {() => ( )} ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormStep, FormItem, Input, FormButtonGroup } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, FormConsumer, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, FormStep, Input, }, }) const form = createForm() const formStep = FormStep.createFormStep() const schema = { type: 'object', properties: { step: { type: 'void', 'x-component': 'FormStep', 'x-component-props': { formStep: '{{formStep}}', }, properties: { step1: { type: 'void', 'x-component': 'FormStep.StepPane', 'x-component-props': { title: 'First Step', }, properties: { aaa: { type: 'string', title: 'AAA', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, step2: { type: 'void', 'x-component': 'FormStep.StepPane', 'x-component-props': { title: 'Second Step', }, properties: { bbb: { type: 'string', title: 'AAA', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, step3: { type: 'void', 'x-component': 'FormStep.StepPane', 'x-component-props': { title: 'The third step', }, properties: { ccc: { type: 'string', title: 'AAA', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, }, }, }, } export default () => { return ( {() => ( )} ) } ``` ## API ### FormStep | Property name | Type | Description | Default value | | ------------- | --------- | ------------------------------------------------------- | ------------- | | formStep | IFormStep | Pass in the model created by createFormStep/useFormStep | | Other references https://ant.design/components/steps-cn/ ### FormStep.StepPane Refer to https://ant.design/components/steps-cn/ Steps.Step properties ### FormStep.createFormStep ```ts pure import { Form } from '@formily/core' interface createFormStep { (current?: number): IFormStep } interface IFormTab { //Current index current: number //Whether to allow backwards allowNext: boolean //Whether to allow forward allowBack: boolean //Set the current index setCurrent(key: number): void //submit Form submit: Form['submit'] //backward next(): void //forward back(): void } ``` ================================================ FILE: packages/antd/docs/components/FormStep.zh-CN.md ================================================ # FormStep > 分步表单组件 > > 注意:该组件只能用在 Schema 场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormStep, FormItem, Input, FormButtonGroup } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, FormConsumer, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, FormStep, Input, }, }) const form = createForm() const formStep = FormStep.createFormStep() export default () => { return ( {() => ( )} ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormStep, FormItem, Input, FormButtonGroup } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, FormConsumer, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, FormStep, Input, }, }) const form = createForm() const formStep = FormStep.createFormStep() const schema = { type: 'object', properties: { step: { type: 'void', 'x-component': 'FormStep', 'x-component-props': { formStep: '{{formStep}}', }, properties: { step1: { type: 'void', 'x-component': 'FormStep.StepPane', 'x-component-props': { title: '第一步', }, properties: { aaa: { type: 'string', title: 'AAA', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, step2: { type: 'void', 'x-component': 'FormStep.StepPane', 'x-component-props': { title: '第二步', }, properties: { bbb: { type: 'string', title: 'AAA', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, step3: { type: 'void', 'x-component': 'FormStep.StepPane', 'x-component-props': { title: '第三步', }, properties: { ccc: { type: 'string', title: 'AAA', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, }, }, }, } export default () => { return ( {() => ( )} ) } ``` ## API ### FormStep | 属性名 | 类型 | 描述 | 默认值 | | -------- | --------- | -------------------------------------------------- | ------ | | formStep | IFormStep | 传入通过 createFormStep/useFormStep 创建出来的模型 | | 其余参考 https://ant.design/components/steps-cn/ ### FormStep.StepPane 参考 https://ant.design/components/steps-cn/ Steps.Step 属性 ### FormStep.createFormStep ```ts pure import { Form } from '@formily/core' interface createFormStep { (current?: number): IFormStep } interface IFormTab { //当前索引 current: number //是否允许向后 allowNext: boolean //是否允许向前 allowBack: boolean //设置当前索引 setCurrent(key: number): void //提交表单 submit: Form['submit'] //向后 next(): void //向前 back(): void } ``` ================================================ FILE: packages/antd/docs/components/FormTab.md ================================================ # FormTab > Tab form > > Note: This component is only applicable to Schema scenarios ## Markup Schema example ```tsx import React from 'react' import { FormTab, FormItem, Input, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, FormTab, Input, }, }) const form = createForm() const formTab = FormTab.createFormTab() export default () => { return ( Submit ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormTab, FormItem, Input, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, FormTab, Input, }, }) const form = createForm() const formTab = FormTab.createFormTab() const schema = { type: 'object', properties: { collapse: { type: 'void', 'x-component': 'FormTab', 'x-component-props': { formTab: '{{formTab}}', }, properties: { tab1: { type: 'void', 'x-component': 'FormTab.TabPane', 'x-component-props': { tab: 'A1', }, properties: { aaa: { type: 'string', title: 'AAA', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, tab2: { type: 'void', 'x-component': 'FormTab.TabPane', 'x-component-props': { tab: 'A2', }, properties: { bbb: { type: 'string', title: 'BBB', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, tab3: { type: 'void', 'x-component': 'FormTab.TabPane', 'x-component-props': { tab: 'A3', }, properties: { ccc: { type: 'string', title: 'CCC', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, }, }, }, } export default () => { return ( Submit ) } ``` ## API ### FormTab | Property name | Type | Description | Default value | | ------------- | -------- | ----------------------------------------------------- | ------------- | | formTab | IFormTab | Pass in the model created by createFormTab/useFormTab | | Other references https://ant.design/components/tabs-cn/ ### FormTab.TabPane Reference https://ant.design/components/tabs-cn/ ### FormTab.createFormTab ```ts pure type ActiveKey = string | number interface createFormTab { (defaultActiveKey?: ActiveKey): IFormTab } interface IFormTab { //Activate the primary key activeKey: ActiveKey //Set the activation key setActiveKey(key: ActiveKey): void } ``` ================================================ FILE: packages/antd/docs/components/FormTab.zh-CN.md ================================================ # FormTab > 选项卡表单 > > 注意:该组件只适用于 Schema 场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormTab, FormItem, Input, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, FormTab, Input, }, }) const form = createForm() const formTab = FormTab.createFormTab() export default () => { return ( 提交 ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormTab, FormItem, Input, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, FormTab, Input, }, }) const form = createForm() const formTab = FormTab.createFormTab() const schema = { type: 'object', properties: { collapse: { type: 'void', 'x-component': 'FormTab', 'x-component-props': { formTab: '{{formTab}}', }, properties: { tab1: { type: 'void', 'x-component': 'FormTab.TabPane', 'x-component-props': { tab: 'A1', }, properties: { aaa: { type: 'string', title: 'AAA', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, tab2: { type: 'void', 'x-component': 'FormTab.TabPane', 'x-component-props': { tab: 'A2', }, properties: { bbb: { type: 'string', title: 'BBB', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, tab3: { type: 'void', 'x-component': 'FormTab.TabPane', 'x-component-props': { tab: 'A3', }, properties: { ccc: { type: 'string', title: 'CCC', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, }, }, }, } export default () => { return ( 提交 ) } ``` ## API ### FormTab | 属性名 | 类型 | 描述 | 默认值 | | ------- | -------- | ------------------------------------------------ | ------ | | formTab | IFormTab | 传入通过 createFormTab/useFormTab 创建出来的模型 | | 其余参考 https://ant.design/components/tabs-cn/ ### FormTab.TabPane 参考 https://ant.design/components/tabs-cn/ ### FormTab.createFormTab ```ts pure type ActiveKey = string | number interface createFormTab { (defaultActiveKey?: ActiveKey): IFormTab } interface IFormTab { //激活主键 activeKey: ActiveKey //设置激活主键 setActiveKey(key: ActiveKey): void } ``` ================================================ FILE: packages/antd/docs/components/Input.md ================================================ # Input > Text input box ## Markup Schema example ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { input: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { style: { width: 240, }, }, }, textarea: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Input.TextArea', 'x-component-props': { style: { width: 240, }, }, }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://ant.design/components/input-cn/ ================================================ FILE: packages/antd/docs/components/Input.zh-CN.md ================================================ # Input > 文本输入框 ## Markup Schema 案例 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { input: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { style: { width: 240, }, }, }, textarea: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'Input.TextArea', 'x-component-props': { style: { width: 240, }, }, }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://ant.design/components/input-cn/ ================================================ FILE: packages/antd/docs/components/NumberPicker.md ================================================ # NumberPicker > Number input box ## Markup Schema example ```tsx import React from 'react' import { NumberPicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { NumberPicker, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { NumberPicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { NumberPicker, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { input: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', 'x-component-props': { style: { width: 240, }, }, }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { NumberPicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://ant.design/components/input-number-cn/ ================================================ FILE: packages/antd/docs/components/NumberPicker.zh-CN.md ================================================ # NumberPicker > 数字输入框 ## Markup Schema 案例 ```tsx import React from 'react' import { NumberPicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { NumberPicker, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { NumberPicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { NumberPicker, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { input: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', 'x-component-props': { style: { width: 240, }, }, }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { NumberPicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://ant.design/components/input-number-cn/ ================================================ FILE: packages/antd/docs/components/Password.md ================================================ # Password > Password input box ## Markup Schema example ```tsx import React from 'react' import { Password, FormItem, FormLayout, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Password, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Password, FormItem, FormLayout, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Password, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { input: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Password', 'x-component-props': { checkStrength: true, }, }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Password, FormItem, FormLayout, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://ant.design/components/input-cn/ ================================================ FILE: packages/antd/docs/components/Password.zh-CN.md ================================================ # Password > 密码输入框 ## Markup Schema 案例 ```tsx import React from 'react' import { Password, FormItem, FormLayout, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Password, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Password, FormItem, FormLayout, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Password, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { input: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'Password', 'x-component-props': { checkStrength: true, }, }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Password, FormItem, FormLayout, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://ant.design/components/input-cn/ ================================================ FILE: packages/antd/docs/components/PreviewText.md ================================================ # PreviewText > Reading state components, mainly used to implement the reading state of these components of class Input and DatePicker ## Simple use case ```tsx import React from 'react' import { PreviewText, FormItem, FormLayout } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, PreviewText, }, }) const form = createForm() export default () => { return ( ) } ``` ## Extended reading mode ```tsx import React from 'react' import { PreviewText, FormItem, FormLayout, FormButtonGroup, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, mapReadPretty, connect, createSchemaField, } from '@formily/react' import { Button, Input as AntdInput } from 'antd' const Input = connect(AntdInput, mapReadPretty(PreviewText.Input)) const SchemaField = createSchemaField({ components: { Input, FormItem, PreviewText, }, }) const form = createForm() export default () => { return ( ) } ``` ## API ### PreviewText.Input Reference https://ant.design/components/input-cn/ ### PreviewText.Select Reference https://ant.design/components/select-cn/ ### PreviewText.TreeSelect Reference https://ant.design/components/tree-select-cn/ ### PreviewText.Cascader Reference https://ant.design/components/cascader-cn/ ### PreviewText.DatePicker Reference https://ant.design/components/date-picker-cn/ ### PreviewText.DateRangePicker Reference https://ant.design/components/date-picker-cn/ ### PreviewText.TimePicker Reference https://ant.design/components/time-picker-cn/ ### PreviewText.TimeRangePicker Reference https://ant.design/components/time-picker-cn/ ### PreviewText.NumberPicker 参考 https://ant.design/components/input-number-cn/ ### PreviewText.Placeholder | Property name | Type | Description | Default value | | ------------- | ------ | ------------------- | ------------- | | value | stirng | Default placeholder | N/A | ### PreviewText.usePlaceholder ```ts pure interface usePlaceholder { (): string } ``` ================================================ FILE: packages/antd/docs/components/PreviewText.zh-CN.md ================================================ # PreviewText > 阅读态组件,主要用来实现类 Input,类 DatePicker 这些组件的阅读态 ## 简单用例 ```tsx import React from 'react' import { PreviewText, FormItem, FormLayout } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, PreviewText, }, }) const form = createForm() export default () => { return ( ) } ``` ## 扩展阅读态 ```tsx import React from 'react' import { PreviewText, FormItem, FormLayout, FormButtonGroup, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, mapReadPretty, connect, createSchemaField, } from '@formily/react' import { Button, Input as AntdInput } from 'antd' const Input = connect(AntdInput, mapReadPretty(PreviewText.Input)) const SchemaField = createSchemaField({ components: { Input, FormItem, PreviewText, }, }) const form = createForm() export default () => { return ( ) } ``` ## API ### PreviewText.Input 参考 https://ant.design/components/input-cn/ ### PreviewText.Select 参考 https://ant.design/components/select-cn/ ### PreviewText.TreeSelect 参考 https://ant.design/components/tree-select-cn/ ### PreviewText.Cascader 参考 https://ant.design/components/cascader-cn/ ### PreviewText.DatePicker 参考 https://ant.design/components/date-picker-cn/ ### PreviewText.DateRangePicker 参考 https://ant.design/components/date-picker-cn/ ### PreviewText.TimePicker 参考 https://ant.design/components/time-picker-cn/ ### PreviewText.TimeRangePicker 参考 https://ant.design/components/time-picker-cn/ ### PreviewText.NumberPicker 参考 https://ant.design/components/input-number-cn/ ### PreviewText.Placeholder | 属性名 | 类型 | 描述 | 默认值 | | ------ | ------ | ---------- | ------ | | value | stirng | 缺省占位符 | N/A | ### PreviewText.usePlaceholder ```ts pure interface usePlaceholder { (): string } ``` ================================================ FILE: packages/antd/docs/components/Radio.md ================================================ # Radio > Single selection box ## Markup Schema example ```tsx import React from 'react' import { Radio, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Radio, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Radio, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Radio, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { radio: { type: 'number', title: 'Single selection', enum: [ { label: 'Option 1', value: 1, }, { label: 'Option 2', value: 2, }, ], 'x-decorator': 'FormItem', 'x-component': 'Radio.Group', }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Radio, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://ant.design/components/radio-cn/ ================================================ FILE: packages/antd/docs/components/Radio.zh-CN.md ================================================ # Radio > 单选框 ## Markup Schema 案例 ```tsx import React from 'react' import { Radio, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Radio, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Radio, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Radio, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { radio: { type: 'number', title: '单选', enum: [ { label: '选项1', value: 1, }, { label: '选项2', value: 2, }, ], 'x-decorator': 'FormItem', 'x-component': 'Radio.Group', }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Radio, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://ant.design/components/radio-cn/ ================================================ FILE: packages/antd/docs/components/Reset.md ================================================ # Reset > Reset button ## Normal reset > Controls with default values cannot be cleared ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Reset } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( Reset ) ``` ## Force empty reset ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Reset } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( Reset ) ``` ## Reset and verify ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Reset } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( Reset ) ``` ## Force empty reset and verify ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Reset } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( Reset ) ``` ## API ### Reset Other API reference https://ant.design/components/button-cn/ | Property name | Type | Description | Default value | | ---------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | ------------- | | onClick | `(event: MouseEvent) => void \| boolean` | Click event, if it returns false, it can block resetting | - | | onResetValidateSuccess | (payload: any) => void | Reset validation success event | - | | onResetValidateFailed | (feedbacks: [IFormFeedback](https://core.formilyjs.org/api/models/form#iformfeedback)[]) => void | Reset validation failure event | - | ================================================ FILE: packages/antd/docs/components/Reset.zh-CN.md ================================================ # Reset > 重置按钮 ## 普通重置 > 有默认值的控件无法被清空 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Reset } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( 重置 ) ``` ## 强制清空重置 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Reset } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( 重置 ) ``` ## 重置并校验 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Reset } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( 重置 ) ``` ## 强制清空重置并校验 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Reset } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( 重置 ) ``` ## API ### Reset 其余 API 参考 https://ant.design/components/button-cn/ | 属性名 | 类型 | 描述 | 默认值 | | ---------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------- | ------ | | onClick | `(event: MouseEvent) => void \| boolean` | 点击事件,如果返回 false 可以阻塞重置 | - | | onResetValidateSuccess | (payload: any) => void | 重置校验成功事件 | - | | onResetValidateFailed | (feedbacks: [IFormFeedback](https://core.formilyjs.org/zh-CN/api/models/form#iformfeedback)[]) => void | 重置校验失败事件 | - | ================================================ FILE: packages/antd/docs/components/Select.md ================================================ # Select > Drop-down box components ## Markup Schema synchronization data source case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## Markup Schema Asynchronous Search Case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm, onFieldReact, onFieldInit, FormPathPattern, Field, } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action, observable } from '@formily/reactive' import { fetch } from 'mfetch' let timeout let currentValue function fetchData(value, callback) { if (timeout) { clearTimeout(timeout) timeout = null } currentValue = value function fake() { fetch(`https://suggest.taobao.com/sug?q=${value}`, { method: 'jsonp', }) .then((response) => response.json()) .then((d) => { if (currentValue === value) { const { result } = d const data = [] result.forEach((r) => { data.push({ value: r[0], text: r[0], }) }) callback(data) } }) } timeout = setTimeout(fake, 300) } const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const useAsyncDataSource = ( pattern: FormPathPattern, service: (param: { keyword: string field: Field }) => Promise<{ label: string; value: any }[]> ) => { const keyword = observable.ref('') onFieldInit(pattern, (field) => { field.setComponentProps({ onSearch: (value) => { keyword.value = value }, }) }) onFieldReact(pattern, (field) => { field.loading = true service({ field, keyword: keyword.value }).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async ({ keyword }) => { if (!keyword) { return [] } return new Promise((resolve) => { fetchData(keyword, resolve) }) }) }, }) export default () => ( Submit ) ``` ## Markup Schema Asynchronous Linkage Data Source Case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm, onFieldReact, FormPathPattern, Field } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: Field) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', }, { label: 'BBB', value: 'ccc', }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', }, { label: 'DDD', value: 'ddd', }, ]) } }, 1500) }) }) }, }) export default () => ( Submit ) ``` ## JSON Schema synchronization data source case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { select: { type: 'string', title: 'Select box', 'x-decorator': 'FormItem', 'x-component': 'Select', enum: [ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 }, ], 'x-component-props': { style: { width: 120, }, }, }, }, } export default () => ( Submit ) ``` ## JSON Schema asynchronous linkage data source case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const loadData = async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', }, { label: 'BBB', value: 'ccc', }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', }, { label: 'DDD', value: 'ddd', }, ]) } }, 1500) }) } const useAsyncDataSource = (service) => (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) } const form = createForm() const schema = { type: 'object', properties: { linkage: { type: 'string', title: 'Linkage selection box', enum: [ { label: 'Request 1', value: 1 }, { label: 'Request 2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 120, }, }, }, select: { type: 'string', title: 'Asynchronous selection box', 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 120, }, }, 'x-reactions': ['{{useAsyncDataSource(loadData)}}'], }, }, } export default () => ( Submit ) ``` ## Pure JSX synchronization data source case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## Pure JSX asynchronous linkage data source case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm, onFieldReact, FormPathPattern, Field as FieldType, } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { action } from '@formily/reactive' const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: FieldType) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', }, { label: 'BBB', value: 'ccc', }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', }, { label: 'DDD', value: 'ddd', }, ]) } }, 1500) }) }) }, }) export default () => ( Submit ) ``` ## API Reference https://ant.design/components/select-cn/ ================================================ FILE: packages/antd/docs/components/Select.zh-CN.md ================================================ # Select > 下拉框组件 ## Markup Schema 同步数据源案例 ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## Markup Schema 异步搜索案例 ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm, onFieldReact, onFieldInit, FormPathPattern, Field, } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action, observable } from '@formily/reactive' import { fetch } from 'mfetch' let timeout let currentValue function fetchData(value, callback) { if (timeout) { clearTimeout(timeout) timeout = null } currentValue = value function fake() { fetch(`https://suggest.taobao.com/sug?q=${value}`, { method: 'jsonp', }) .then((response) => response.json()) .then((d) => { if (currentValue === value) { const { result } = d const data = [] result.forEach((r) => { data.push({ value: r[0], text: r[0], }) }) callback(data) } }) } timeout = setTimeout(fake, 300) } const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const useAsyncDataSource = ( pattern: FormPathPattern, service: (param: { keyword: string field: Field }) => Promise<{ label: string; value: any }[]> ) => { const keyword = observable.ref('') onFieldInit(pattern, (field) => { field.setComponentProps({ onSearch: (value) => { keyword.value = value }, }) }) onFieldReact(pattern, (field) => { field.loading = true service({ field, keyword: keyword.value }).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async ({ keyword }) => { if (!keyword) { return [] } return new Promise((resolve) => { fetchData(keyword, resolve) }) }) }, }) export default () => ( 提交 ) ``` ## Markup Schema 异步联动数据源案例 ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm, onFieldReact, FormPathPattern, Field } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: Field) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', }, { label: 'BBB', value: 'ccc', }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', }, { label: 'DDD', value: 'ddd', }, ]) } }, 1500) }) }) }, }) export default () => ( 提交 ) ``` ## JSON Schema 同步数据源案例 ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { select: { type: 'string', title: '选择框', 'x-decorator': 'FormItem', 'x-component': 'Select', enum: [ { label: '选项1', value: 1 }, { label: '选项2', value: 2 }, ], 'x-component-props': { style: { width: 120, }, }, }, }, } export default () => ( 提交 ) ``` ## JSON Schema 异步联动数据源案例 ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const loadData = async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', }, { label: 'BBB', value: 'ccc', }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', }, { label: 'DDD', value: 'ddd', }, ]) } }, 1500) }) } const useAsyncDataSource = (service) => (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) } const form = createForm() const schema = { type: 'object', properties: { linkage: { type: 'string', title: '联动选择框', enum: [ { label: '发请求1', value: 1 }, { label: '发请求2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 120, }, }, }, select: { type: 'string', title: '异步选择框', 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 120, }, }, 'x-reactions': ['{{useAsyncDataSource(loadData)}}'], }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 同步数据源案例 ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## 纯 JSX 异步联动数据源案例 ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm, onFieldReact, FormPathPattern, Field as FieldType, } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { action } from '@formily/reactive' const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: FieldType) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', }, { label: 'BBB', value: 'ccc', }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', }, { label: 'DDD', value: 'ddd', }, ]) } }, 1500) }) }) }, }) export default () => ( 提交 ) ``` ## API 参考 https://ant.design/components/select-cn/ ================================================ FILE: packages/antd/docs/components/SelectTable.md ================================================ # SelectTable > Optional table components ## Markup Schema single case ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, SelectTable, }, }) const form = createForm() export default () => { return ( Submit ) } ``` ## Markup Schema filter case ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, SelectTable, }, }) const form = createForm() export default () => { return ( Submit ) } ``` ## Markup Schema async data source case ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, SelectTable, }, }) const form = createForm() export default () => { const onSearch = (value) => { const field = form.query('selectTable').take() field.loading = true setTimeout(() => { field.setState({ dataSource: [ { key: '3', name: 'AAA' + value, description: 'aaa', }, { key: '4', name: 'BBB' + value, description: 'bbb', }, ], loading: false, }) }, 1500) } return ( Submit ) } ``` ## Markup Schema read-pretty case ```tsx import React from 'react' import { Form, FormItem, FormButtonGroup, Submit, SelectTable, } from '@formily/antd' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, SelectTable, }, }) const form = createForm() export default () => { return (
Submit
) } ``` ## JSON Schema multiple case ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { SelectTable, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { selectTable: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'SelectTable', 'x-component-props': { bordered: false, mode: 'multiple', }, enum: [ { key: '1', name: 'Title-1', description: 'description-1' }, { key: '2', name: 'Title-2', description: 'description-2' }, ], properties: { name: { title: 'Title', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '40%', }, }, description: { title: 'Description', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '60%', }, }, }, }, }, } export default () => ( Submit ) ``` ## JSON Schema custom filter case ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { SelectTable, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { selectTable: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'SelectTable', 'x-component-props': { bordered: false, showSearch: true, primaryKey: 'key', isTree: true, filterOption: (input, option) => option.description.toLowerCase().indexOf(input.toLowerCase()) >= 0, filterSort: (optionA, optionB) => optionA.description .toLowerCase() .localeCompare(optionB.description.toLowerCase()), optionAsValue: true, rowSelection: { checkStrictly: false, }, }, enum: [ { key: '1', name: 'title-1', description: 'A-description' }, { key: '2', name: 'title-2', description: 'X-description', children: [ { key: '2-1', name: 'title2-1', description: 'Y-description', children: [ { key: '2-1-1', name: 'title-2-1-1', description: 'Z-description', }, ], }, { key: '2-2', name: 'title2-2', description: 'YY-description', }, ], }, { key: '3', name: 'title-3', description: 'C-description' }, ], properties: { name: { title: 'Title', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '40%', }, }, description: { title: 'Description', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '60%', }, }, }, }, }, } export default () => ( Submit ) ``` ## JSON Schema async data source case ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { SelectTable, FormItem, }, }) const loadData = async (value) => { return new Promise((resolve) => { setTimeout(() => { resolve([ { key: '3', name: 'AAA' + value, description: 'aaa' }, { key: '4', name: 'BBB' + value, description: 'bbb' }, ]) }, 1500) }) } const useAsyncDataSource = (service, field) => (value) => { field.loading = true service(value).then((data) => { field.setState({ dataSource: data, loading: false, }) }) } const form = createForm() const schema = { type: 'object', properties: { selectTable: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'SelectTable', 'x-component-props': { showSearch: true, filterOption: false, onSearch: '{{useAsyncDataSource(loadData,$self)}}', }, enum: [ { key: '1', name: 'title-1', description: 'description-1' }, { key: '2', name: 'title-2', description: 'description-2' }, ], properties: { name: { title: 'Title', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '40%', }, }, description: { title: 'Description', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '60%', }, }, }, }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API ### SelectTable | Property name | Type | Description | Default value | | ------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | | mode | `'multiple' \| 'single'` | Set mode of SelectTable | `'multiple'` | | valueType | `'all' \| 'parent' \| 'child' \| 'path'` | value type, Only applies when checkStrictly is set to `false` | `'all'` | | optionAsValue | boolean | use `option` as value, Only applies when valueType is not set to `'path'` | false | | showSearch | boolean | show `Search` component | false | | searchProps | object | `Search` component props | - | | primaryKey | `string \| (record) => string` | Row's unique key | `'key'` | | filterOption | `boolean \| (inputValue, option) => boolean` | If true, filter options by input, if function, filter options against it. The function will receive two arguments, `inputValue` and `option`, if the function returns true, the option will be included in the filtered set; Otherwise, it will be excluded | | filterSort | (optionA, optionB) => number | Sort function for search options sorting, see Array.sort's compareFunction | - | | onSearch | Callback function that is fired when input changed | (inputValue) => void | - | `TableProps` type definition reference antd https://ant.design/components/table/ ### rowSelection | Property name | Type | Description | Default value | | ------------- | ------- | -------------------------------------------------------------------------- | ------------- | | checkStrictly | boolean | Check table row precisely; parent row and children rows are not associated | true | `rowSelectionProps` type definition reference antd https://ant.design/components/table/#rowSelection ### SelectTable.Column `ColumnProps` type definition reference antd https://ant.design/components/table/ Table.Column ================================================ FILE: packages/antd/docs/components/SelectTable.zh-CN.md ================================================ # SelectTable > 表格选择组件 ## Markup Schema 单选案例 ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, SelectTable, }, }) const form = createForm() export default () => { return ( 提交 ) } ``` ## Markup Schema 筛选案例 ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, SelectTable, }, }) const form = createForm() export default () => { return ( 提交 ) } ``` ## Markup Schema 异步数据源案例 ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, SelectTable, }, }) const form = createForm() export default () => { const onSearch = (value) => { const field = form.query('selectTable').take() field.loading = true setTimeout(() => { field.setState({ dataSource: [ { key: '3', name: 'AAA' + value, description: 'aaa', }, { key: '4', name: 'BBB' + value, description: 'bbb', }, ], loading: false, }) }, 1500) } return ( 提交 ) } ``` ## Markup Schema 阅读态案例 ```tsx import React from 'react' import { Form, FormItem, FormButtonGroup, Submit, SelectTable, } from '@formily/antd' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, SelectTable, }, }) const form = createForm() export default () => { return (
提交
) } ``` ## JSON Schema 多选案例 ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { SelectTable, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { selectTable: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'SelectTable', 'x-component-props': { bordered: false, mode: 'multiple', }, enum: [ { key: '1', name: '标题1', description: '描述1' }, { key: '2', name: '标题2', description: '描述2' }, ], properties: { name: { title: '标题', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '40%', }, }, description: { title: '描述', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '60%', }, }, }, }, }, } export default () => ( 提交 ) ``` ## JSON Schema 自定义筛选案例 ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { SelectTable, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { selectTable: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'SelectTable', 'x-component-props': { bordered: false, showSearch: true, primaryKey: 'key', isTree: true, filterOption: (input, option) => option.description.toLowerCase().indexOf(input.toLowerCase()) >= 0, filterSort: (optionA, optionB) => optionA.description .toLowerCase() .localeCompare(optionB.description.toLowerCase()), optionAsValue: true, rowSelection: { checkStrictly: false, }, }, enum: [ { key: '1', name: '标题1', description: 'A-描述' }, { key: '2', name: '标题2', description: 'X-描述', children: [ { key: '2-1', name: '标题2-1', description: 'Y-描述', children: [ { key: '2-1-1', name: '标题2-1-1', description: 'Z-描述' }, ], }, { key: '2-2', name: '标题2-2', description: 'YY-描述', }, ], }, { key: '3', name: '标题3', description: 'C-描述' }, ], properties: { name: { title: '标题', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '40%', }, }, description: { title: '描述', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '60%', }, }, }, }, }, } export default () => ( 提交 ) ``` ## JSON Schema 异步数据源案例 ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { SelectTable, FormItem, }, }) const loadData = async (value) => { return new Promise((resolve) => { setTimeout(() => { resolve([ { key: '3', name: 'AAA' + value, description: 'aaa' }, { key: '4', name: 'BBB' + value, description: 'bbb' }, ]) }, 1500) }) } const useAsyncDataSource = (service, field) => (value) => { field.loading = true service(value).then((data) => { field.setState({ dataSource: data, loading: false, }) }) } const form = createForm() const schema = { type: 'object', properties: { selectTable: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'SelectTable', 'x-component-props': { showSearch: true, filterOption: false, onSearch: '{{useAsyncDataSource(loadData,$self)}}', }, enum: [ { key: '1', name: '标题1', description: '描述1' }, { key: '2', name: '标题2', description: '描述2' }, ], properties: { name: { title: '标题', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '40%', }, }, description: { title: '描述', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '60%', }, }, }, }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API ### SelectTable | 属性名 | 类型 | 描述 | 默认值 | | ------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------ | | mode | `'multiple' \| 'single'` | 设置 SelectTable 模式为单选或多选 | `'multiple'` | | valueType | `'all' \| 'parent' \| 'child' \| 'path'` | 返回值类型,checkStrictly 设置为 `false` 时有效 | `'all'` | | optionAsValue | boolean | 使用表格行数据作为值,valueType 值为 `'path'` 时无效 | false | | showSearch | boolean | 是否显示搜索组件 | false | | searchProps | object | Search 组件属性 | - | | primaryKey | `string \| (record) => string` | 表格行 key 的取值 | `'key'` | | filterOption | `boolean \| (inputValue, option) => boolean` | 是否根据输入项进行筛选。当其为一个函数时,会接收 inputValue option 两个参数,当 option 符合筛选条件时,应返回 true,反之则返回 false | true | | filterSort | (optionA, optionB) => number | 搜索时对筛选结果项的排序函数, 类似 Array.sort 里的 compareFunction | - | | onSearch | 文本框值变化时回调 | (inputValue) => void | - | 参考 https://ant.design/components/table-cn/ ### rowSelection | 属性名 | 类型 | 描述 | 默认值 | | ------------- | ------- | ------------------------------------------------------------ | ------ | | checkStrictly | boolean | checkable 状态下节点选择完全受控(父子数据选中状态不再关联) | true | 参考 https://ant.design/components/table/#rowSelection ### SelectTable.Column 参考 https://ant.design/components/table-cn/ Table.Column 属性 ================================================ FILE: packages/antd/docs/components/Space.md ================================================ # Space > Super convenient Flex layout component, can help users quickly realize the layout of any element side by side next to each other ## Markup Schema example ```tsx import React from 'react' import { Input, FormItem, FormLayout, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, Space, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Input, FormItem, FormLayout, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, Space, }, }) const form = createForm() const schema = { type: 'object', properties: { name: { type: 'void', title: 'Name', 'x-decorator': 'FormItem', 'x-decorator-props': { asterisk: true, feedbackLayout: 'none', }, 'x-component': 'Space', properties: { firstName: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', required: true, }, lastName: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', required: true, }, }, }, texts: { type: 'void', title: 'Text concatenation', 'x-decorator': 'FormItem', 'x-decorator-props': { asterisk: true, feedbackLayout: 'none', }, 'x-component': 'Space', properties: { aa: { type: 'string', 'x-decorator': 'FormItem', 'x-decorator-props': { addonAfter: 'Unit', }, 'x-component': 'Input', required: true, }, bb: { type: 'string', 'x-decorator': 'FormItem', 'x-decorator-props': { addonAfter: 'Unit', }, 'x-component': 'Input', required: true, }, cc: { type: 'string', 'x-decorator': 'FormItem', 'x-decorator-props': { addonAfter: 'Unit', }, 'x-component': 'Input', required: true, }, }, }, textarea: { type: 'string', title: 'Text box', 'x-decorator': 'FormItem', 'x-component': 'Input.TextArea', 'x-component-props': { style: { width: 400, }, }, required: true, }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Input, FormItem, FormLayout, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field, VoidField } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://ant.design/components/space-cn/ ================================================ FILE: packages/antd/docs/components/Space.zh-CN.md ================================================ # Space > 超级便捷的 Flex 布局组件,可以帮助用户快速实现任何元素的并排紧挨布局 ## Markup Schema 案例 ```tsx import React from 'react' import { Input, FormItem, FormLayout, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, Space, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Input, FormItem, FormLayout, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, Space, }, }) const form = createForm() const schema = { type: 'object', properties: { name: { type: 'void', title: '姓名', 'x-decorator': 'FormItem', 'x-decorator-props': { asterisk: true, feedbackLayout: 'none', }, 'x-component': 'Space', properties: { firstName: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', required: true, }, lastName: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', required: true, }, }, }, texts: { type: 'void', title: '文本串联', 'x-decorator': 'FormItem', 'x-decorator-props': { asterisk: true, feedbackLayout: 'none', }, 'x-component': 'Space', properties: { aa: { type: 'string', 'x-decorator': 'FormItem', 'x-decorator-props': { addonAfter: '单位', }, 'x-component': 'Input', required: true, }, bb: { type: 'string', 'x-decorator': 'FormItem', 'x-decorator-props': { addonAfter: '单位', }, 'x-component': 'Input', required: true, }, cc: { type: 'string', 'x-decorator': 'FormItem', 'x-decorator-props': { addonAfter: '单位', }, 'x-component': 'Input', required: true, }, }, }, textarea: { type: 'string', title: '文本框', 'x-decorator': 'FormItem', 'x-component': 'Input.TextArea', 'x-component-props': { style: { width: 400, }, }, required: true, }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Input, FormItem, FormLayout, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field, VoidField } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://ant.design/components/space-cn/ ================================================ FILE: packages/antd/docs/components/Submit.md ================================================ # Submit > Submit button ## Ordinary submission ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## Prevent Duplicate Submission (Loading) ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( { return new Promise((resolve) => { setTimeout(() => { console.log(values) resolve() }, 2000) }) }} onSubmitFailed={console.log} > submit ) ``` ## API For button-related API properties, we can refer to https://ant.design/components/button-cn/, and the rest are the unique API properties of the Submit component | Property name | Type | Description | Default value | | --------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------- | ------------- | | onClick | `(event: MouseEvent) => void \| boolean` | Click event, if it returns false, it can block submission | - | | onSubmit | `(values: any) => Promise \| any` | Submit event callback | - | | onSubmitSuccess | (payload: any) => void | Submit successful response event | - | | onSubmitFailed | (feedbacks: [IFormFeedback](https://core.formilyjs.org/api/models/form#iformfeedback)[]) => void | Submit verification failure event callback | - | ================================================ FILE: packages/antd/docs/components/Submit.zh-CN.md ================================================ # Submit > 提交按钮 ## 普通提交 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## 防重复提交(Loading) ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( { return new Promise((resolve) => { setTimeout(() => { console.log(values) resolve() }, 2000) }) }} onSubmitFailed={console.log} > 提交 ) ``` ## API 按钮相关的 API 属性,我们参考 https://ant.design/components/button-cn/ 即可,剩下是 Submit 组件独有的 API 属性 | 属性名 | 类型 | 描述 | 默认值 | | --------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------- | ------ | | onClick | `(event: MouseEvent) => void \| boolean` | 点击事件,如果返回 false 可以阻塞提交 | - | | onSubmit | `(values: any) => Promise \| any` | 提交事件回调 | - | | onSubmitSuccess | (payload: any) => void | 提交成功响应事件 | - | | onSubmitFailed | (feedbacks: [IFormFeedback](https://core.formilyjs.org/zh-CN/api/models/form#iformfeedback)[]) => void | 提交校验失败事件回调 | - | ================================================ FILE: packages/antd/docs/components/Switch.md ================================================ # Switch > Switch Components ## Markup Schema example ```tsx import React from 'react' import { Switch, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Switch, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Switch, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Switch, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { switch: { type: 'boolean', title: 'Switch', 'x-decorator': 'FormItem', 'x-component': 'Switch', }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Switch, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://ant.design/components/switch-cn/ ================================================ FILE: packages/antd/docs/components/Switch.zh-CN.md ================================================ # Switch > 开关组件 ## Markup Schema 案例 ```tsx import React from 'react' import { Switch, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Switch, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Switch, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Switch, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { switch: { type: 'boolean', title: '开关', 'x-decorator': 'FormItem', 'x-component': 'Switch', }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Switch, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://ant.design/components/switch-cn/ ================================================ FILE: packages/antd/docs/components/TimePicker.md ================================================ # TimePicker > Time Picker ## Markup Schema example ```tsx import React from 'react' import { TimePicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TimePicker, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { TimePicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TimePicker, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { time: { title: 'Time', 'x-decorator': 'FormItem', 'x-component': 'TimePicker', type: 'string', }, '[startTime,endTime]': { title: 'Time Range', 'x-decorator': 'FormItem', 'x-component': 'TimePicker.RangePicker', type: 'string', }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { TimePicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://ant.design/components/time-picker-cn/ ================================================ FILE: packages/antd/docs/components/TimePicker.zh-CN.md ================================================ # TimePicker > 时间选择器 ## Markup Schema 案例 ```tsx import React from 'react' import { TimePicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TimePicker, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { TimePicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TimePicker, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { time: { title: '时间', 'x-decorator': 'FormItem', 'x-component': 'TimePicker', type: 'string', }, '[startTime,endTime]': { title: '时间范围', 'x-decorator': 'FormItem', 'x-component': 'TimePicker.RangePicker', type: 'string', }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { TimePicker, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://ant.design/components/time-picker-cn/ ================================================ FILE: packages/antd/docs/components/Transfer.md ================================================ # Transfer > Shuttle Box ## Markup Schema example ```tsx import React from 'react' import { Transfer, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Transfer, FormItem, }, }) const form = createForm() export default () => ( item.title, }} /> Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Transfer, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Transfer, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { transfer: { type: 'array', title: 'shuttle box', 'x-decorator': 'FormItem', 'x-component': 'Transfer', enum: [ { title: 'Option 1', key: 1 }, { title: 'Option 2', key: 2 }, ], 'x-component-props': { render: '{{renderTitle}}', }, }, }, } const renderTitle = (item) => item.title export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Transfer, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( item.title, }, ]} /> Submit ) ``` ## API Reference https://ant.design/components/transfer-cn/ ================================================ FILE: packages/antd/docs/components/Transfer.zh-CN.md ================================================ # Transfer > 穿梭框 ## Markup Schema 案例 ```tsx import React from 'react' import { Transfer, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Transfer, FormItem, }, }) const form = createForm() export default () => ( item.title, }} /> 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Transfer, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Transfer, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { transfer: { type: 'array', title: '穿梭框', 'x-decorator': 'FormItem', 'x-component': 'Transfer', enum: [ { title: '选项1', key: 1 }, { title: '选项2', key: 2 }, ], 'x-component-props': { render: '{{renderTitle}}', }, }, }, } const renderTitle = (item) => item.title export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Transfer, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( item.title, }, ]} /> 提交 ) ``` ## API 参考 https://ant.design/components/transfer-cn/ ================================================ FILE: packages/antd/docs/components/TreeSelect.md ================================================ # TreeSelect > Tree selector ## Markup Schema synchronization data source case ```tsx import React from 'react' import { TreeSelect, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TreeSelect, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## Markup Schema Asynchronous Linkage Data Source Case ```tsx import React from 'react' import { TreeSelect, Select, FormItem, FormButtonGroup, Submit, } from '@formily/antd' import { createForm, onFieldReact, FormPathPattern, Field } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, TreeSelect, FormItem, }, }) const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: Field) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', children: [ { title: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { title: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { title: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'BBB', value: 'ccc', children: [ { title: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { title: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { title: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', children: [ { title: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { title: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { title: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'DDD', value: 'ddd', children: [ { title: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { title: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { title: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } }, 1500) }) }) }, }) export default () => ( Submit ) ``` ## JSON Schema synchronization data source case ```tsx import React from 'react' import { TreeSelect, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TreeSelect, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { select: { type: 'string', title: 'Select box', 'x-decorator': 'FormItem', 'x-component': 'TreeSelect', enum: [ { label: 'Option 1', value: 1, children: [ { title: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { title: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { title: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'Option 2', value: 2, children: [ { title: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { title: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { title: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ], 'x-component-props': { style: { width: 200, }, }, }, }, } export default () => ( Submit ) ``` ## JSON Schema asynchronous linkage data source case ```tsx import React from 'react' import { TreeSelect, Select, FormItem, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, TreeSelect, FormItem, }, }) const loadData = async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', children: [ { title: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { title: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { title: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'BBB', value: 'ccc', children: [ { title: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { title: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { title: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', children: [ { title: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { title: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { title: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'DDD', value: 'ddd', children: [ { title: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { title: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { title: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } }, 1500) }) } const useAsyncDataSource = (service) => (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) } const form = createForm() const schema = { type: 'object', properties: { linkage: { type: 'string', title: 'Linkage selection box', enum: [ { label: 'Request 1', value: 1 }, { label: 'Request 2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 200, }, }, }, select: { type: 'string', title: 'Asynchronous selection box', 'x-decorator': 'FormItem', 'x-component': 'TreeSelect', 'x-component-props': { style: { width: 200, }, }, 'x-reactions': ['{{useAsyncDataSource(loadData)}}'], }, }, } export default () => ( Submit ) ``` ## Pure JSX synchronization data source case ```tsx import React from 'react' import { TreeSelect, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## Pure JSX asynchronous linkage data source case ```tsx import React from 'react' import { TreeSelect, Select, FormItem, FormButtonGroup, Submit, } from '@formily/antd' import { createForm, onFieldReact, FormPathPattern, Field as FieldType, } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { action } from '@formily/reactive' const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: FieldType) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', children: [ { title: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { title: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { title: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'BBB', value: 'ccc', children: [ { title: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { title: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { title: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', children: [ { title: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { title: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { title: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'DDD', value: 'ddd', children: [ { title: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { title: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { title: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } }, 1500) }) }) }, }) export default () => ( Submit ) ``` ## API Reference https://ant.design/components/tree-select-cn/ ================================================ FILE: packages/antd/docs/components/TreeSelect.zh-CN.md ================================================ # TreeSelect > 树选择器 ## Markup Schema 同步数据源案例 ```tsx import React from 'react' import { TreeSelect, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TreeSelect, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## Markup Schema 异步联动数据源案例 ```tsx import React from 'react' import { TreeSelect, Select, FormItem, FormButtonGroup, Submit, } from '@formily/antd' import { createForm, onFieldReact, FormPathPattern, Field } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, TreeSelect, FormItem, }, }) const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: Field) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', children: [ { title: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { title: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { title: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'BBB', value: 'ccc', children: [ { title: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { title: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { title: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', children: [ { title: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { title: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { title: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'DDD', value: 'ddd', children: [ { title: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { title: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { title: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } }, 1500) }) }) }, }) export default () => ( 提交 ) ``` ## JSON Schema 同步数据源案例 ```tsx import React from 'react' import { TreeSelect, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TreeSelect, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { select: { type: 'string', title: '选择框', 'x-decorator': 'FormItem', 'x-component': 'TreeSelect', enum: [ { label: '选项1', value: 1, children: [ { title: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { title: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { title: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: '选项2', value: 2, children: [ { title: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { title: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { title: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ], 'x-component-props': { style: { width: 200, }, }, }, }, } export default () => ( 提交 ) ``` ## JSON Schema 异步联动数据源案例 ```tsx import React from 'react' import { TreeSelect, Select, FormItem, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, TreeSelect, FormItem, }, }) const loadData = async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', children: [ { title: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { title: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { title: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'BBB', value: 'ccc', children: [ { title: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { title: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { title: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', children: [ { title: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { title: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { title: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'DDD', value: 'ddd', children: [ { title: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { title: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { title: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } }, 1500) }) } const useAsyncDataSource = (service) => (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) } const form = createForm() const schema = { type: 'object', properties: { linkage: { type: 'string', title: '联动选择框', enum: [ { label: '发请求1', value: 1 }, { label: '发请求2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 200, }, }, }, select: { type: 'string', title: '异步选择框', 'x-decorator': 'FormItem', 'x-component': 'TreeSelect', 'x-component-props': { style: { width: 200, }, }, 'x-reactions': ['{{useAsyncDataSource(loadData)}}'], }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 同步数据源案例 ```tsx import React from 'react' import { TreeSelect, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## 纯 JSX 异步联动数据源案例 ```tsx import React from 'react' import { TreeSelect, Select, FormItem, FormButtonGroup, Submit, } from '@formily/antd' import { createForm, onFieldReact, FormPathPattern, Field as FieldType, } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { action } from '@formily/reactive' const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: FieldType) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', children: [ { title: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { title: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { title: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'BBB', value: 'ccc', children: [ { title: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { title: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { title: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', children: [ { title: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { title: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { title: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'DDD', value: 'ddd', children: [ { title: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { title: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { title: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } }, 1500) }) }) }, }) export default () => ( 提交 ) ``` ## API 参考 https://ant.design/components/tree-select-cn/ ================================================ FILE: packages/antd/docs/components/Upload.md ================================================ # Upload > Upload components > > Note: Using the upload component, it is recommended that users perform secondary packaging. Users do not need to care about the data communication between the upload component and Formily, only the style and basic upload configuration are required. ## Markup Schema example ```tsx import React from 'react' import { Upload, FormItem, FormLayout, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' import { UploadOutlined, InboxOutlined } from '@ant-design/icons' const NormalUpload = (props) => { return ( ) } const CardUpload = (props) => { return ( ) } const DraggerUpload = (props) => { return (

Click or drag file to this area to upload

Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files

) } const SchemaField = createSchemaField({ components: { NormalUpload, CardUpload, DraggerUpload, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Upload, FormItem, FormLayout, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' import { UploadOutlined, InboxOutlined } from '@ant-design/icons' const NormalUpload = (props) => { return ( ) } const CardUpload = (props) => { return ( ) } const DraggerUpload = (props) => { return (

Click or drag file to this area to upload

Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files

) } const SchemaField = createSchemaField({ components: { NormalUpload, CardUpload, DraggerUpload, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { upload: { type: 'array', title: 'Upload', required: true, 'x-decorator': 'FormItem', 'x-component': 'NormalUpload', }, upload2: { type: 'array', title: 'Card upload', required: true, 'x-decorator': 'FormItem', 'x-component': 'CardUpload', }, upload3: { type: 'array', title: 'Drag and drop upload', required: true, 'x-decorator': 'FormItem', 'x-component': 'DraggerUpload', }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Upload, FormItem, FormLayout, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { Button } from 'antd' import { UploadOutlined, InboxOutlined } from '@ant-design/icons' const NormalUpload = (props) => { return ( ) } const CardUpload = (props) => { return ( ) } const DraggerUpload = (props) => { return (

Click or drag file to this area to upload

Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files

) } const form = createForm() export default () => ( Submit ) ``` ## API Reference https://ant.design/components/upload-cn/ ================================================ FILE: packages/antd/docs/components/Upload.zh-CN.md ================================================ # Upload > 上传组件 > > 注意:使用上传组件,推荐用户进行二次封装,用户无需关心上传组件与 Formily 的数据通信,只需要处理样式与基本上传配置即可。 ## Markup Schema 案例 ```tsx import React from 'react' import { Upload, FormItem, FormLayout, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' import { UploadOutlined, InboxOutlined } from '@ant-design/icons' const NormalUpload = (props) => { return ( ) } const CardUpload = (props) => { return ( ) } const DraggerUpload = (props) => { return (

Click or drag file to this area to upload

Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files

) } const SchemaField = createSchemaField({ components: { NormalUpload, CardUpload, DraggerUpload, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Upload, FormItem, FormLayout, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' import { UploadOutlined, InboxOutlined } from '@ant-design/icons' const NormalUpload = (props) => { return ( ) } const CardUpload = (props) => { return ( ) } const DraggerUpload = (props) => { return (

Click or drag file to this area to upload

Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files

) } const SchemaField = createSchemaField({ components: { NormalUpload, CardUpload, DraggerUpload, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { upload: { type: 'array', title: '上传', required: true, 'x-decorator': 'FormItem', 'x-component': 'NormalUpload', }, upload2: { type: 'array', title: '卡片上传', required: true, 'x-decorator': 'FormItem', 'x-component': 'CardUpload', }, upload3: { type: 'array', title: '拖拽上传', required: true, 'x-decorator': 'FormItem', 'x-component': 'DraggerUpload', }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Upload, FormItem, FormLayout, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { Button } from 'antd' import { UploadOutlined, InboxOutlined } from '@ant-design/icons' const NormalUpload = (props) => { return ( ) } const CardUpload = (props) => { return ( ) } const DraggerUpload = (props) => { return (

Click or drag file to this area to upload

Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files

) } const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://ant.design/components/upload-cn/ ================================================ FILE: packages/antd/docs/components/index.md ================================================ # Ant Design ## Introduction @formily/antd is a professional component library for form scenarios based on Ant Design encapsulation. It has the following characteristics: - Only Formily 2.x is supported - Most components are not backward compatible - Unfortunately, many components of 1.x have inherent flaws in the API design. This is also because the form scheme has been explored, so there will be version breaks. - Richer component system - Layout components - FormLayout - FormItem - FormGrid - FormButtonGroup - Space - Submit - Reset - Input controls - Input - Password - Select - TreeSelect - DatePicker - TimePicker - NumberPicker - Transfer - Cascader - Radio - Checkbox - Upload - Switch - Scene components - ArrayCards - ArrayItems - ArrayTable - ArrayTabs - FormCollapse - FormStep - FormTab - FormDialog - FormDrawer - Editable - Reading state component - PreviewText - Theme customization ability - Completely abandon the 1.x styled-components solution, follow the style system of the component library, it is more convenient to customize the theme - Support secondary packaging - All components can be repackaged, and the 1.x component system cannot be repackaged, so providing this capability makes it more convenient for users to do business customization - Support reading mode - Although 1.x also supports reading mode, 2.x provides a separate PreviewText component, users can make reading mode encapsulation based on it, which is more flexible - Type is more friendly - Each component has an extremely complete type definition, and users can feel an unprecedented intelligent reminder experience during the actual development process - More complete layout control capabilities - 1.x's layout capabilities have basically converged to FormMegaLayout. This time, we directly removed Mega. Mega is a standard component and is completely internalized into FormLayout and FormItem components. At the same time, MegaLayout's grid layout capabilities are placed in FormGrid components. In, it also provides smarter layout capabilities. - More elegant and easy-to-use APIs, such as: - FormStep in the past has many problems. First, the type is not friendly. Second, the API is too hidden. To control the forward and backwards, you need to understand a bunch of private events. In the new version of FormStep, users only need to pay attention to the FormStep Reactive Model. You can create a Reactive Model through createFormStep and pass it to the FormStep component to quickly communicate. Similarly, FormTab/FormCollapse is the same communication mode. - Pop-up forms, drawer forms, presumably in the past, users had to write a lot of code on these two scenarios almost every time. This time, an extremely simple API is directly provided for users to use, which maximizes development efficiency. ## Installation ```bash $ npm install --save antd moment $ npm install --save @formily/core @formily/react @formily/antd ``` ## Q/A Q: I want to package a set of component libraries by myself, what should I do? Answer: If it is an open source component library, you can directly participate in the project co-construction and provide PR. If it is a private component library in the enterprise, you can refer to the source code. The source code does not have too much complicated logic. Question: Why do components such as ArrayCards/ArrayTable/FormStep only support Schema mode and not pure JSX mode? Answer: This is the core advantage of Schema mode. With the help of protocols, we can do scene-based abstraction. On the contrary, pure JSX mode is limited by the unparseability of JSX. It is difficult for us to achieve UI-level scene-based abstraction. It's just an abstract hook. ================================================ FILE: packages/antd/docs/components/index.zh-CN.md ================================================ # Ant Design ## 介绍 @formily/antd 是基于 Ant Design 封装的针对表单场景专业级(Professional)组件库,它主要有以下几个特点: - 仅支持 Formily2.x - 大部分组件无法向后兼容 - 很遗憾,1.x 的很多组件在 API 设计上存在本质上的缺陷,这也是因为表单方案一直在探索之中,所以才会出现版本断裂。 - 更丰富的组件体系 - 布局组件 - FormLayout - FormItem - FormGrid - FormButtonGroup - Space - Submit - Reset - 输入控件 - Input - Password - Select - TreeSelect - DatePicker - TimePicker - NumberPicker - Transfer - Cascader - Radio - Checkbox - Upload - Switch - 场景组件 - ArrayCards - ArrayItems - ArrayTable - ArrayTabs - FormCollapse - FormStep - FormTab - FormDialog - FormDrawer - Editable - 阅读态组件 - PreviewText - 主题定制能力 - 完全放弃了 1.x styled-components 方案,follow 组件库的样式体系,更方便定制主题 - 支持二次封装 - 所有组件都能二次封装,1.x 的组件体系是不能二次封装的,所以提供了这个能力则更方便用户做业务定制 - 支持阅读态 - 虽然 1.x 同样支持阅读态,但是 2.x 单独提供了 PreviewText 组件,用户可以基于它自己做阅读态封装,灵活性更强 - 类型更加友好 - 每个组件都有着极其完整的类型定义,用户在实际开发过程中,可以感受到前所未有的智能提示体验 - 更完备的布局控制能力 - 1.x 的布局能力基本上都收敛到了 FormMegaLayout 上,这次,我们直接去掉 Mega,Mega 就是标准组件,完全内化到 FormLayout 和 FormItem 组件中,同时将 MegaLayout 的网格布局能力放到了 FormGrid 组件中,也提供了更智能的布局能力。 - 更优雅易用的 API,比如: - 过去的 FormStep,有很多问题,第一,类型不友好,第二,API 隐藏太深,想要控制前进后退需要理解一堆的私有事件。新版 FormStep,用户只需要关注 FormStep Reactive Model 即可,通过 createFormStep 就可以创建出 Reactive Model,传给 FormStep 组件即可快速通讯。同理,FormTab/FormCollapse 也是一样的通讯模式。 - 弹窗表单,抽屉表单,想必过去,用户几乎每次都得在这两个场景上写大量的代码,这次直接提供了极其简易的 API 让用户使用,最大化提升开发效率。 ## 安装 ```bash $ npm install --save antd moment $ npm install --save @formily/core @formily/react @formily/antd ``` ## Q/A 问:我想自己封装一套组件库,该怎么做? 答:如果是开源组件库,可以直接参与项目共建,提供 PR,如果是企业内私有组件库,参考源码即可,源码并没有太多复杂逻辑。 问:为什么 ArrayCards/ArrayTable/FormStep 这类组件只支持 Schema 模式,不支持纯 JSX 模式? 答:这就是 Schema 模式的核心优势,借助协议,我们可以做场景化抽象,相反,纯 JSX 模式,受限于 JSX 的不可解析性,我们很难做到 UI 级别的场景化抽象,更多的只是抽象 Hook。 ================================================ FILE: packages/antd/docs/index.md ================================================ --- title: Formily-Alibaba unified front-end form solution order: 10 hero: title: Formily Antd desc: Formily Component System Based on Ant Design Encapsulation actions: - text: Home Site link: //formilyjs.org - text: Document link: /components features: - icon: https://img.alicdn.com/imgextra/i2/O1CN016i72sH1c5wh1kyy9U_!!6000000003550-55-tps-800-800.svg title: Easier To Use desc: Out of the box, rich cases - icon: https://img.alicdn.com/imgextra/i1/O1CN01bHdrZJ1rEOESvXEi5_!!6000000005599-55-tps-800-800.svg title: More Efficient desc: Stupid writing, super high performance - icon: https://img.alicdn.com/imgextra/i3/O1CN01xlETZk1G0WSQT6Xii_!!6000000000560-55-tps-800-800.svg title: More Professional desc: complete, flexible, elegant footer: Open-source MIT Licensed | Copyright © 2019-present
Powered by self --- ## Installation ```bash $ npm install --save antd moment $ npm install --save @formily/core @formily/react @formily/antd ``` ## Quick start ```tsx /** * defaultShowCode: true */ import React from 'react' import { NumberPicker, FormItem, Space } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, FormConsumer, Field } from '@formily/react' const form = createForm() export default () => ( × {(form) => ( ={` ${form.values.price * form.values.count}元`} )} ) ``` ================================================ FILE: packages/antd/docs/index.zh-CN.md ================================================ --- title: Formily - 阿里巴巴统一前端表单解决方案 order: 10 hero: title: Formily Antd desc: 基于Ant Design封装的优雅且易用的Formily2.x组件体系 actions: - text: 主站文档 link: //formilyjs.org - text: 组件文档 link: /zh-CN/components features: - icon: https://img.alicdn.com/imgextra/i2/O1CN016i72sH1c5wh1kyy9U_!!6000000003550-55-tps-800-800.svg title: 更易用 desc: 开箱即用,案例丰富 - icon: https://img.alicdn.com/imgextra/i1/O1CN01bHdrZJ1rEOESvXEi5_!!6000000005599-55-tps-800-800.svg title: 更高效 desc: 傻瓜写法,超高性能 - icon: https://img.alicdn.com/imgextra/i3/O1CN01xlETZk1G0WSQT6Xii_!!6000000000560-55-tps-800-800.svg title: 更专业 desc: 完备,灵活,优雅 footer: Open-source MIT Licensed | Copyright © 2019-present
Powered by self --- ## 安装 ```bash $ npm install --save antd moment $ npm install --save @formily/core @formily/react @formily/antd ``` ## 快速开始 ```tsx /** * defaultShowCode: true */ import React from 'react' import { NumberPicker, FormItem, Space } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, FormConsumer, Field } from '@formily/react' const form = createForm() export default () => ( × {(form) => ( ={` ${form.values.price * form.values.count} 元`} )} ) ``` ================================================ FILE: packages/antd/package.json ================================================ { "name": "@formily/antd", "version": "2.3.7", "license": "MIT", "main": "lib", "module": "esm", "umd:main": "dist/formily.antd.umd.production.js", "unpkg": "dist/formily.antd.umd.production.js", "jsdelivr": "dist/formily.antd.umd.production.js", "jsnext:main": "esm", "sideEffects": [ "dist/*", "esm/*.js", "lib/*.js", "src/*.ts", "*.less", "**/*/style.js" ], "repository": { "type": "git", "url": "git+https://github.com/alibaba/formily.git" }, "types": "esm/index.d.ts", "bugs": { "url": "https://github.com/alibaba/formily/issues" }, "homepage": "https://github.com/alibaba/formily#readme", "engines": { "npm": ">=3.0.0" }, "scripts": { "start": "dumi dev", "build": "rimraf -rf lib esm dist && npm run create:style && npm run build:cjs && npm run build:esm && npm run build:umd && npm run build:style", "create:style": "ts-node create-style", "build:style": "ts-node build-style", "build:cjs": "tsc --project tsconfig.build.json", "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm", "build:umd": "rollup --config", "build:docs": "dumi build" }, "devDependencies": { "@umijs/plugin-sass": "^1.1.1", "dumi": "^1.1.0-rc.8" }, "peerDependencies": { "@ant-design/icons": "4.x", "@types/react": ">=16.8.0", "@types/react-dom": ">=16.8.0", "antd": "<=4.22.8", "react": ">=16.8.0", "react-dom": ">=16.8.0", "react-is": ">=16.8.0" }, "peerDependenciesMeta": { "@types/react": { "optional": true }, "@types/react-dom": { "optional": true } }, "dependencies": { "@dnd-kit/core": "^6.0.0", "@dnd-kit/sortable": "^7.0.0", "@formily/core": "2.3.7", "@formily/grid": "2.3.7", "@formily/json-schema": "2.3.7", "@formily/react": "2.3.7", "@formily/reactive": "2.3.7", "@formily/reactive-react": "2.3.7", "@formily/shared": "2.3.7", "classnames": "^2.2.6", "react-sticky-box": "^0.9.3" }, "publishConfig": { "access": "public" }, "gitHead": "ac79c196ae9324889aca5e0501146f9b37b04283" } ================================================ FILE: packages/antd/rollup.config.js ================================================ import baseConfig, { removeImportStyleFromInputFilePlugin, } from '../../scripts/rollup.base.js' export default baseConfig( 'formily.antd', 'Formily.Antd', removeImportStyleFromInputFilePlugin() ) ================================================ FILE: packages/antd/src/__builtins__/hooks/index.ts ================================================ export * from './useClickAway' export * from './usePrefixCls' ================================================ FILE: packages/antd/src/__builtins__/hooks/useClickAway.ts ================================================ import { useRef, useEffect, MutableRefObject } from 'react' const defaultEvent = 'click' type EventType = MouseEvent | TouchEvent type BasicTarget = | (() => T | null) | T | null | MutableRefObject type TargetElement = HTMLElement | Element | Document | Window function getTargetElement( target?: BasicTarget, defaultElement?: TargetElement ): TargetElement | undefined | null { if (!target) { return defaultElement } let targetElement: TargetElement | undefined | null if (typeof target === 'function') { targetElement = target() } else if ('current' in target) { targetElement = target.current } else { targetElement = target } return targetElement } export const useClickAway = ( onClickAway: (event: EventType) => void, target: BasicTarget | BasicTarget[], eventName: string = defaultEvent ) => { const onClickAwayRef = useRef(onClickAway) onClickAwayRef.current = onClickAway useEffect(() => { const handler = (event: any) => { const targets = Array.isArray(target) ? target : [target] if ( targets.some((targetItem) => { const targetElement = getTargetElement(targetItem) as HTMLElement return !targetElement || targetElement?.contains(event.target) }) ) { return } onClickAwayRef.current(event) } document.addEventListener(eventName, handler) return () => { document.removeEventListener(eventName, handler) } }, [target, eventName]) } ================================================ FILE: packages/antd/src/__builtins__/hooks/usePrefixCls.ts ================================================ import { useContext } from 'react' import { ConfigProvider } from 'antd' export const usePrefixCls = ( tag?: string, props?: { prefixCls?: string } ) => { if ('ConfigContext' in ConfigProvider) { const { getPrefixCls } = useContext(ConfigProvider.ConfigContext) return getPrefixCls(tag, props?.prefixCls) } else { const prefix = props?.prefixCls ?? 'ant-' return `${prefix}${tag ?? ''}` } } ================================================ FILE: packages/antd/src/__builtins__/index.ts ================================================ export * from './moment' export * from './hooks' export * from './portal' export * from './loading' export * from './pickDataProps' export * from './sort' ================================================ FILE: packages/antd/src/__builtins__/loading.ts ================================================ import { message } from 'antd' export const loading = async ( title: React.ReactNode = 'Loading...', processor: () => Promise ) => { let hide = null let loading = setTimeout(() => { hide = message.loading(title) }, 100) try { return await processor() } finally { hide?.() clearTimeout(loading) } } ================================================ FILE: packages/antd/src/__builtins__/moment.ts ================================================ import { isArr, isFn, isEmpty } from '@formily/shared' import moment from 'moment' export const momentable = (value: any, format?: string) => { return Array.isArray(value) ? value.map((val) => moment(val, format)) : value ? moment(value, format) : value } export const formatMomentValue = ( value: any, format: any, placeholder?: string ): string | string[] => { const formatDate = (date: any, format: any, i = 0) => { if (!date) return placeholder const TIME_REG = /^(?:[01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/ let _format = format if (isArr(format)) { _format = format[i] } if (isFn(_format)) { return _format(date) } if (isEmpty(_format)) { return date } // moment '19:55:22' 下需要传入第二个参数 if (TIME_REG.test(date)) { return moment(date, _format).format(_format) } return moment(date).format(_format) } if (isArr(value)) { return value.map((val, index) => { return formatDate(val, format, index) }) } else { return value ? formatDate(value, format) : value || placeholder } } ================================================ FILE: packages/antd/src/__builtins__/pickDataProps.ts ================================================ export const pickDataProps = (props: any = {}) => { const results = {} for (let key in props) { if (key.indexOf('data-') > -1) { results[key] = props[key] } } return results } ================================================ FILE: packages/antd/src/__builtins__/portal.tsx ================================================ import React, { Fragment } from 'react' import { createPortal } from 'react-dom' import { observable } from '@formily/reactive' import { Observer } from '@formily/react' import { render as reactRender, unmount as reactUnmount } from './render' export interface IPortalProps { id?: string | symbol } const PortalMap = observable(new Map()) export const createPortalProvider = (id: string | symbol) => { const Portal = (props: React.PropsWithChildren) => { const portalId = props.id ?? id if (portalId && !PortalMap.has(portalId)) { PortalMap.set(portalId, null) } return ( {props.children} {() => { if (!portalId) return null const portal = PortalMap.get(portalId) if (portal) return createPortal(portal, document.body) return null }} ) } return Portal } export function createPortalRoot( host: HTMLElement, id: string ) { function render(renderer?: () => T) { if (PortalMap.has(id)) { PortalMap.set(id, renderer?.()) } else if (host) { reactRender({renderer?.()}, host) } } function unmount() { if (PortalMap.has(id)) { PortalMap.set(id, null) } if (host) { const unmountResult = reactUnmount(host) if (unmountResult && host.parentNode) { host.parentNode?.removeChild(host) } } } return { render, unmount, } } ================================================ FILE: packages/antd/src/__builtins__/render.ts ================================================ import { ReactElement } from 'react' import * as ReactDOM from 'react-dom' import type { Root } from 'react-dom/client' // 移植自rc-util: https://github.com/react-component/util/blob/master/src/React/render.ts type CreateRoot = (container: ContainerType) => Root // Let compiler not to search module usage const fullClone = { ...ReactDOM, } as typeof ReactDOM & { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?: { usingClientEntryPoint?: boolean } createRoot?: CreateRoot } const { version, render: reactRender, unmountComponentAtNode } = fullClone let createRoot: CreateRoot try { const mainVersion = Number((version || '').split('.')[0]) if (mainVersion >= 18 && fullClone.createRoot) { // eslint-disable-next-line @typescript-eslint/no-var-requires createRoot = fullClone.createRoot } } catch (e) { // Do nothing; } function toggleWarning(skip: boolean) { const { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } = fullClone if ( __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED && typeof __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED === 'object' ) { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = skip } } const MARK = '__antd_mobile_root__' // ========================== Render ========================== type ContainerType = (Element | DocumentFragment) & { [MARK]?: Root } function legacyRender(node: ReactElement, container: ContainerType) { reactRender(node, container) } function concurrentRender(node: ReactElement, container: ContainerType) { toggleWarning(true) const root = container[MARK] || createRoot(container) toggleWarning(false) root.render(node) container[MARK] = root } export function render(node: ReactElement, container: ContainerType) { if (createRoot as unknown) { concurrentRender(node, container) return } legacyRender(node, container) } // ========================== Unmount ========================= function legacyUnmount(container: ContainerType) { return unmountComponentAtNode(container) } async function concurrentUnmount(container: ContainerType) { // Delay to unmount to avoid React 18 sync warning return Promise.resolve().then(() => { container[MARK]?.unmount() delete container[MARK] }) } export function unmount(container: ContainerType) { if (createRoot as unknown) { return concurrentUnmount(container) } return legacyUnmount(container) } ================================================ FILE: packages/antd/src/__builtins__/sort.tsx ================================================ import { DndContext, DragEndEvent, DragStartEvent } from '@dnd-kit/core' import { SortableContext, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable' import { ReactFC } from '@formily/reactive-react' import React, { createContext, useContext, useMemo } from 'react' export interface ISortableContainerProps { list: any[] start?: number accessibility?: { container?: Element } onSortStart?: (event: DragStartEvent) => void onSortEnd?: (event: { oldIndex: number; newIndex: number }) => void } export function SortableContainer>( Component: ReactFC ): ReactFC { return ({ list, start = 0, accessibility, onSortStart, onSortEnd, ...props }) => { const _onSortEnd = (event: DragEndEvent) => { const { active, over } = event const oldIndex = (active.id as number) - 1 const newIndex = (over?.id as number) - 1 onSortEnd?.({ oldIndex, newIndex, }) } return ( index + start + 1)} strategy={verticalListSortingStrategy} > {props.children} ) } } export const useSortableItem = () => { return useContext(SortableItemContext) } export const SortableItemContext = createContext< Partial> >({}) export interface ISortableElementProps { index?: number lockAxis?: 'x' | 'y' } export function SortableElement>( Component: ReactFC ): ReactFC { return ({ index = 0, lockAxis, ...props }) => { const sortable = useSortable({ id: index + 1, }) const { setNodeRef, transform, transition, isDragging } = sortable if (transform) { switch (lockAxis) { case 'x': transform.y = 0 break case 'y': transform.x = 0 break default: break } } const style = useMemo(() => { const itemStyle: React.CSSProperties = { position: 'relative', touchAction: 'none', zIndex: 1, transform: `translate3d(${transform?.x || 0}px, ${ transform?.y || 0 }px, 0)`, transition: `${transform ? 'all 200ms ease' : ''}`, } const dragStyle: React.CSSProperties = { transition, opacity: '0.8', transform: `translate3d(${transform?.x || 0}px, ${ transform?.y || 0 }px, 0)`, } const computedStyle = isDragging ? { ...itemStyle, ...dragStyle, ...props.style, } : { ...itemStyle, ...props.style, } return computedStyle }, [isDragging, transform, transition, props.style]) return ( {Component({ ...props, style, ref: setNodeRef, } as unknown as T)} ) } } export function SortableHandle>( Component: ReactFC ): ReactFC { return (props: T) => { const { attributes, listeners } = useSortableItem() return } } ================================================ FILE: packages/antd/src/array-base/index.tsx ================================================ import { CopyOutlined, DeleteOutlined, DownOutlined, MenuOutlined, PlusOutlined, UpOutlined, } from '@ant-design/icons' import { ArrayField } from '@formily/core' import { JSXComponent, Schema, useField, useFieldSchema } from '@formily/react' import { clone, isUndef, isValid } from '@formily/shared' import { Button } from 'antd' import { ButtonProps } from 'antd/lib/button' import cls from 'classnames' import React, { createContext, useContext } from 'react' import { SortableHandle, usePrefixCls } from '../__builtins__' export interface IArrayBaseAdditionProps extends ButtonProps { title?: string method?: 'push' | 'unshift' defaultValue?: any } export interface IArrayBaseOperationProps extends ButtonProps { title?: string index?: number ref?: React.Ref } export interface IArrayBaseContext { props: IArrayBaseProps field: ArrayField schema: Schema } export interface IArrayBaseItemProps { index: number record: ((index: number) => Record) | Record } export type ArrayBaseMixins = { Addition?: React.FC> Copy?: React.FC< React.PropsWithChildren > Remove?: React.FC< React.PropsWithChildren > MoveUp?: React.FC< React.PropsWithChildren > MoveDown?: React.FC< React.PropsWithChildren > SortHandle?: React.FC< React.PropsWithChildren > Index?: React.FC useArray?: () => IArrayBaseContext useIndex?: (index?: number) => number useRecord?: (record?: number) => any } export interface IArrayBaseProps { disabled?: boolean onAdd?: (index: number) => void onCopy?: (index: number) => void onRemove?: (index: number) => void onMoveDown?: (index: number) => void onMoveUp?: (index: number) => void } type ComposedArrayBase = React.FC> & ArrayBaseMixins & { Item?: React.FC> mixin?: (target: T) => T & ArrayBaseMixins } const ArrayBaseContext = createContext(null) const ItemContext = createContext(null) const takeRecord = (val: any, index?: number) => typeof val === 'function' ? val(index) : val const useArray = () => { return useContext(ArrayBaseContext) } const useIndex = (index?: number) => { const ctx = useContext(ItemContext) return ctx ? ctx.index : index } const useRecord = (record?: number) => { const ctx = useContext(ItemContext) return takeRecord(ctx ? ctx.record : record, ctx?.index) } const getSchemaDefaultValue = (schema: Schema) => { if (schema?.type === 'array') return [] if (schema?.type === 'object') return {} if (schema?.type === 'void') { for (let key in schema.properties) { const value = getSchemaDefaultValue(schema.properties[key]) if (isValid(value)) return value } } } const getDefaultValue = (defaultValue: any, schema: Schema) => { if (isValid(defaultValue)) return clone(defaultValue) if (Array.isArray(schema?.items)) return getSchemaDefaultValue(schema?.items[0]) return getSchemaDefaultValue(schema?.items) } export const ArrayBase: ComposedArrayBase = (props) => { const field = useField() const schema = useFieldSchema() return ( {props.children} ) } ArrayBase.Item = ({ children, ...props }) => { return {children} } const SortHandle = SortableHandle((props: any) => { const prefixCls = usePrefixCls('formily-array-base') return ( ) }) as any ArrayBase.SortHandle = (props) => { const array = useArray() if (!array) return null if (array.field?.pattern !== 'editable') return null return } ArrayBase.Index = (props) => { const index = useIndex() const prefixCls = usePrefixCls('formily-array-base') return ( #{index + 1}. ) } ArrayBase.Addition = (props) => { const self = useField() const array = useArray() const prefixCls = usePrefixCls('formily-array-base') if (!array) return null if ( array.field?.pattern !== 'editable' && array.field?.pattern !== 'disabled' ) return null return ( ) } ArrayBase.Copy = React.forwardRef((props, ref) => { const self = useField() const array = useArray() const index = useIndex(props.index) const prefixCls = usePrefixCls('formily-array-base') if (!array) return null if (array.field?.pattern !== 'editable') return null return ( ) }) ArrayBase.Remove = React.forwardRef((props, ref) => { const index = useIndex(props.index) const self = useField() const array = useArray() const prefixCls = usePrefixCls('formily-array-base') if (!array) return null if (array.field?.pattern !== 'editable') return null return ( ) }) ArrayBase.MoveDown = React.forwardRef((props, ref) => { const index = useIndex(props.index) const self = useField() const array = useArray() const prefixCls = usePrefixCls('formily-array-base') if (!array) return null if (array.field?.pattern !== 'editable') return null return ( ) }) ArrayBase.MoveUp = React.forwardRef((props, ref) => { const index = useIndex(props.index) const self = useField() const array = useArray() const prefixCls = usePrefixCls('formily-array-base') if (!array) return null if (array.field?.pattern !== 'editable') return null return ( ) }) ArrayBase.useArray = useArray ArrayBase.useIndex = useIndex ArrayBase.useRecord = useRecord ArrayBase.mixin = (target: any) => { target.Index = ArrayBase.Index target.SortHandle = ArrayBase.SortHandle target.Addition = ArrayBase.Addition target.Copy = ArrayBase.Copy target.Remove = ArrayBase.Remove target.MoveDown = ArrayBase.MoveDown target.MoveUp = ArrayBase.MoveUp target.useArray = ArrayBase.useArray target.useIndex = ArrayBase.useIndex target.useRecord = ArrayBase.useRecord return target } export default ArrayBase ================================================ FILE: packages/antd/src/array-base/style.less ================================================ @root-entry-name: 'default'; @import (reference) '~antd/es/style/themes/index.less'; @array-base-prefix-cls: ~'@{ant-prefix}-formily-array-base'; .@{array-base-prefix-cls}-remove, .@{array-base-prefix-cls}-copy { transition: all 0.25s ease-in-out; color: @text-color; font-size: 14px; margin-left: 6px; padding: 0; border: none; width: auto; height: auto; &:hover { color: @primary-5; } &-disabled { color: @disabled-color; cursor: not-allowed !important; &:hover { color: @disabled-color; } } } .@{array-base-prefix-cls}-sort-handle { cursor: move; color: #888 !important; // overrid iconfont.less .anticon[tabindex] cursor &.anticon[tabindex] { cursor: move; } } .@{array-base-prefix-cls}-addition { transition: all 0.25s ease-in-out; } .@{array-base-prefix-cls}-move-down { transition: all 0.25s ease-in-out; color: @text-color; font-size: 14px; margin-left: 6px; padding: 0; border: none; width: auto; height: auto; &:hover { color: @primary-5; } &-disabled { color: @disabled-color; cursor: not-allowed !important; &:hover { color: @disabled-color; } } } .@{array-base-prefix-cls}-move-up { transition: all 0.25s ease-in-out; color: @text-color; font-size: 14px; margin-left: 6px; padding: 0; border: none; width: auto; height: auto; &:hover { color: @primary-5; } &-disabled { color: @disabled-color; cursor: not-allowed !important; &:hover { color: @disabled-color; } } } ================================================ FILE: packages/antd/src/array-base/style.ts ================================================ import 'antd/lib/button/style/index' import './style.less' ================================================ FILE: packages/antd/src/array-cards/index.tsx ================================================ import React from 'react' import { Card, Empty } from 'antd' import { CardProps } from 'antd/lib/card' import { ArrayField } from '@formily/core' import { useField, observer, useFieldSchema, RecursionField, } from '@formily/react' import cls from 'classnames' import { ISchema } from '@formily/json-schema' import { usePrefixCls } from '../__builtins__' import { ArrayBase, ArrayBaseMixins, IArrayBaseProps } from '../array-base' type ComposedArrayCards = React.FC< React.PropsWithChildren > & ArrayBaseMixins const isAdditionComponent = (schema: ISchema) => { return schema['x-component']?.indexOf('Addition') > -1 } const isIndexComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Index') > -1 } const isRemoveComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Remove') > -1 } const isCopyComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Copy') > -1 } const isMoveUpComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('MoveUp') > -1 } const isMoveDownComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('MoveDown') > -1 } const isOperationComponent = (schema: ISchema) => { return ( isAdditionComponent(schema) || isRemoveComponent(schema) || isCopyComponent(schema) || isMoveDownComponent(schema) || isMoveUpComponent(schema) ) } export const ArrayCards: ComposedArrayCards = observer((props) => { const field = useField() const schema = useFieldSchema() const dataSource = Array.isArray(field.value) ? field.value : [] const prefixCls = usePrefixCls('formily-array-cards', props) const { onAdd, onCopy, onRemove, onMoveDown, onMoveUp } = props if (!schema) throw new Error('can not found schema object') const renderItems = () => { return dataSource?.map((item, index) => { const items = Array.isArray(schema.items) ? schema.items[index] || schema.items[0] : schema.items const title = ( { if (!isIndexComponent(schema)) return false return true }} onlyRenderProperties /> {props.title || field.title} ) const extra = ( { if (!isOperationComponent(schema)) return false return true }} onlyRenderProperties /> {props.extra} ) const content = ( { if (isIndexComponent(schema)) return false if (isOperationComponent(schema)) return false return true }} /> ) return ( field.value?.[index]} > {}} className={cls(`${prefixCls}-item`, props.className)} title={title} extra={extra} > {content} ) }) } const renderAddition = () => { return schema.reduceProperties((addition, schema, key) => { if (isAdditionComponent(schema)) { return } return addition }, null) } const renderEmpty = () => { if (dataSource?.length) return return ( {}} className={cls(`${prefixCls}-item`, props.className)} title={props.title || field.title} > ) } return ( {renderEmpty()} {renderItems()} {renderAddition()} ) }) ArrayCards.displayName = 'ArrayCards' ArrayBase.mixin(ArrayCards) export default ArrayCards ================================================ FILE: packages/antd/src/array-cards/style.less ================================================ @root-entry-name: 'default'; @import (reference) '~antd/es/style/themes/index.less'; @array-base-prefix-cls: ~'@{ant-prefix}-formily-array-base'; @array-cards-prefix-cls: ~'@{ant-prefix}-formily-array-cards'; .@{array-cards-prefix-cls}-item { margin-bottom: 10px !important; } .ant-card-extra { .@{array-base-prefix-cls}-copy { margin-left: 6px; } } ================================================ FILE: packages/antd/src/array-cards/style.ts ================================================ import 'antd/lib/card/style/index' import 'antd/lib/empty/style/index' import 'antd/lib/button/style/index' import './style.less' ================================================ FILE: packages/antd/src/array-collapse/index.tsx ================================================ import React, { Fragment, useState, useEffect } from 'react' import { Badge, Card, Collapse, CollapsePanelProps, CollapseProps, Empty, } from 'antd' import { ArrayField } from '@formily/core' import { RecursionField, useField, useFieldSchema, observer, ISchema, } from '@formily/react' import { toArr } from '@formily/shared' import cls from 'classnames' import ArrayBase, { ArrayBaseMixins, IArrayBaseProps } from '../array-base' import { usePrefixCls } from '../__builtins__' export interface IArrayCollapseProps extends CollapseProps { defaultOpenPanelCount?: number } type ComposedArrayCollapse = React.FC< React.PropsWithChildren > & ArrayBaseMixins & { CollapsePanel?: React.FC> } const isAdditionComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Addition') > -1 } const isIndexComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Index') > -1 } const isRemoveComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Remove') > -1 } const isMoveUpComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('MoveUp') > -1 } const isMoveDownComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('MoveDown') > -1 } const isOperationComponent = (schema: ISchema) => { return ( isAdditionComponent(schema) || isRemoveComponent(schema) || isMoveDownComponent(schema) || isMoveUpComponent(schema) ) } const range = (count: number) => Array.from({ length: count }).map((_, i) => i) const takeDefaultActiveKeys = ( dataSourceLength: number, defaultOpenPanelCount: number ) => { if (dataSourceLength < defaultOpenPanelCount) return range(dataSourceLength) return range(defaultOpenPanelCount) } const insertActiveKeys = (activeKeys: number[], index: number) => { if (activeKeys.length <= index) return activeKeys.concat(index) return activeKeys.reduce((buf, key) => { if (key < index) return buf.concat(key) if (key === index) return buf.concat([key, key + 1]) return buf.concat(key + 1) }, []) } export const ArrayCollapse: ComposedArrayCollapse = observer( ({ defaultOpenPanelCount = 5, ...props }) => { const field = useField() const dataSource = Array.isArray(field.value) ? field.value : [] const [activeKeys, setActiveKeys] = useState( takeDefaultActiveKeys(dataSource.length, defaultOpenPanelCount) ) const schema = useFieldSchema() const prefixCls = usePrefixCls('formily-array-collapse', props) useEffect(() => { if (!field.modified && dataSource.length) { setActiveKeys( takeDefaultActiveKeys(dataSource.length, defaultOpenPanelCount) ) } }, [dataSource.length, field]) if (!schema) throw new Error('can not found schema object') const { onAdd, onCopy, onRemove, onMoveDown, onMoveUp } = props const renderAddition = () => { return schema.reduceProperties((addition, schema, key) => { if (isAdditionComponent(schema)) { return } return addition }, null) } const renderEmpty = () => { if (dataSource.length) return return ( ) } const renderItems = () => { return ( setActiveKeys(toArr(keys).map(Number))} className={cls(`${prefixCls}-item`, props.className)} > {dataSource.map((item, index) => { const items = Array.isArray(schema.items) ? schema.items[index] || schema.items[0] : schema.items const panelProps = field .query(`${field.address}.${index}`) .get('componentProps') const props: CollapsePanelProps = items['x-component-props'] const header = () => { const header = panelProps?.header || props.header || field.title const path = field.address.concat(index) const errors = field.form.queryFeedbacks({ type: 'error', address: `${path}.**`, }) return ( field.value?.[index]} > { if (!isIndexComponent(schema)) return false return true }} onlyRenderProperties /> {errors.length ? ( {header} ) : ( header )} ) } const extra = ( { if (!isOperationComponent(schema)) return false return true }} onlyRenderProperties /> {panelProps?.extra} ) const content = ( { if (isIndexComponent(schema)) return false if (isOperationComponent(schema)) return false return true }} /> ) return ( {content} ) })} ) } return ( { onAdd?.(index) setActiveKeys(insertActiveKeys(activeKeys, index)) }} onCopy={onCopy} onRemove={onRemove} onMoveUp={onMoveUp} onMoveDown={onMoveDown} > {renderEmpty()} {renderItems()} {renderAddition()} ) } ) const CollapsePanel: React.FC> = ({ children, }) => { return {children} } CollapsePanel.displayName = 'CollapsePanel' ArrayCollapse.displayName = 'ArrayCollapse' ArrayCollapse.CollapsePanel = CollapsePanel ArrayBase.mixin(ArrayCollapse) export default ArrayCollapse ================================================ FILE: packages/antd/src/array-collapse/style.less ================================================ @root-entry-name: 'default'; @import (reference) '~antd/es/style/themes/index.less'; @array-collapse-prefix-cls: ~'@{ant-prefix}-formily-array-collapse'; .@{array-collapse-prefix-cls}-item { margin-bottom: 10px !important; } ================================================ FILE: packages/antd/src/array-collapse/style.ts ================================================ import 'antd/lib/collapse/style/index' import 'antd/lib/empty/style/index' import 'antd/lib/button/style/index' import 'antd/lib/badge/style/index' import './style.less' ================================================ FILE: packages/antd/src/array-items/index.tsx ================================================ import React, { useRef } from 'react' import { ArrayField } from '@formily/core' import { useField, observer, useFieldSchema, RecursionField, } from '@formily/react' import cls from 'classnames' import { ISchema } from '@formily/json-schema' import { usePrefixCls, SortableContainer, SortableElement, } from '../__builtins__' import { ArrayBase, ArrayBaseMixins, IArrayBaseProps } from '../array-base' type ComposedArrayItems = React.FC< React.PropsWithChildren< React.HTMLAttributes & IArrayBaseProps > > & ArrayBaseMixins & { Item?: React.FC< React.HTMLAttributes & { type?: 'card' | 'divide' } > } const SortableItem = SortableElement( (props: React.PropsWithChildren>) => { const prefixCls = usePrefixCls('formily-array-items') return (
{props.children}
) } ) const SortableList = SortableContainer( (props: React.PropsWithChildren>) => { const prefixCls = usePrefixCls('formily-array-items') return (
{props.children}
) } ) const isAdditionComponent = (schema: ISchema) => { return schema['x-component']?.indexOf('Addition') > -1 } const useAddition = () => { const schema = useFieldSchema() return schema.reduceProperties((addition, schema, key) => { if (isAdditionComponent(schema)) { return } return addition }, null) } export const ArrayItems: ComposedArrayItems = observer((props) => { const field = useField() const prefixCls = usePrefixCls('formily-array-items') const ref = useRef(null) const schema = useFieldSchema() const addition = useAddition() const dataSource = Array.isArray(field.value) ? field.value : [] const { onAdd, onCopy, onRemove, onMoveDown, onMoveUp } = props if (!schema) throw new Error('can not found schema object') return (
{}} className={cls(prefixCls, props.className)} > { field.move(oldIndex, newIndex) }} > {dataSource?.map((item, index) => { const items = Array.isArray(schema.items) ? schema.items[index] || schema.items[0] : schema.items return ( field.value?.[index]} >
) })}
{addition}
) }) ArrayItems.displayName = 'ArrayItems' ArrayItems.Item = (props) => { const prefixCls = usePrefixCls('formily-array-items') return (
{}} className={cls(`${prefixCls}-${props.type || 'card'}`, props.className)} > {props.children}
) } ArrayBase.mixin(ArrayItems) export default ArrayItems ================================================ FILE: packages/antd/src/array-items/style.less ================================================ @root-entry-name: 'default'; @import (reference) '~antd/es/style/themes/index.less'; @array-items-prefix-cls: ~'@{ant-prefix}-formily-array-items'; .@{array-items-prefix-cls}-item-inner { visibility: visible; } // fix https://github.com/alibaba/formily/issues/2891 .@{array-items-prefix-cls}-item { z-index: 100000; } .@{array-items-prefix-cls}-card { display: flex; border: 1px solid @border-color-split; margin-bottom: 10px; padding: 3px 6px; background: @card-background; justify-content: space-between; color: @text-color; .@{ant-prefix}-formily-item:not(.@{ant-prefix}-formily-item-feedback-layout-popover) { margin-bottom: 0 !important; .@{ant-prefix}-formily-item-help { position: absolute; font-size: 12px; top: 100%; background: @card-background; width: 100%; margin-top: 3px; padding: 3px; z-index: 1; border-radius: 3px; box-shadow: 0 0 10px @border-color-split; } } } .@{array-items-prefix-cls}-divide { display: flex; border-bottom: 1px solid @border-color-split; padding: 10px 0; justify-content: space-between; .@{ant-prefix}-formily-item:not(.@{ant-prefix}-formily-item-feedback-layout-popover) { margin-bottom: 0 !important; .@{ant-prefix}-formily-item-help { position: absolute; font-size: 12px; top: 100%; background: @card-background; width: 100%; margin-top: 3px; padding: 3px; z-index: 1; border-radius: 3px; box-shadow: 0 0 10px @border-color-split; } } } ================================================ FILE: packages/antd/src/array-items/style.ts ================================================ import 'antd/lib/button/style/index' import './style.less' ================================================ FILE: packages/antd/src/array-table/index.tsx ================================================ import React, { Fragment, useState, useRef, useEffect, createContext, useContext, useCallback, } from 'react' import { Table, Pagination, Space, Select, Badge } from 'antd' import { PaginationProps } from 'antd/lib/pagination' import { TableProps, ColumnProps } from 'antd/lib/table' import { SelectProps } from 'antd/lib/select' import cls from 'classnames' import { GeneralField, FieldDisplayTypes, ArrayField } from '@formily/core' import { useField, observer, useFieldSchema, RecursionField, ReactFC, } from '@formily/react' import { isArr, isBool, isFn } from '@formily/shared' import { Schema } from '@formily/json-schema' import { usePrefixCls, SortableContainer, SortableElement, } from '../__builtins__' import { ArrayBase, ArrayBaseMixins, IArrayBaseProps } from '../array-base' interface ObservableColumnSource { field: GeneralField columnProps: ColumnProps schema: Schema display: FieldDisplayTypes name: string } interface IArrayTablePaginationProps extends PaginationProps { dataSource?: any[] showPagination?: boolean children?: ( dataSource: any[], pagination: React.ReactNode, options: { startIndex: number } ) => React.ReactElement } interface IStatusSelectProps extends SelectProps { pageSize?: number } type ComposedArrayTable = React.FC< React.PropsWithChildren & IArrayBaseProps> > & ArrayBaseMixins & { Column?: React.FC>> } interface PaginationAction { totalPage?: number pageSize?: number showPagination?: boolean changePage?: (page: number) => void } const SortableRow = SortableElement((props: any) => ) const SortableBody = SortableContainer((props: any) => ) const isColumnComponent = (schema: Schema) => { return schema['x-component']?.indexOf('Column') > -1 } const isOperationsComponent = (schema: Schema) => { return schema['x-component']?.indexOf('Operations') > -1 } const isAdditionComponent = (schema: Schema) => { return schema['x-component']?.indexOf('Addition') > -1 } const useArrayTableSources = () => { const arrayField = useField() const schema = useFieldSchema() const parseSources = (schema: Schema): ObservableColumnSource[] => { if ( isColumnComponent(schema) || isOperationsComponent(schema) || isAdditionComponent(schema) ) { if (!schema['x-component-props']?.['dataIndex'] && !schema['name']) return [] const name = schema['x-component-props']?.['dataIndex'] || schema['name'] const field = arrayField.query(arrayField.address.concat(name)).take() const columnProps = field?.component?.[1] || schema['x-component-props'] || {} const display = field?.display || schema['x-display'] || 'visible' return [ { name, display, field, schema, columnProps, }, ] } else if (schema.properties) { return schema.reduceProperties((buf, schema) => { return buf.concat(parseSources(schema)) }, []) } } const parseArrayItems = (schema: Schema['items']) => { if (!schema) return [] const sources: ObservableColumnSource[] = [] const items = isArr(schema) ? schema : [schema] return items.reduce((columns, schema) => { const item = parseSources(schema) if (item) { return columns.concat(item) } return columns }, sources) } if (!schema) throw new Error('can not found schema object') return parseArrayItems(schema.items) } const useArrayTableColumns = ( dataSource: any[], field: ArrayField, sources: ObservableColumnSource[] ): TableProps['columns'] => { return sources.reduce((buf, { name, columnProps, schema, display }, key) => { if (display !== 'visible') return buf if (!isColumnComponent(schema)) return buf return buf.concat({ ...columnProps, key, dataIndex: name, render: (value: any, record: any) => { const index = dataSource?.indexOf(record) const children = ( field?.value?.[index]}> ) return children }, }) }, []) } const useAddition = () => { const schema = useFieldSchema() return schema.reduceProperties((addition, schema, key) => { if (isAdditionComponent(schema)) { return } return addition }, null) } const schedulerRequest = { request: null, } const StatusSelect: ReactFC = observer( (props) => { const field = useField() const prefixCls = usePrefixCls('formily-array-table') const errors = field.errors const parseIndex = (address: string) => { return Number( address .slice(address.indexOf(field.address.toString()) + 1) .match(/(\d+)/)?.[1] ) } const options = props.options?.map(({ label, value }) => { const val = Number(value) const hasError = errors.some(({ address }) => { const currentIndex = parseIndex(address) const startIndex = (val - 1) * props.pageSize const endIndex = val * props.pageSize return currentIndex >= startIndex && currentIndex <= endIndex }) return { label: hasError ? {label} : label, value, } }) const width = String(options?.length).length * 15 return ( ) } //Form management entrance const FormProvider = (props) => { useEffect(() => { //Mount form props.form?.onMount() return () => { //Uninstall the form props.form?.onUnmount() } }) return ( {props.children} ) } //Form response monitor const FormConsumer = observer((props) => { const form = useContext(FormContext) return
{props.children(form)}
}) /* * The above logic has been implemented in @formily/react or @formily/vue, and there is no need to rewrite it in actual use */ //Switch the built-in check internationalization copy to English setValidateLanguage('en') export default () => { const form = useMemo(() => createForm({ validateFirst: true })) const createPasswordEqualValidate = (equalName) => (field) => { if ( form.values.confirm_password && field.value && form.values[equalName] !== field.value ) { field.selfErrors = ['Password does not match Confirm Password.'] } else { field.selfErrors = [] } } return (
          
            {(form) => JSON.stringify(form.values, null, 2)}
          
        
) } ``` ================================================ FILE: packages/core/docs/index.zh-CN.md ================================================ --- title: Formily - 阿里巴巴统一前端表单解决方案 order: 10 hero: title: Core Library desc: 阿里巴巴统一前端表单解决方案 actions: - text: 主站文档 link: //formilyjs.org - text: 内核文档 link: /zh-CN/guide features: - icon: https://img.alicdn.com/imgextra/i1/O1CN01bHdrZJ1rEOESvXEi5_!!6000000005599-55-tps-800-800.svg title: 超高的性能 desc: 依赖追踪,高效更新,按需渲染 - icon: https://img.alicdn.com/imgextra/i3/O1CN0194OqFF1ui6mMT4g7O_!!6000000006070-55-tps-800-800.svg title: 极佳的复用性 desc: 副作用独立,逻辑可拔插 - icon: https://img.alicdn.com/imgextra/i2/O1CN01QnfYS71E44I1ZpxU9_!!6000000000297-55-tps-800-800.svg title: 优雅的联动写法 desc: 灵活,完备,优雅 - icon: https://img.alicdn.com/imgextra/i2/O1CN01YqmcpN1tDalwgyHBH_!!6000000005868-55-tps-800-800.svg title: 完备的领域模型 desc: 跨终端,跨框架,UI无关 - icon: https://img.alicdn.com/imgextra/i4/O1CN018vDmpl2186xdLu6KI_!!6000000006939-55-tps-800-800.svg title: 友好的调试体验 desc: 天然对接Formily DevTools - icon: https://img.alicdn.com/imgextra/i4/O1CN01u6jHgs1ZMwXpjAYnh_!!6000000003181-55-tps-800-800.svg title: 完美的智能提示 desc: 拥抱Typescript footer: Open-source MIT Licensed | Copyright © 2019-present
Powered by self --- ## 安装 ```bash $ npm install --save @formily/core ``` ## 快速开始 > 以下案例是一步步教您从零实现一个表单 > > @formily/core 给您带来了以下几个能力: > > 1. 响应式计算能力 > 2. 校验能力、校验国际化能力 > 3. 值管理能力 > 4. 联动管理能力 > 5. 开发工具调试能力,[下载 Formily Devtools](https://chrome.google.com/webstore/detail/formily-devtools/kkocalmbfnplecdmbadaapgapdioecfm?hl=zh-CN) ```tsx /** * defaultShowCode: true */ import React, { createContext, useMemo, useContext, useEffect } from 'react' import { createForm, setValidateLanguage } from '@formily/core' import { observer } from '@formily/reactive-react' //创建上下文,方便Field消费 const FormContext = createContext() //创建上下文,方便FormItem消费 const FieldContext = createContext() //状态桥接器组件 const Field = observer((props) => { const form = useContext(FormContext) //创建字段 const field = form.createField(props) useEffect(() => { //挂载字段 field.onMount() return () => { //卸载字段 field.onUnmount() } }) if (!field.visible || field.hidden) return null //渲染字段,将字段状态与UI组件关联 const component = React.createElement(field.component[0], { ...field.component[1], value: field.value, onChange: field.onInput, }) //渲染字段包装器 const decorator = React.createElement( field.decorator[0], field.decorator[1], component ) return ( {decorator} ) }) // FormItem UI组件 const FormItem = observer(({ children }) => { const field = useContext(FieldContext) return (
{field.title}:
{children}
{field.selfErrors.join(',')}
) }) // Input UI组件 const Input = (props) => { return ( ) } //表单管理入口 const FormProvider = (props) => { useEffect(() => { //挂载表单 props.form?.onMount() return () => { //卸载表单 props.form?.onUnmount() } }) return ( {props.children} ) } //表单响应式监控器 const FormConsumer = observer((props) => { const form = useContext(FormContext) return
{props.children(form)}
}) /* * 以上逻辑都已经在 @formily/react 或 @formily/vue 中实现,实际使用无需重复编写 */ //切换内置校验国际化文案为英文 setValidateLanguage('en') export default () => { const form = useMemo(() => createForm({ validateFirst: true })) const createPasswordEqualValidate = (equalName) => (field) => { if ( form.values.confirm_password && field.value && form.values[equalName] !== field.value ) { field.selfErrors = ['Password does not match Confirm Password.'] } else { field.selfErrors = [] } } return (
          
            {(form) => JSON.stringify(form.values, null, 2)}
          
        
) } ``` ================================================ FILE: packages/core/package.json ================================================ { "name": "@formily/core", "version": "2.3.7", "license": "MIT", "main": "lib", "module": "esm", "umd:main": "dist/formily.core.umd.production.js", "unpkg": "dist/formily.core.umd.production.js", "jsdelivr": "dist/formily.core.umd.production.js", "jsnext:main": "esm", "repository": { "type": "git", "url": "git+https://github.com/alibaba/formily.git" }, "types": "esm/index.d.ts", "bugs": { "url": "https://github.com/alibaba/formily/issues" }, "homepage": "https://github.com/alibaba/formily#readme", "engines": { "npm": ">=3.0.0" }, "scripts": { "start": "dumi dev", "build": "rimraf -rf lib esm dist && npm run build:cjs && npm run build:esm && npm run build:umd", "build:cjs": "tsc --project tsconfig.build.json", "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm", "build:umd": "rollup --config", "build:docs": "dumi build" }, "devDependencies": { "dumi": "^1.1.0-rc.8" }, "dependencies": { "@formily/reactive": "2.3.7", "@formily/shared": "2.3.7", "@formily/validator": "2.3.7" }, "publishConfig": { "access": "public" }, "gitHead": "ac79c196ae9324889aca5e0501146f9b37b04283" } ================================================ FILE: packages/core/rollup.config.js ================================================ import baseConfig from '../../scripts/rollup.base.js' export default baseConfig('formily.core', 'Formily.Core') ================================================ FILE: packages/core/src/__tests__/array.spec.ts ================================================ import { createForm } from '../' import { onFieldValueChange, onFormInitialValuesChange, onFormValuesChange, } from '../effects' import { DataField } from '../types' import { attach } from './shared' test('create array field', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) expect(array.value).toEqual([]) expect(array.push).toBeDefined() expect(array.pop).toBeDefined() expect(array.shift).toBeDefined() expect(array.unshift).toBeDefined() expect(array.move).toBeDefined() expect(array.moveDown).toBeDefined() expect(array.moveUp).toBeDefined() expect(array.insert).toBeDefined() expect(array.remove).toBeDefined() }) test('array field methods', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', value: [], }) ) array.push({ aa: 11 }, { bb: 22 }) expect(array.value).toEqual([{ aa: 11 }, { bb: 22 }]) array.pop() expect(array.value).toEqual([{ aa: 11 }]) array.unshift({ cc: 33 }) expect(array.value).toEqual([{ cc: 33 }, { aa: 11 }]) array.remove(1) expect(array.value).toEqual([{ cc: 33 }]) array.insert(1, { dd: 44 }, { ee: 55 }) expect(array.value).toEqual([{ cc: 33 }, { dd: 44 }, { ee: 55 }]) array.move(0, 2) expect(array.value).toEqual([{ dd: 44 }, { ee: 55 }, { cc: 33 }]) array.shift() expect(array.value).toEqual([{ ee: 55 }, { cc: 33 }]) array.moveDown(0) expect(array.value).toEqual([{ cc: 33 }, { ee: 55 }]) array.moveUp(1) expect(array.value).toEqual([{ ee: 55 }, { cc: 33 }]) array.move(1, 0) expect(array.value).toEqual([{ cc: 33 }, { ee: 55 }]) }) test('array field children state exchanges', () => { //注意:插入新节点,如果指定位置有节点,会丢弃,需要重新插入节点,主要是为了防止上一个节点状态对新节点状态产生污染 const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createField({ name: 'other', basePath: 'array', }) ) array.push({ value: 11 }, { value: 22 }) attach( form.createField({ name: 'value', basePath: 'array.0', }) ) attach( form.createField({ name: 'value', basePath: 'array.1', }) ) expect(array.value).toEqual([{ value: 11 }, { value: 22 }]) expect(form.query('array.0.value').get('value')).toEqual(11) expect(form.query('array.1.value').get('value')).toEqual(22) expect(Object.keys(form.fields).sort()).toEqual([ 'array', 'array.0.value', 'array.1.value', 'array.other', ]) array.pop() expect(array.value).toEqual([{ value: 11 }]) expect(form.query('array.0.value').get('value')).toEqual(11) expect(form.query('array.1.value').get('value')).toBeUndefined() array.unshift({ value: 33 }) attach( form.createField({ name: 'value', basePath: 'array.0', }) ) attach( form.createField({ name: 'value', basePath: 'array.1', }) ) expect(array.value).toEqual([{ value: 33 }, { value: 11 }]) expect(form.query('array.0.value').get('value')).toEqual(33) expect(form.query('array.1.value').get('value')).toEqual(11) array.remove(1) expect(array.value).toEqual([{ value: 33 }]) expect(form.query('array.0.value').get('value')).toEqual(33) expect(form.query('array.1.value').get('value')).toBeUndefined() array.insert(1, { value: 44 }, { value: 55 }) attach( form.createField({ name: 'value', basePath: 'array.1', }) ) attach( form.createField({ name: 'value', basePath: 'array.2', }) ) expect(array.value).toEqual([{ value: 33 }, { value: 44 }, { value: 55 }]) expect(form.query('array.0.value').get('value')).toEqual(33) expect(form.query('array.1.value').get('value')).toEqual(44) expect(form.query('array.2.value').get('value')).toEqual(55) array.move(0, 2) expect(array.value).toEqual([{ value: 44 }, { value: 55 }, { value: 33 }]) expect(form.query('array.0.value').get('value')).toEqual(44) expect(form.query('array.1.value').get('value')).toEqual(55) expect(form.query('array.2.value').get('value')).toEqual(33) array.move(2, 0) expect(array.value).toEqual([{ value: 33 }, { value: 44 }, { value: 55 }]) expect(form.query('array.0.value').get('value')).toEqual(33) expect(form.query('array.1.value').get('value')).toEqual(44) expect(form.query('array.2.value').get('value')).toEqual(55) }) test('array field move up/down then fields move', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createField({ name: 'value', basePath: 'array.0', }) ) attach( form.createField({ name: 'value', basePath: 'array.1', }) ) attach( form.createField({ name: 'value', basePath: 'array.2', }) ) attach( form.createField({ name: 'value', basePath: 'array.3', }) ) const line0 = form.fields['array.0.value'] const line1 = form.fields['array.1.value'] const line2 = form.fields['array.2.value'] const line3 = form.fields['array.3.value'] array.push({ value: '0' }, { value: '1' }, { value: '2' }, { value: '3' }) array.move(0, 3) // 1,2,3,0 expect(form.fields['array.0.value']).toBe(line1) expect(form.fields['array.1.value']).toBe(line2) expect(form.fields['array.2.value']).toBe(line3) expect(form.fields['array.3.value']).toBe(line0) array.move(3, 1) // 1,0,2,3 expect(form.fields['array.0.value']).toBe(line1) expect(form.fields['array.1.value']).toBe(line0) expect(form.fields['array.2.value']).toBe(line2) expect(form.fields['array.3.value']).toBe(line3) }) // 重现 issues #3932 , 补全 PR #3992 测试用例 test('lazy array field query each', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) const init = Array.from({ length: 6 }).map((_, i) => ({ value: i })) array.setValue(init) // page1: 0, 1 // page2: 2, 3 untouch // page3: 4, 5 init.forEach((item) => { const len = item.value //2, 3 if (len >= 2 && len <= 3) { } else { // 0, 1, 4, 5 attach( form.createField({ name: 'value', basePath: 'array.' + len, }) ) } }) array.insert(1, { value: '11' }) expect(() => form.query('*').take()).not.toThrowError() expect(Object.keys(form.fields)).toEqual([ 'array', 'array.0.value', 'array.5.value', 'array.2.value', 'array.6.value', ]) }) test('void children', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createField({ name: 'other', basePath: 'array', }) ) attach( form.createVoidField({ name: 0, basePath: 'array', }) ) const aaa = attach( form.createField({ name: 'aaa', basePath: 'array.0', value: 123, }) ) expect(aaa.value).toEqual(123) expect(array.value).toEqual([123]) }) test('exchange children', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createField({ name: 'other', basePath: 'array', }) ) attach( form.createField({ name: '0.aaa', basePath: 'array', value: '123', }) ) attach( form.createField({ name: '0.bbb', basePath: 'array', value: '321', }) ) attach( form.createField({ name: '1.bbb', basePath: 'array', value: 'kkk', }) ) expect(array.value).toEqual([{ aaa: '123', bbb: '321' }, { bbb: 'kkk' }]) array.move(0, 1) expect(array.value).toEqual([{ bbb: 'kkk' }, { aaa: '123', bbb: '321' }]) expect(form.query('array.0.aaa').take()).toBeUndefined() }) test('fault tolerance', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) const array2 = attach( form.createArrayField({ name: 'array2', value: [1, 2], }) ) array.setValue({} as any) array.push(11) expect(array.value).toEqual([11]) array.pop() expect(array.value).toEqual([]) array.remove(1) expect(array.value).toEqual([]) array.shift() expect(array.value).toEqual([]) array.unshift(1) expect(array.value).toEqual([1]) array.move(0, 1) expect(array.value).toEqual([1]) array.moveUp(1) expect(array.value).toEqual([1]) array.moveDown(1) expect(array.value).toEqual([1]) array.insert(1) expect(array.value).toEqual([1]) array2.move(1, 1) expect(array2.value).toEqual([1, 2]) array2.push(3) array2.moveUp(2) expect(array2.value).toEqual([1, 3, 2]) array2.moveUp(0) expect(array2.value).toEqual([3, 2, 1]) array2.moveDown(0) expect(array2.value).toEqual([2, 3, 1]) array2.moveDown(1) expect(array2.value).toEqual([2, 1, 3]) array2.moveDown(2) expect(array2.value).toEqual([3, 2, 1]) }) test('mutation fault tolerance', () => { const form = attach(createForm()) const pushArray = attach( form.createArrayField({ name: 'array1', }) ) const popArray = attach( form.createArrayField({ name: 'array2', }) ) const insertArray = attach( form.createArrayField({ name: 'array3', }) ) const removeArray = attach( form.createArrayField({ name: 'array4', }) ) const shiftArray = attach( form.createArrayField({ name: 'array5', }) ) const unshiftArray = attach( form.createArrayField({ name: 'array6', }) ) const moveArray = attach( form.createArrayField({ name: 'array7', }) ) const moveUpArray = attach( form.createArrayField({ name: 'array8', }) ) const moveDownArray = attach( form.createArrayField({ name: 'array9', }) ) pushArray.setValue({} as any) pushArray.push(123) expect(pushArray.value).toEqual([123]) popArray.setValue({} as any) popArray.pop() expect(popArray.value).toEqual({}) insertArray.setValue({} as any) insertArray.insert(0, 123) expect(insertArray.value).toEqual([123]) removeArray.setValue({} as any) removeArray.remove(0) expect(removeArray.value).toEqual({}) shiftArray.setValue({} as any) shiftArray.shift() expect(shiftArray.value).toEqual({}) unshiftArray.setValue({} as any) unshiftArray.unshift(123) expect(unshiftArray.value).toEqual([123]) moveArray.setValue({} as any) moveArray.move(0, 1) expect(moveArray.value).toEqual({}) moveUpArray.setValue({} as any) moveUpArray.moveUp(0) expect(moveUpArray.value).toEqual({}) moveDownArray.setValue({} as any) moveDownArray.moveDown(1) expect(moveDownArray.value).toEqual({}) }) test('array field move api with children', async () => { const form = attach(createForm()) attach( form.createField({ name: 'other', }) ) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createArrayField({ name: '0', basePath: 'array', }) ) attach( form.createArrayField({ name: '1', basePath: 'array', }) ) attach( form.createArrayField({ name: '2', basePath: 'array', }) ) attach( form.createArrayField({ name: 'name', basePath: 'array.2', }) ) await array.move(0, 2) expect(form.fields['array.0.name']).toBeUndefined() expect(form.fields['array.2.name']).toBeUndefined() expect(form.fields['array.1.name']).not.toBeUndefined() }) test('array field remove memo leak', async () => { const handler = jest.fn() const valuesChange = jest.fn() const initialValuesChange = jest.fn() const form = attach( createForm({ effects() { onFormValuesChange(valuesChange) onFormInitialValuesChange(initialValuesChange) onFieldValueChange('*', handler) }, }) ) const array = attach( form.createArrayField({ name: 'array', }) ) await array.push('') attach( form.createField({ name: '0', basePath: 'array', }) ) await array.remove(0) await array.push('') attach( form.createField({ name: '0', basePath: 'array', }) ) expect(handler).toBeCalledTimes(0) expect(valuesChange).toBeCalledTimes(4) expect(initialValuesChange).toBeCalledTimes(0) }) test('nest array remove', async () => { const form = attach(createForm()) const metrics = attach( form.createArrayField({ name: 'metrics', }) ) attach( form.createObjectField({ name: '0', basePath: 'metrics', }) ) attach( form.createObjectField({ name: '1', basePath: 'metrics', }) ) attach( form.createArrayField({ name: 'content', basePath: 'metrics.0', }) ) attach( form.createArrayField({ name: 'content', basePath: 'metrics.1', }) ) const obj00 = attach( form.createObjectField({ name: '0', basePath: 'metrics.0.content', }) ) const obj10 = attach( form.createObjectField({ name: '0', basePath: 'metrics.1.content', }) ) attach( form.createField({ name: 'attr', basePath: 'metrics.0.content.0', initialValue: '123', }) ) attach( form.createField({ name: 'attr', basePath: 'metrics.1.content.0', initialValue: '123', }) ) expect(obj00.indexes[0]).toBe(0) expect(obj00.index).toBe(0) expect(obj10.index).toBe(0) expect(obj10.indexes[0]).toBe(1) await (form.query('metrics.1.content').take() as any).remove(0) expect(form.fields['metrics.0.content.0.attr']).not.toBeUndefined() await metrics.remove(1) expect(form.fields['metrics.0.content.0.attr']).not.toBeUndefined() expect( form.initialValues.metrics?.[1]?.content?.[0]?.attr ).not.toBeUndefined() }) test('indexes: nest path need exclude incomplete number', () => { const form = attach(createForm()) const objPathIncludeNum = attach( form.createField({ name: 'attr', basePath: 'metrics.0.a.10.iconWidth50', }) ) expect(objPathIncludeNum.indexes.length).toBe(2) expect(objPathIncludeNum.indexes).toEqual([0, 10]) expect(objPathIncludeNum.index).toBe(10) }) test('incomplete insertion of array elements', async () => { const form = attach( createForm({ values: { array: [{ aa: 1 }, { aa: 2 }, { aa: 3 }], }, }) ) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createObjectField({ name: '0', basePath: 'array', }) ) attach( form.createField({ name: 'aa', basePath: 'array.0', }) ) attach( form.createObjectField({ name: '2', basePath: 'array', }) ) attach( form.createField({ name: 'aa', basePath: 'array.2', }) ) expect(form.fields['array.0.aa']).not.toBeUndefined() expect(form.fields['array.1.aa']).toBeUndefined() expect(form.fields['array.2.aa']).not.toBeUndefined() await array.unshift({}) expect(form.fields['array.0.aa']).toBeUndefined() expect(form.fields['array.1.aa']).not.toBeUndefined() expect(form.fields['array.2.aa']).toBeUndefined() expect(form.fields['array.3.aa']).not.toBeUndefined() }) test('void array items need skip data', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) const array2 = attach( form.createArrayField({ name: 'array2', }) ) attach( form.createVoidField({ name: '0', basePath: 'array', }) ) attach( form.createVoidField({ name: '0', basePath: 'array2', }) ) attach( form.createVoidField({ name: 'space', basePath: 'array.0', }) ) const select = attach( form.createField({ name: 'select', basePath: 'array.0.space', }) ) const select2 = attach( form.createField({ name: 'select2', basePath: 'array2.0', }) ) select.value = 123 select2.value = 123 expect(array.value).toEqual([123]) expect(array2.value).toEqual([123]) }) test('array field reset', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createObjectField({ name: '0', basePath: 'array', }) ) attach( form.createField({ name: 'input', initialValue: '123', basePath: 'array.0', }) ) form.reset('*', { forceClear: true }) expect(form.values).toEqual({ array: [] }) expect(array.value).toEqual([]) }) test('array field remove can not memory leak', async () => { const handler = jest.fn() const form = attach( createForm({ values: { array: [{ aa: 1 }, { aa: 2 }], }, effects() { onFieldValueChange('array.*.aa', handler) }, }) ) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createObjectField({ name: '0', basePath: 'array', }) ) attach( form.createField({ name: 'aa', basePath: 'array.0', }) ) attach( form.createObjectField({ name: '1', basePath: 'array', }) ) attach( form.createField({ name: 'aa', basePath: 'array.1', }) ) const bb = attach( form.createField({ name: 'bb', basePath: 'array.1', reactions: (field) => { field.visible = field.query('.aa').value() === '123' }, }) ) expect(bb.visible).toBeFalsy() await array.remove(0) form.query('array.0.aa').take((field) => { ;(field as DataField).value = '123' }) expect(bb.visible).toBeTruthy() expect(handler).toBeCalledTimes(1) }) test('array field patch values', async () => { const form = attach(createForm()) const arr = attach( form.createArrayField({ name: 'a', }) ) await arr.unshift({}) attach( form.createObjectField({ name: '0', basePath: 'a', }) ) attach( form.createField({ name: 'c', initialValue: 'A', basePath: 'a.0', }) ) expect(form.values).toEqual({ a: [{ c: 'A' }] }) await arr.unshift({}) attach( form.createObjectField({ name: '0', basePath: 'a', }) ) attach( form.createField({ name: 'c', initialValue: 'A', basePath: 'a.0', }) ) attach( form.createObjectField({ name: '1', basePath: 'a', }) ) attach( form.createField({ name: 'c', initialValue: 'A', basePath: 'a.1', }) ) expect(form.values).toEqual({ a: [{ c: 'A' }, { c: 'A' }] }) }) test('array remove with initialValues', async () => { const form = attach( createForm({ initialValues: { array: [{ a: 1 }, { a: 2 }], }, }) ) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createObjectField({ name: '0', basePath: 'array', }) ) attach( form.createObjectField({ name: '1', basePath: 'array', }) ) attach( form.createField({ name: 'a', basePath: 'array.0', }) ) attach( form.createField({ name: 'a', basePath: 'array.1', }) ) expect(form.values).toEqual({ array: [{ a: 1 }, { a: 2 }] }) await array.remove(1) expect(form.values).toEqual({ array: [{ a: 1 }] }) expect(form.initialValues).toEqual({ array: [{ a: 1 }, { a: 2 }] }) await array.reset() attach( form.createObjectField({ name: '1', basePath: 'array', }) ) attach( form.createField({ name: 'a', basePath: 'array.0', }) ) attach( form.createField({ name: 'a', basePath: 'array.1', }) ) expect(form.values).toEqual({ array: [{ a: 1 }, { a: 2 }] }) expect(form.initialValues).toEqual({ array: [{ a: 1 }, { a: 2 }] }) }) test('records: find array fields', () => { const form = attach( createForm({ initialValues: { array: [{ a: 1 }, { a: 2 }], }, }) ) attach( form.createArrayField({ name: 'array', }) ) attach( form.createObjectField({ name: '0', basePath: 'array', }) ) attach( form.createObjectField({ name: '1', basePath: 'array', }) ) const field0 = attach( form.createField({ name: 'a', basePath: 'array.0', }) ) const field1 = attach( form.createField({ name: 'a', basePath: 'array.1', }) ) expect(field0.records.length).toBe(2) expect(field0.record).toEqual({ a: 1 }) expect(field1.record).toEqual({ a: 2 }) }) test('record: find array nest field record', () => { const form = attach( createForm({ initialValues: { array: [{ a: { b: { c: 1, d: 1 } } }, { a: { b: { c: 2, d: 2 } } }], }, }) ) attach( form.createArrayField({ name: 'array', }) ) attach( form.createObjectField({ name: '0', basePath: 'array', }) ) attach( form.createObjectField({ name: '1', basePath: 'array', }) ) attach( form.createObjectField({ name: 'a', basePath: 'array.0', }) ) attach( form.createObjectField({ name: 'a', basePath: 'array.1', }) ) attach( form.createObjectField({ name: 'b', basePath: 'array.0.a', }) ) attach( form.createObjectField({ name: 'b', basePath: 'array.1.a', }) ) const field0 = attach( form.createField({ name: 'c', basePath: 'array.0.a.b', }) ) const field1 = attach( form.createField({ name: 'c', basePath: 'array.1.a.b', }) ) const field2 = attach( form.createField({ name: 'cc', basePath: 'array.1.a.b.c', }) ) expect(field0.records.length).toBe(2) expect(field1.records.length).toBe(2) expect(field1.records).toEqual([ { a: { b: { c: 1, d: 1 } } }, { a: { b: { c: 2, d: 2 } } }, ]) expect(field0.record).toEqual({ c: 1, d: 1 }) expect(field1.record).toEqual({ c: 2, d: 2 }) expect(field2.record).toEqual({ c: 2, d: 2 }) }) test('record: find array field record', () => { const form = attach( createForm({ initialValues: { array: [1, 2, 3], }, }) ) attach( form.createArrayField({ name: 'array', }) ) const field = attach( form.createField({ basePath: 'array', name: '0', }) ) expect(field.records.length).toBe(3) expect(field.record).toEqual(1) }) test('record: find object field record', () => { const form = attach( createForm({ initialValues: { a: { b: { c: 1, d: 1, }, }, }, }) ) attach( form.createArrayField({ name: 'a', }) ) attach( form.createObjectField({ name: 'b', basePath: 'a', }) ) const fieldc = attach( form.createObjectField({ name: 'c', basePath: 'a.b', }) ) expect(fieldc.records).toEqual(undefined) expect(fieldc.record).toEqual({ c: 1, d: 1, }) }) test('record: find form fields', () => { const form = attach( createForm({ initialValues: { array: [{ a: 1 }, { a: 2 }], }, }) ) const array = attach( form.createArrayField({ name: 'array', }) ) expect(array.record).toEqual({ array: [{ a: 1 }, { a: 2 }] }) }) ================================================ FILE: packages/core/src/__tests__/effects.spec.ts ================================================ import { createForm, createEffectContext, onFieldChange, onFieldInit, onFieldInitialValueChange, onFieldInputValueChange, onFieldMount, onFieldReact, onFieldUnmount, onFieldValidateEnd, onFieldValidateStart, onFieldValidateFailed, onFieldValidateSuccess, onFieldValueChange, onFormInit, onFormInitialValuesChange, onFormInputChange, onFormMount, onFormReact, onFormReset, onFormSubmit, onFormSubmitEnd, onFormSubmitFailed, onFormSubmitStart, onFormSubmitSuccess, onFormSubmitValidateFailed, onFormSubmitValidateStart, onFormSubmitValidateSuccess, onFormSubmitValidateEnd, onFormUnmount, onFormValidateEnd, onFormValidateStart, onFormValidateFailed, onFormValidateSuccess, onFormValuesChange, isVoidField, } from '../' import { runEffects } from '../shared/effective' import { attach, sleep } from './shared' test('onFormInit/onFormMount/onFormUnmount', () => { const mount = jest.fn() const init = jest.fn() const unmount = jest.fn() const form = attach( createForm({ effects() { onFormInit(init) onFormMount(mount) onFormUnmount(unmount) }, }) ) expect(init).toBeCalled() expect(mount).toBeCalled() expect(unmount).not.toBeCalled() form.onUnmount() expect(unmount).toBeCalled() }) test('onFormValuesChange/onFormInitialValuesChange', () => { const valuesChange = jest.fn() const initialValuesChange = jest.fn() const form = attach( createForm({ effects() { onFormValuesChange(valuesChange) onFormInitialValuesChange(initialValuesChange) }, }) ) expect(valuesChange).not.toBeCalled() expect(initialValuesChange).not.toBeCalled() form.setValues({ aa: '123', }) expect(form.values.aa).toEqual('123') expect(valuesChange).toBeCalled() form.setInitialValues({ aa: '321', bb: '123', }) expect(form.values.aa).toEqual('321') expect(form.values.bb).toEqual('123') expect(initialValuesChange).toBeCalled() }) test('onFormInputChange', () => { const inputChange = jest.fn() const valuesChange = jest.fn() const form = attach( createForm({ effects() { onFormValuesChange(valuesChange) onFormInputChange(inputChange) }, }) ) const field = attach( form.createField({ name: 'aa', }) ) expect(inputChange).not.toBeCalled() expect(valuesChange).not.toBeCalled() field.setValue('123') expect(inputChange).not.toBeCalled() expect(valuesChange).toBeCalledTimes(1) field.onInput('123') expect(inputChange).toBeCalled() expect(valuesChange).toBeCalledTimes(1) field.onInput('321') expect(inputChange).toBeCalledTimes(2) expect(valuesChange).toBeCalledTimes(2) }) test('onFormReact', () => { const react = jest.fn() const form = attach( createForm({ effects() { onFormReact((form) => { if (form.values.aa) { react() } }) }, }) ) expect(react).not.toBeCalled() form.setValues({ aa: 123 }) expect(react).toBeCalled() form.onUnmount() // will not throw error const form2 = attach( createForm({ effects() { onFormReact() }, }) ) form2.onUnmount() }) test('onFormReset', async () => { const reset = jest.fn() const form = attach( createForm({ initialValues: { aa: 123, }, effects() { onFormReset(reset) }, }) ) const field = attach( form.createField({ name: 'aa', }) ) field.setValue('xxxx') expect(field.value).toEqual('xxxx') expect(form.values.aa).toEqual('xxxx') expect(reset).not.toBeCalled() await form.reset() expect(field.value).toEqual(123) expect(form.values.aa).toEqual(123) expect(reset).toBeCalled() }) test('onFormSubmit', async () => { const submit = jest.fn() const submitStart = jest.fn() const submitEnd = jest.fn() const submitSuccess = jest.fn() const submitFailed = jest.fn() const submitValidateStart = jest.fn() const submitValidateFailed = jest.fn() const submitValidateSuccess = jest.fn() const submitValidateEnd = jest.fn() const form = attach( createForm({ effects() { onFormSubmitStart(submitStart) onFormSubmit(submit) onFormSubmitEnd(submitEnd) onFormSubmitFailed(submitFailed) onFormSubmitSuccess(submitSuccess) onFormSubmitValidateStart(submitValidateStart) onFormSubmitValidateFailed(submitValidateFailed) onFormSubmitValidateSuccess(submitValidateSuccess) onFormSubmitValidateEnd(submitValidateEnd) }, }) ) const field = attach( form.createField({ name: 'aa', required: true, }) ) try { await form.submit() } catch {} expect(submitStart).toBeCalled() expect(submit).toBeCalled() expect(submitEnd).toBeCalled() expect(submitSuccess).not.toBeCalled() expect(submitFailed).toBeCalled() expect(submitValidateStart).toBeCalled() expect(submitValidateFailed).toBeCalled() expect(submitValidateSuccess).not.toBeCalled() expect(submitValidateEnd).toBeCalled() field.onInput('123') try { await form.submit() } catch (e) {} expect(submitStart).toBeCalledTimes(2) expect(submit).toBeCalledTimes(2) expect(submitEnd).toBeCalledTimes(2) expect(submitSuccess).toBeCalledTimes(1) expect(submitFailed).toBeCalledTimes(1) expect(submitValidateStart).toBeCalledTimes(2) expect(submitValidateFailed).toBeCalledTimes(1) expect(submitValidateSuccess).toBeCalledTimes(1) expect(submitValidateEnd).toBeCalledTimes(2) }) test('onFormValidate', async () => { const validateStart = jest.fn() const validateEnd = jest.fn() const validateFailed = jest.fn() const validateSuccess = jest.fn() const form = attach( createForm({ effects() { onFormValidateStart(validateStart) onFormValidateEnd(validateEnd) onFormValidateFailed(validateFailed) onFormValidateSuccess(validateSuccess) }, }) ) const field = attach( form.createField({ name: 'aa', required: true, }) ) try { await form.validate() } catch {} expect(validateStart).toBeCalled() expect(validateEnd).toBeCalled() expect(validateFailed).toBeCalled() expect(validateSuccess).not.toBeCalled() field.onInput('123') try { await form.validate() } catch {} expect(validateStart).toBeCalledTimes(2) expect(validateEnd).toBeCalledTimes(2) expect(validateFailed).toBeCalledTimes(1) expect(validateSuccess).toBeCalledTimes(1) }) test('onFieldChange', async () => { const fieldChange = jest.fn() const valueChange = jest.fn() const valueChange2 = jest.fn() const form = attach( createForm({ effects() { onFieldChange( 'aa', [ 'value', 'disabled', 'initialized', 'inputValue', 'loading', 'visible', 'editable', ], fieldChange ) onFieldChange('aa', valueChange) onFieldChange('aa', undefined, valueChange2) onFieldChange('aa') }, }) ) const field = attach( form.createField({ name: 'aa', }) ) expect(fieldChange).toBeCalledTimes(1) field.setValue('123') expect(fieldChange).toBeCalledTimes(2) field.onInput('321') expect(fieldChange).toBeCalledTimes(3) field.setLoading(true) expect(fieldChange).toBeCalledTimes(3) await sleep() expect(fieldChange).toBeCalledTimes(4) field.setPattern('disabled') expect(fieldChange).toBeCalledTimes(5) field.setDisplay('none') expect(fieldChange).toBeCalledTimes(6) form.onUnmount() expect(valueChange).toBeCalledTimes(4) expect(valueChange2).toBeCalledTimes(4) }) test('onFieldInit/onFieldMount/onFieldUnmount', () => { const fieldInit = jest.fn() const fieldMount = jest.fn() const fieldUnmount = jest.fn() const form = attach( createForm({ effects() { onFieldInit('aa', fieldInit) onFieldMount('aa', fieldMount) onFieldUnmount('aa', fieldUnmount) }, }) ) const field = attach( form.createField({ name: 'aa', }) ) expect(fieldInit).toBeCalledTimes(1) expect(fieldMount).toBeCalledTimes(1) expect(fieldUnmount).toBeCalledTimes(0) field.onUnmount() expect(fieldUnmount).toBeCalledTimes(1) }) test('onFieldInitialValueChange/onFieldValueChange/onFieldInputValueChange', () => { const fieldValueChange = jest.fn() const fieldInitialValueChange = jest.fn() const fieldInputValueChange = jest.fn() const notTrigger = jest.fn() const form = attach( createForm({ effects() { onFieldInitialValueChange('aa', fieldInitialValueChange) onFieldValueChange('aa', fieldValueChange) onFieldInputValueChange('aa', fieldInputValueChange) onFieldValueChange('xx', notTrigger) }, }) ) const field = attach( form.createField({ name: 'aa', }) ) field.setValue('123') expect(fieldValueChange).toBeCalledTimes(1) expect(fieldInitialValueChange).toBeCalledTimes(0) expect(fieldInputValueChange).toBeCalledTimes(0) field.setInitialValue('xxx') expect(fieldValueChange).toBeCalledTimes(2) expect(fieldInitialValueChange).toBeCalledTimes(1) expect(fieldInputValueChange).toBeCalledTimes(0) field.onInput('321') expect(fieldValueChange).toBeCalledTimes(3) expect(fieldInitialValueChange).toBeCalledTimes(1) expect(fieldInputValueChange).toBeCalledTimes(1) expect(notTrigger).toBeCalledTimes(0) }) test('onFieldReact', () => { const react = jest.fn() const form = attach( createForm({ effects() { onFieldReact('aa', (field) => { if (isVoidField(field)) return if (field.value) { react() } if (field.display === 'hidden') { react() } }) onFieldReact('aa', null) }, }) ) const field = attach( form.createField({ name: 'aa', }) ) expect(react).not.toBeCalled() form.setValues({ aa: 123 }) expect(react).toBeCalledTimes(1) field.setDisplay('hidden') expect(react).toBeCalledTimes(3) form.onUnmount() }) test('onFieldValidate', async () => { const validateStart = jest.fn() const validateFailed = jest.fn() const validateSuccess = jest.fn() const validateEnd = jest.fn() const form = attach( createForm({ effects() { onFieldValidateStart('aa', validateStart) onFieldValidateEnd('aa', validateEnd) onFieldValidateFailed('aa', validateFailed) onFieldValidateSuccess('aa', validateSuccess) }, }) ) const field = attach( form.createField({ name: 'aa', required: true, }) ) try { await field.validate() } catch {} expect(validateStart).toBeCalled() expect(validateFailed).toBeCalled() expect(validateSuccess).not.toBeCalled() expect(validateEnd).toBeCalled() field.setValue('123') try { await field.validate() } catch {} expect(validateStart).toBeCalledTimes(2) expect(validateFailed).toBeCalledTimes(1) expect(validateSuccess).toBeCalledTimes(1) expect(validateEnd).toBeCalledTimes(2) }) test('async use will throw error', async () => { const valueChange = jest.fn() let error const form = attach( createForm({ effects() { setTimeout(() => { try { onFieldValueChange('aa', valueChange) } catch (e) { error = e } }, 0) }, }) ) const aa = attach( form.createField({ name: 'aa', }) ) await sleep(10) aa.setValue('123') expect(valueChange).toBeCalledTimes(0) expect(error).not.toBeUndefined() }) test('effect context', async () => { const context = createEffectContext() const context2 = createEffectContext() const context3 = createEffectContext(123) let results: any let error: any let error2: any const consumer = () => { results = context.consume() } const consumer2 = () => { setTimeout(() => { try { results = context2.consume() } catch (e) { error2 = e } }, 0) } attach( createForm({ effects() { context.provide(123) context3.provide() consumer() setTimeout(() => { try { context2.provide(123) } catch (e) { error = e } }, 0) consumer2() }, }) ) await sleep(10) expect(results).toEqual(123) expect(error).not.toBeUndefined() expect(error2).not.toBeUndefined() }) test('runEffects', () => { expect( runEffects(123, () => { onFormMount(() => {}) }).length ).toEqual(1) }) ================================================ FILE: packages/core/src/__tests__/externals.spec.ts ================================================ import { createForm } from '..' import { isArrayField, isArrayFieldState, isDataField, isDataFieldState, isField, isFieldState, isForm, isFormState, isGeneralField, isGeneralFieldState, isObjectField, isObjectFieldState, isQuery, isVoidField, isVoidFieldState, createEffectHook, } from '../shared/externals' import { attach } from './shared' test('type checkers', () => { const form = attach(createForm()) const normal = attach( form.createField({ name: 'normal', }) ) const array = attach( form.createArrayField({ name: 'array', }) ) const object = attach( form.createObjectField({ name: 'object', }) ) const void_ = attach( form.createVoidField({ name: 'void', }) ) expect(isField(normal)).toBeTruthy() expect(isFieldState(normal.getState())).toBeTruthy() expect(isFieldState(null)).toBeFalsy() expect(isFieldState({})).toBeFalsy() expect(isFieldState(normal)).toBeFalsy() expect(isArrayField(array)).toBeTruthy() expect(isArrayFieldState(array.getState())).toBeTruthy() expect(isArrayFieldState(null)).toBeFalsy() expect(isArrayFieldState({})).toBeFalsy() expect(isArrayFieldState(array)).toBeFalsy() expect(isObjectField(object)).toBeTruthy() expect(isObjectFieldState(object.getState())).toBeTruthy() expect(isObjectFieldState(null)).toBeFalsy() expect(isObjectFieldState({})).toBeFalsy() expect(isObjectFieldState(object)).toBeFalsy() expect(isVoidField(void_)).toBeTruthy() expect(isVoidFieldState(void_.getState())).toBeTruthy() expect(isVoidFieldState(null)).toBeFalsy() expect(isVoidFieldState({})).toBeFalsy() expect(isVoidFieldState(void_)).toBeFalsy() expect(isDataField(void_)).toBeFalsy() expect(isDataFieldState(void_.getState())).toBeFalsy() expect(isDataField(normal)).toBeTruthy() expect(isDataFieldState(normal.getState())).toBeTruthy() expect(isGeneralField(normal)).toBeTruthy() expect(isGeneralField(array)).toBeTruthy() expect(isGeneralField(object)).toBeTruthy() expect(isGeneralField(void_)).toBeTruthy() expect(isGeneralFieldState(normal.getState())).toBeTruthy() expect(isGeneralFieldState(array.getState())).toBeTruthy() expect(isGeneralFieldState(object.getState())).toBeTruthy() expect(isGeneralFieldState(void_.getState())).toBeTruthy() expect(isGeneralFieldState(null)).toBeFalsy() expect(isGeneralFieldState({})).toBeFalsy() expect(isGeneralFieldState(void_)).toBeFalsy() expect(isForm(form)).toBeTruthy() expect(isFormState(form.getState())).toBeTruthy() expect(isFormState({})).toBeFalsy() expect(isFormState(form)).toBeFalsy() expect(isFormState(null)).toBeFalsy() expect(isQuery(form.query('*'))).toBeTruthy() }) test('createEffectHook', () => { try { createEffectHook('xxx')() } catch {} const form = attach( createForm({ effects() { createEffectHook('xxx')() createEffectHook('yyy', () => () => {})() }, }) ) form.notify('xxx') form.notify('yyy') }) ================================================ FILE: packages/core/src/__tests__/field.spec.ts ================================================ import { autorun, batch, observable } from '@formily/reactive' import { createForm, onFieldReact, isField } from '../' import { DataField } from '../types' import { attach, sleep } from './shared' test('create field', () => { const form = attach(createForm()) const field = attach( form.createField({ name: 'normal', }) ) expect(field).not.toBeUndefined() }) test('create field props', () => { const form = attach(createForm()) const field1 = attach( form.createField({ name: 'field1', title: 'Field 1', description: 'This is Field 1', required: true, }) ) expect(field1.title).toEqual('Field 1') expect(field1.description).toEqual('This is Field 1') expect(field1.required).toBeTruthy() expect(field1.validator).not.toBeUndefined() const field2 = attach( form.createField({ name: 'field2', disabled: true, hidden: true, }) ) expect(field2.pattern).toEqual('disabled') expect(field2.disabled).toBeTruthy() expect(field2.display).toEqual('hidden') expect(field2.hidden).toBeTruthy() const field3 = attach( form.createField({ name: 'field3', readOnly: true, visible: false, }) ) expect(field3.pattern).toEqual('readOnly') expect(field3.readOnly).toBeTruthy() expect(field3.display).toEqual('none') expect(field3.visible).toBeFalsy() const field4 = attach( form.createField({ name: 'field4', value: 123, }) ) expect(field4.value).toEqual(123) expect(field4.initialValue).toBeUndefined() const field5 = attach( form.createField({ name: 'field5', initialValue: 123, }) ) expect(field5.value).toEqual(123) expect(field5.initialValue).toEqual(123) }) test('field display and value', () => { const form = attach(createForm()) const objectField = attach( form.createObjectField({ name: 'object', }) ) const arrayField = attach( form.createArrayField({ name: 'array', }) ) const valueField = attach( form.createField({ name: 'value', }) ) expect(objectField.value).toEqual({}) expect(arrayField.value).toEqual([]) expect(valueField.value).toBeUndefined() objectField.hidden = true arrayField.hidden = true valueField.hidden = true expect(objectField.value).toEqual({}) expect(arrayField.value).toEqual([]) expect(valueField.value).toBeUndefined() objectField.hidden = false arrayField.hidden = false valueField.hidden = false expect(objectField.value).toEqual({}) expect(arrayField.value).toEqual([]) expect(valueField.value).toBeUndefined() objectField.visible = false arrayField.visible = false valueField.visible = false expect(objectField.value).toBeUndefined() expect(arrayField.value).toBeUndefined() expect(valueField.value).toBeUndefined() objectField.visible = true arrayField.visible = true valueField.visible = true expect(objectField.value).toEqual({}) expect(arrayField.value).toEqual([]) expect(valueField.value).toBeUndefined() objectField.value = { value: '123' } arrayField.value = ['123'] valueField.value = '123' expect(objectField.value).toEqual({ value: '123' }) expect(arrayField.value).toEqual(['123']) expect(valueField.value).toEqual('123') objectField.hidden = true arrayField.hidden = true valueField.hidden = true expect(objectField.value).toEqual({ value: '123' }) expect(arrayField.value).toEqual(['123']) expect(valueField.value).toEqual('123') objectField.hidden = false arrayField.hidden = false valueField.hidden = false expect(objectField.value).toEqual({ value: '123' }) expect(arrayField.value).toEqual(['123']) expect(valueField.value).toEqual('123') objectField.visible = false arrayField.visible = false valueField.visible = false expect(objectField.value).toBeUndefined() expect(arrayField.value).toBeUndefined() expect(valueField.value).toBeUndefined() objectField.visible = true arrayField.visible = true valueField.visible = true expect(objectField.value).toEqual({ value: '123' }) expect(arrayField.value).toEqual(['123']) expect(valueField.value).toEqual('123') }) test('nested display/pattern', () => { const form = attach(createForm()) const object_ = attach( form.createObjectField({ name: 'object', }) ) const void_ = attach( form.createVoidField({ name: 'void', basePath: 'object', }) ) const aaa = attach( form.createField({ name: 'aaa', basePath: 'object.void', }) ) const bbb = attach( form.createField({ name: 'bbb', basePath: 'object', }) ) const ddd = attach( form.createField({ name: 'ddd', }) ) expect(ddd.visible).toBeTruthy() expect(ddd.editable).toBeTruthy() object_.setPattern('readPretty') expect(void_.pattern).toEqual('readPretty') expect(aaa.pattern).toEqual('readPretty') expect(bbb.pattern).toEqual('readPretty') object_.setPattern('readOnly') expect(void_.pattern).toEqual('readOnly') expect(aaa.pattern).toEqual('readOnly') expect(bbb.pattern).toEqual('readOnly') object_.setPattern('disabled') expect(void_.pattern).toEqual('disabled') expect(aaa.pattern).toEqual('disabled') expect(bbb.pattern).toEqual('disabled') object_.setPattern() expect(void_.pattern).toEqual('editable') expect(aaa.pattern).toEqual('editable') expect(bbb.pattern).toEqual('editable') object_.setDisplay('hidden') expect(void_.display).toEqual('hidden') expect(aaa.display).toEqual('hidden') expect(bbb.display).toEqual('hidden') object_.setDisplay('none') expect(void_.display).toEqual('none') expect(aaa.display).toEqual('none') expect(bbb.display).toEqual('none') object_.setDisplay() expect(void_.display).toEqual('visible') expect(aaa.display).toEqual('visible') expect(bbb.display).toEqual('visible') aaa.setValue('123') expect(aaa.value).toEqual('123') aaa.setDisplay('none') expect(aaa.value).toBeUndefined() aaa.setDisplay('visible') expect(aaa.value).toEqual('123') aaa.setValue('123') object_.setDisplay('none') expect(aaa.value).toBeUndefined() object_.setDisplay('visible') expect(aaa.value).toEqual('123') }) test('setValue/setInitialValue', () => { const form = attach(createForm()) const aaa = attach( form.createField({ name: 'aaa', }) ) const bbb = attach( form.createField({ name: 'bbb', }) ) aaa.setValue('123') expect(aaa.value).toEqual('123') expect(form.values.aaa).toEqual('123') bbb.setValue('123') expect(bbb.value).toEqual('123') expect(form.values.bbb).toEqual('123') const ccc = attach( form.createField({ name: 'ccc', }) ) const ddd = attach( form.createField({ name: 'ddd', }) ) ccc.setInitialValue('123') expect(ccc.value).toEqual('123') expect(ccc.initialValue).toEqual('123') expect(form.values.ccc).toEqual('123') ddd.setInitialValue('123') expect(ddd.value).toEqual('123') expect(ddd.initialValue).toEqual('123') expect(form.values.ddd).toEqual('123') ccc.setInitialValue('222') expect(ccc.value).toEqual('222') expect(ccc.initialValue).toEqual('222') expect(form.values.ccc).toEqual('222') ddd.setInitialValue('222') expect(ddd.value).toEqual('222') expect(ddd.initialValue).toEqual('222') expect(form.values.ddd).toEqual('222') }) test('setLoading/setValidating', async () => { const form = attach(createForm()) const field = attach( form.createField({ name: 'aa', }) ) field.setLoading(true) expect(field.loading).toBeFalsy() await sleep() expect(field.loading).toBeTruthy() field.setLoading(false) field.setLoading(false) expect(field.loading).toBeFalsy() field.setValidating(true) expect(field.validating).toBeFalsy() await sleep() expect(field.validating).toBeTruthy() field.setValidating(false) expect(field.validating).toBeFalsy() }) test('setComponent/setComponentProps', () => { const component = () => null const form = attach(createForm()) const field = attach( form.createField({ name: 'aa', }) ) field.setComponent(undefined, { props: 123 }) field.setComponent(component) expect(field.component[0]).toEqual(component) expect(field.component[1]).toEqual({ props: 123 }) field.setComponentProps({ hello: 'world', }) expect(field.component[1]).toEqual({ props: 123, hello: 'world' }) }) test('setDecorator/setDecoratorProps', () => { const component = () => null const form = attach(createForm()) const field = attach( form.createField({ name: 'aa', }) ) field.setDecorator(undefined, { props: 123 }) field.setDecorator(component) expect(field.decorator[0]).toEqual(component) expect(field.decorator[1]).toEqual({ props: 123 }) field.setDecoratorProps({ hello: 'world', }) expect(field.decorator[1]).toEqual({ props: 123, hello: 'world' }) }) test('reaction initialValue', () => { const form = attach( createForm({ values: { aa: 123, }, }) ) const aa = attach( form.createField({ name: 'aa', reactions(field) { field.initialValue = 321 }, }) ) const bb = attach( form.createField({ name: 'bb', value: 123, reactions(field) { field.initialValue = 321 }, }) ) expect(aa.value).toEqual(123) expect(bb.value).toEqual(123) }) test('selfValidate/errors/warnings/successes/valid/invalid/validateStatus/queryFeedbacks', async () => { const form = attach(createForm()) const field = attach( form.createField({ name: 'aa', required: true, validateFirst: true, validator: [ (value) => { if (value == '123') { return { type: 'success', message: 'success', } } else if (value == '321') { return { type: 'warning', message: 'warning', } } else if (value == '111') { return 'error' } }, { triggerType: 'onBlur', format: 'url', }, { triggerType: 'onFocus', format: 'date', }, ], }) ) const field2 = attach( form.createField({ name: 'bb', required: true, value: '111', validator: [ (value) => { if (value == '123') { return { type: 'success', message: 'success', } } else if (value == '321') { return { type: 'warning', message: 'warning', } } else if (value == '111') { return 'error' } }, { triggerType: 'onBlur', format: 'url', }, { triggerType: 'onFocus', format: 'date', }, ], }) ) const field3 = attach( form.createField({ name: 'xxx', }) ) const field4 = attach( form.createField({ name: 'ppp', required: true, }) ) try { await field.validate() } catch {} try { await field2.validate() } catch {} expect(field.invalid).toBeTruthy() expect(field.selfErrors.length).toEqual(1) expect(field2.invalid).toBeTruthy() expect(field2.selfErrors.length).toEqual(3) await field.onInput('123') expect(field.selfSuccesses).toEqual(['success']) await field.onInput('321') expect(field.selfWarnings).toEqual(['warning']) await field.onInput('111') expect(field.selfErrors).toEqual(['error']) await field.onBlur() expect(field.selfErrors).toEqual([ 'error', 'The field value is a invalid url', ]) await field.onFocus() expect(field.selfErrors).toEqual([ 'error', 'The field value is a invalid url', 'The field value is not a valid date format', ]) field.setFeedback() expect(field.selfErrors).toEqual([ 'error', 'The field value is a invalid url', 'The field value is not a valid date format', ]) expect(field3.feedbacks).toEqual([]) field3.setFeedback() field3.setFeedback({ messages: null }) field3.setFeedback({ messages: ['error'], code: 'EffectError' }) field3.setFeedback({ messages: ['error2'], code: 'EffectError' }) expect(field3.feedbacks).toEqual([ { code: 'EffectError', messages: ['error2'] }, ]) expect( field3.queryFeedbacks({ address: 'xxx', }) ).toEqual([{ code: 'EffectError', messages: ['error2'] }]) expect( field3.queryFeedbacks({ address: 'yyy', }) ).toEqual([]) expect( field3.queryFeedbacks({ path: 'yyy', }) ).toEqual([]) field3.setFeedback({ messages: null, code: 'EffectError' }) field3.setFeedback({ messages: [], code: 'EffectError' }) field4.setDisplay('none') await field4.validate() expect(field4.selfErrors).toEqual([]) }) test('setValidateRule', () => { const form = attach(createForm()) const field1 = attach( form.createField({ name: 'aa', validator: [{ required: true }], }) ) const field2 = attach( form.createField({ name: 'bb', validator: 'phone', }) ) const field3 = attach( form.createField({ name: 'cc', validator: 'phone', }) ) const field4 = attach( form.createField({ name: 'dd', validator: { format: 'phone' }, }) ) const field5 = attach( form.createField({ name: 'ee', validator: [{ format: 'phone' }], }) ) const field6 = attach( form.createField({ name: 'ff', }) ) field1.setValidatorRule('format', 'phone') field2.setValidatorRule('max', 3) field3.setValidatorRule('format', 'url') field4.setValidatorRule('min', 3) field5.setValidatorRule('min', 3) field6.setValidatorRule('min', 3) expect(field1.validator).toEqual([{ required: true }, { format: 'phone' }]) expect(field2.validator).toEqual([{ format: 'phone' }, { max: 3 }]) expect(field3.validator).toEqual([{ format: 'url' }]) expect(field4.validator).toEqual([{ format: 'phone' }, { min: 3 }]) expect(field5.validator).toEqual([{ format: 'phone' }, { min: 3 }]) expect(field6.validator).toEqual([{ min: 3 }]) }) test('query', () => { const form = attach(createForm()) const object_ = attach( form.createObjectField({ name: 'object', }) ) const void_ = attach( form.createVoidField({ name: 'void', basePath: 'object', }) ) const aaa = attach( form.createField({ name: 'aaa', basePath: 'object.void', }) ) const bbb = attach( form.createField({ name: 'bbb', basePath: 'object', }) ) expect(object_.query('object.void').take()).not.toBeUndefined() expect(object_.query('object.void.aaa').take()).not.toBeUndefined() expect(void_.query('.')).not.toBeUndefined() expect(void_.query('.bbb').take()).not.toBeUndefined() expect(aaa.query('.ccc').take()).toBeUndefined() expect(aaa.query('..').take()).not.toBeUndefined() expect(aaa.query('..bbb').take()).not.toBeUndefined() expect(bbb.query('.void').take()).not.toBeUndefined() expect(bbb.query('.void.aaa').take()).not.toBeUndefined() expect(bbb.query('.void.ccc').take()).toBeUndefined() }) test('empty initialValue', () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', initialValue: '', }) ) const bb = attach( form.createField({ name: 'bb', }) ) expect(aa.value).toEqual('') expect(form.values.aa).toEqual('') expect(bb.value).toEqual(undefined) expect(form.values.bb).toEqual(undefined) }) test('objectFieldWithInitialValue', async () => { const form = attach( createForm({ initialValues: { obj: { a: 'a', }, }, }) ) attach( form.createObjectField({ name: 'obj', }) ) const fieldObjA = attach( form.createField({ name: 'obj.a', }) ) expect(fieldObjA.initialValue).toEqual('a') fieldObjA.value = 'aa' expect(fieldObjA.value).toEqual('aa') expect(fieldObjA.initialValue).toEqual('a') }) test('initialValueWithArray', () => { const form = attach(createForm()) const field = attach( form.createArrayField({ name: 'aaa', initialValue: [1, 2], }) ) expect(field.initialValue).toEqual([1, 2]) expect(field.value).toEqual([1, 2]) expect(form.initialValues.aaa).toEqual([1, 2]) expect(form.values.aaa).toEqual([1, 2]) }) test('resetObjectFieldWithInitialValue', async () => { const form = attach(createForm()) attach( form.createObjectField({ name: 'obj', }) ) const fieldObjA = attach( form.createField({ name: 'obj.a', initialValue: 'a', }) ) fieldObjA.value = 'aa' expect(fieldObjA.value).toEqual('aa') await form.reset() expect(fieldObjA.value).toEqual('a') fieldObjA.value = 'aa' expect(fieldObjA.value).toEqual('aa') await form.reset() expect(fieldObjA.initialValue).toEqual('a') expect(fieldObjA.value).toEqual('a') }) test('reset', async () => { const form = attach( createForm({ values: { bb: 123, }, initialValues: { aa: 123, cc: null, }, }) ) const aa = attach( form.createField({ name: 'aa', required: true, }) ) const bb = attach( form.createField({ name: 'bb', required: true, }) ) const cc = attach( form.createField({ name: 'cc', required: true, }) ) const dd = attach( form.createField({ name: 'dd', required: true, }) ) expect(aa.value).toEqual(123) expect(bb.value).toEqual(123) expect(cc.value).toEqual(null) expect(form.values.aa).toEqual(123) expect(form.values.bb).toEqual(123) expect(form.values.cc).toEqual(null) aa.onInput('xxxxx') expect(form.values.aa).toEqual('xxxxx') dd.onInput(null) expect(form.values.dd).toEqual(null) aa.reset() expect(aa.value).toEqual(123) expect(form.values.aa).toEqual(123) bb.onInput('xxxxx') expect(form.values.bb).toEqual('xxxxx') bb.reset() expect(bb.value).toBeUndefined() expect(form.values.bb).toBeUndefined() cc.onInput('xxxxx') expect(form.values.cc).toEqual('xxxxx') cc.reset() expect(cc.value).toBeNull() expect(form.values.cc).toBeNull() dd.reset() expect(dd.value).toBeUndefined() expect(form.values.dd).toBeUndefined() aa.reset({ forceClear: true, }) expect(aa.value).toBeUndefined() expect(form.values.aa).toBeUndefined() cc.reset({ forceClear: true, }) expect(cc.value).toBeUndefined() expect(form.values.cc).toBeUndefined() expect(aa.valid).toBeTruthy() await aa.reset({ forceClear: true, validate: true, }) expect(aa.valid).toBeFalsy() expect(cc.valid).toBeTruthy() await cc.reset({ forceClear: true, validate: true, }) expect(cc.valid).toBeFalsy() }) test('match', () => { const form = attach( createForm({ values: { bb: 123, }, initialValues: { aa: 123, }, }) ) const aa = attach( form.createField({ name: 'aa', required: true, }) ) expect(aa.match('aa')).toBeTruthy() expect(aa.match('*')).toBeTruthy() expect(aa.match('a~')).toBeTruthy() expect(aa.match('*(aa,bb)')).toBeTruthy() }) test('setState/getState', () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', required: true, }) ) const state = aa.getState() aa.setState((state) => { state.value = '123' state.title = 'AAA' }) expect(aa.value).toEqual('123') expect(aa.title).toEqual('AAA') state['setState'] = () => {} aa.setState(state) expect(aa.value).toBeUndefined() expect(aa.title).toBeUndefined() aa.setState((state) => { state.hidden = false }) expect(aa.display).toEqual('visible') aa.setState((state) => { state.visible = true }) expect(aa.display).toEqual('visible') aa.setState((state) => { state.readOnly = false }) expect(aa.pattern).toEqual('editable') aa.setState((state) => { state.disabled = false }) expect(aa.pattern).toEqual('editable') aa.setState((state) => { state.editable = true }) expect(aa.pattern).toEqual('editable') aa.setState((state) => { state.editable = false }) expect(aa.pattern).toEqual('readPretty') aa.setState((state) => { state.readPretty = true }) expect(aa.pattern).toEqual('readPretty') aa.setState((state) => { state.readPretty = false }) expect(aa.pattern).toEqual('editable') form.setFieldState('bb', (state) => { state.value = 'bbb' }) form.setFieldState('bb', (state) => { state.visible = false }) const bb = attach( form.createField({ name: 'bb', }) ) expect(bb.value).toEqual(undefined) expect(bb.visible).toBeFalsy() form.setFieldState('*', (state) => { state.value = '123' }) const cc = attach( form.createField({ name: 'cc', }) ) expect(aa.value).toEqual('123') expect(bb.value).toBeUndefined() expect(cc.value).toEqual('123') form.setFieldState(form.query('cc'), (state) => { state.value = 'ccc' }) expect(cc.value).toEqual('ccc') form.setFieldState(cc, (state) => { state.value = '123' }) expect(cc.value).toEqual('123') expect(form.getFieldState(aa)).not.toBeUndefined() expect(form.getFieldState(form.query('aa'))).not.toBeUndefined() }) test('setDataSource', () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', required: true, }) ) aa.setDataSource([ { label: 's1', value: 's1' }, { label: 's2', value: 's2' }, ]) expect(aa.dataSource).toEqual([ { label: 's1', value: 's1' }, { label: 's2', value: 's2' }, ]) }) test('setTitle/setDescription', () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', required: true, }) ) aa.setTitle('AAA') aa.setDescription('This is AAA') expect(aa.title).toEqual('AAA') expect(aa.description).toEqual('This is AAA') }) test('required/setRequired', () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', }) ) aa.setRequired(true) expect(aa.required).toBeTruthy() aa.setRequired(false) expect(aa.required).toBeFalsy() const bb = attach( form.createField({ name: 'bb', validator: { max: 3, required: true, }, }) ) expect(bb.required).toBeTruthy() bb.setRequired(false) expect(bb.required).toBeFalsy() const cc = attach( form.createField({ name: 'cc', validator: [ 'date', { max: 3, }, { required: true, }, ], }) ) expect(cc.required).toBeTruthy() cc.setRequired(false) expect(cc.required).toBeFalsy() const dd = attach( form.createField({ name: 'dd', validator: { max: 3, }, }) ) expect(dd.required).toBeFalsy() dd.setRequired(true) expect(dd.required).toBeTruthy() }) test('setData/setContent', () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', required: true, }) ) aa.setData('This is data') aa.setContent('This is Content') expect(aa.data).toEqual('This is data') expect(aa.content).toEqual('This is Content') }) test('setData/setContent in void field', () => { const form = attach(createForm()) const voidFeild = attach( form.createVoidField({ name: 'voidFeild', }) ) voidFeild.setData('This is data') voidFeild.setContent('This is Content') expect(voidFeild.data).toEqual('This is data') expect(voidFeild.content).toEqual('This is Content') }) test('setErrors/setWarnings/setSuccesses/setValidator', async () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', }) ) const bb = attach( form.createField({ name: 'bb', }) ) const cc = attach( form.createField({ name: 'cc', }) ) const dd = attach( form.createField({ name: 'dd', validator() { return new Promise(() => {}) }, }) ) aa.setSelfErrors(['error']) aa.setSelfWarnings(['warning']) aa.setSelfSuccesses(['success']) bb.setSelfSuccesses(['success']) cc.setSelfWarnings(['warning']) expect(aa.selfErrors).toEqual(['error']) expect(aa.valid).toBeFalsy() expect(aa.selfWarnings).toEqual(['warning']) expect(aa.selfSuccesses).toEqual(['success']) expect(bb.validateStatus).toEqual('success') expect(cc.validateStatus).toEqual('warning') aa.setValidator('date') await aa.onInput('123') expect(aa.selfErrors.length).toEqual(2) dd.onInput('123') await sleep() expect(dd.validateStatus).toEqual('validating') }) test('reactions', async () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', }) ) const bb = attach( form.createField({ name: 'bb', reactions: [ (field) => { const aa = field.query('aa') if (aa.get('value') === '123') { field.visible = false } else { field.visible = true } if (aa.get('inputValue') === '333') { field.editable = false } else if (aa.get('inputValue') === '444') { field.editable = true } if (aa.get('initialValue') === '555') { field.readOnly = true } else if (aa.get('initialValue') === '666') { field.readOnly = false } }, null, ], }) ) expect(bb.visible).toBeTruthy() aa.setValue('123') expect(bb.visible).toBeFalsy() await aa.onInput('333') expect(bb.editable).toBeFalsy() await aa.onInput('444') expect(bb.editable).toBeTruthy() aa.setInitialValue('555') expect(bb.readOnly).toBeTruthy() aa.setInitialValue('666') expect(bb.readOnly).toBeFalsy() form.onUnmount() }) test('fault tolerance', () => { const form = attach(createForm()) const field = attach( form.createField({ name: 'aa', value: 123, }) ) field.setDisplay('none') expect(field.value).toBeUndefined() field.setDisplay('visible') expect(field.value).toEqual(123) field.setDisplay('none') expect(field.value).toBeUndefined() field.setValue(321) expect(field.value).toBeUndefined() field.setDisplay('visible') expect(field.value).toEqual(321) form.setDisplay(null) form.setPattern(null) const field2 = attach( form.createField({ name: 'xxx', }) ) expect(field2.display).toEqual('visible') expect(field2.pattern).toEqual('editable') }) test('initialValue', () => { const form = attach(createForm()) const field = attach( form.createField({ name: 'aaa', initialValue: 123, }) ) expect(form.values.aaa).toEqual(123) expect(form.initialValues.aaa).toEqual(123) expect(field.value).toEqual(123) expect(field.initialValue).toEqual(123) }) test('array path calculation with none index', async () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) await array.push({}) const input = attach( form.createField({ name: '0.input', basePath: 'array', }) ) expect(input.path.toString()).toEqual('array.0.input') }) test('array path calculation with none index and void nested', async () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) await array.push({}) attach( form.createVoidField({ name: '0.column', basePath: 'array', }) ) const input = attach( form.createField({ name: 'input', basePath: 'array.0.column', }) ) expect(input.path.toString()).toEqual('array.0.input') }) test('array path calculation with object index', async () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) await array.push({}) attach( form.createObjectField({ name: '0', basePath: 'array', }) ) const input = attach( form.createField({ name: 'input', basePath: 'array.0', }) ) expect(input.path.toString()).toEqual('array.0.input') }) test('array path calculation with void index', async () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) await array.push('') attach( form.createVoidField({ name: '0', basePath: 'array', }) ) const input = attach( form.createField({ name: 'input', basePath: 'array.0', }) ) expect(input.path.toString()).toEqual('array.0') }) test('array path calculation with void index and void wrapper', async () => { const form = attach(createForm()) attach( form.createVoidField({ name: 'layout', }) ) const array_in_layout = attach( form.createArrayField({ name: 'array_in_layout', basePath: 'layout', }) ) await array_in_layout.push('') attach( form.createVoidField({ name: '0', basePath: 'layout.array_in_layout', }) ) const input = attach( form.createField({ name: 'input', basePath: 'layout.array_in_layout.0', }) ) expect(input.path.toString()).toEqual('array_in_layout.0') }) test('reaction in reaction', () => { const form = attach(createForm()) const void_ = attach( form.createVoidField({ name: 'void', }) ) attach( form.createField({ name: 'field1', basePath: 'void', initialValue: 123, }) ) const field2 = attach( form.createField({ name: 'field2', basePath: 'void', initialValue: 456, reactions: (field) => { const f1 = field.query('field1') if (f1.get('value') === 123) { field.display = 'visible' } else { field.display = 'none' } }, }) ) void_.setDisplay('none') expect(field2.value).toEqual(undefined) expect(field2.display).toEqual('none') }) test('nested fields hidden and selfValidate', async () => { const form = attach(createForm()) const parent = attach( form.createVoidField({ name: 'parent', }) ) attach( form.createField({ name: 'aa', basePath: 'parent', required: true, }) ) attach( form.createField({ name: 'bb', basePath: 'parent', required: true, }) ) try { await form.validate() } catch {} expect(form.invalid).toBeTruthy() parent.display = 'hidden' await form.validate() expect(form.invalid).toBeFalsy() }) test('deep nested fields hidden and selfValidate', async () => { const form = attach(createForm()) const parent1 = attach( form.createVoidField({ name: 'parent1', }) ) const parent2 = attach( form.createVoidField({ name: 'parent2', basePath: 'parent1', }) ) const aa = attach( form.createField({ name: 'aa', basePath: 'parent1.parent2', required: true, }) ) const bb = attach( form.createField({ name: 'bb', basePath: 'parent1.parent2', required: true, }) ) try { await form.validate() } catch {} expect(form.invalid).toBeTruthy() parent2.display = 'visible' parent1.display = 'hidden' expect(parent2.display).toEqual('hidden') expect(aa.display).toEqual('hidden') expect(bb.display).toEqual('hidden') await form.validate() expect(form.invalid).toBeFalsy() }) test('deep nested fields hidden and selfValidate with middle hidden', async () => { const form = attach(createForm()) const parent1 = attach( form.createVoidField({ name: 'parent1', }) ) const parent2 = attach( form.createVoidField({ name: 'parent2', basePath: 'parent1', }) ) const aa = attach( form.createField({ name: 'aa', basePath: 'parent1.parent2', required: true, }) ) const bb = attach( form.createField({ name: 'bb', basePath: 'parent1.parent2', required: true, }) ) try { await form.validate() } catch {} expect(form.invalid).toBeTruthy() parent2.display = 'hidden' parent1.display = 'none' expect(parent2.display).toEqual('hidden') expect(aa.display).toEqual('hidden') expect(bb.display).toEqual('hidden') await form.validate() expect(form.invalid).toBeFalsy() }) test('fields unmount and selfValidate', async () => { const form = attach(createForm()) const field = attach( form.createField({ name: 'parent', required: true, }) ) try { await form.validate() } catch {} expect(form.invalid).toBeTruthy() field.onUnmount() try { await form.validate() } catch {} expect(form.invalid).toBeTruthy() form.clearFormGraph('parent') await form.validate() expect(form.invalid).toBeFalsy() }) test('auto clean with ArrayField', () => { const form = attach(createForm()) attach( form.createArrayField({ name: 'array', initialValue: [{}, {}], }) ) attach( form.createField({ name: '0.aa', basePath: 'array', }) ) attach( form.createField({ name: '1.aa', basePath: 'array', }) ) const array1 = attach( form.createArrayField({ name: 'array1', initialValue: [{}, {}], }) ) attach( form.createField({ name: '0.aa', basePath: 'array1', }) ) attach( form.createField({ name: '1.aa', basePath: 'array1', }) ) const array2 = attach( form.createArrayField({ name: 'array2', initialValue: [{}, {}], }) ) attach( form.createField({ name: '0.aa', basePath: 'array2', }) ) attach( form.createField({ name: '1.aa', basePath: 'array2', }) ) expect(form.fields['array.1.aa']).not.toBeUndefined() expect(form.values.array).toEqual([{}, {}]) form.setValues( { array: [{}], }, 'shallowMerge' ) expect(form.values.array).toEqual([{}]) expect(form.fields['array.1.aa']).toBeUndefined() expect(form.fields['array1.0.aa']).not.toBeUndefined() expect(form.fields['array1.1.aa']).not.toBeUndefined() expect(form.values.array1).toEqual([{}, {}]) array1.setValue([]) expect(form.fields['array1.0.aa']).toBeUndefined() expect(form.fields['array1.1.aa']).toBeUndefined() expect(form.fields['array2.0.aa']).not.toBeUndefined() expect(form.fields['array2.1.aa']).not.toBeUndefined() array2.setValue([]) expect(form.fields['array2.0.aa']).toBeUndefined() expect(form.fields['array2.1.aa']).toBeUndefined() }) test('auto clean with ObjectField', () => { const form = attach(createForm()) attach( form.createObjectField({ name: 'obj', initialValue: { aa: 'aa', bb: 'bb', }, }) ) attach( form.createField({ name: 'aa', basePath: 'obj', }) ) attach( form.createField({ name: 'bb', basePath: 'obj', }) ) const obj1 = attach( form.createObjectField({ name: 'obj1', initialValue: { aa: 'aa', bb: 'bb', }, }) ) attach( form.createField({ name: 'aa', basePath: 'obj1', }) ) attach( form.createField({ name: 'bb', basePath: 'obj1', }) ) const obj2 = attach( form.createObjectField({ name: 'obj2', initialValue: { aa: 'aa', bb: 'bb', }, }) ) attach( form.createField({ name: 'aa', basePath: 'obj2', }) ) attach( form.createField({ name: 'bb', basePath: 'obj2', }) ) expect(form.fields['obj.aa']).not.toBeUndefined() expect(form.fields['obj.bb']).not.toBeUndefined() expect(form.values.obj).toEqual({ aa: 'aa', bb: 'bb' }) form.setValues( { obj: { aa: '123', }, }, 'shallowMerge' ) expect(form.values.obj).toEqual({ aa: '123' }) expect(form.fields['obj.aa']).not.toBeUndefined() expect(form.fields['obj.bb']).not.toBeUndefined() expect(form.fields['obj1.aa']).not.toBeUndefined() expect(form.fields['obj1.bb']).not.toBeUndefined() expect(form.values.obj1).toEqual({ aa: 'aa', bb: 'bb' }) obj1.setValue({}) expect(form.values.obj1).toEqual({}) expect(form.fields['obj1.aa']).not.toBeUndefined() expect(form.fields['obj1.bb']).not.toBeUndefined() expect(form.fields['obj2.aa']).not.toBeUndefined() expect(form.fields['obj2.bb']).not.toBeUndefined() expect(form.values.obj2).toEqual({ aa: 'aa', bb: 'bb' }) obj2.setValue({ aa: 'aa', bb: 'bb', cc: 'cc' }) expect(form.fields['obj2.aa']).not.toBeUndefined() expect(form.fields['obj2.bb']).not.toBeUndefined() expect(form.fields['obj2.cc']).toBeUndefined() obj2.addProperty('cc', '123') attach( form.createField({ name: 'cc', basePath: 'obj2', }) ) expect(form.fields['obj2.cc']).not.toBeUndefined() obj2.removeProperty('cc') expect(form.fields['obj2.cc']).toBeUndefined() }) test('initial value with empty', () => { const form = attach(createForm()) const array = attach(form.createField({ name: 'array', initialValue: '' })) expect(array.value).toEqual('') const beNull = attach(form.createField({ name: 'null', initialValue: null })) expect(beNull.value).toEqual(null) }) test('field submit', async () => { const form = attach( createForm({ initialValues: { aa: { cc: 'cc', }, bb: 'bb', }, }) ) const childForm = attach( form.createObjectField({ name: 'aa', }) ) attach( form.createField({ name: 'bb', }) ) attach( form.createField({ name: 'cc', basePath: 'aa', }) ) const onSubmit = jest.fn() await childForm.submit(onSubmit) expect(onSubmit).toBeCalledWith({ cc: 'cc', }) }) test('field submit with error', async () => { const form = attach(createForm()) const childForm = attach( form.createObjectField({ name: 'aa', }) ) attach( form.createField({ name: 'bb', required: true, }) ) attach( form.createField({ name: 'cc', basePath: 'aa', required: true, }) ) const onSubmit = jest.fn() try { await childForm.submit(onSubmit) } catch (e) { expect(e).not.toBeUndefined() } expect(onSubmit).toBeCalledTimes(0) }) test('initial display with value', () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', value: 123, visible: false, }) ) const bb = attach( form.createField({ name: 'bb', value: 123, visible: true, }) ) const cc = attach( form.createField({ name: 'cc', value: 123, hidden: true, }) ) expect(aa.value).toBeUndefined() expect(aa.visible).toBeFalsy() expect(bb.value).toEqual(123) expect(bb.visible).toBeTruthy() expect(cc.value).toEqual(123) expect(cc.hidden).toBeTruthy() }) test('state depend field visible value', async () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', }) ) const bb = attach( form.createField({ name: 'bb', reactions(field) { field.visible = aa.value === '123' }, }) ) const cc = attach( form.createField({ name: 'cc', reactions(field) { field.visible = aa.value === '123' field.disabled = !bb.value }, }) ) expect(bb.visible).toBeFalsy() expect(cc.visible).toBeFalsy() expect(cc.disabled).toBeTruthy() aa.value = '123' await sleep(10) expect(bb.visible).toBeTruthy() expect(cc.visible).toBeTruthy() expect(cc.disabled).toBeTruthy() bb.value = '321' await sleep(10) expect(bb.visible).toBeTruthy() expect(cc.visible).toBeTruthy() expect(cc.disabled).toBeFalsy() aa.value = '' await sleep(10) expect(bb.visible).toBeFalsy() expect(cc.visible).toBeFalsy() expect(cc.disabled).toBeTruthy() aa.value = '123' await sleep(10) expect(bb.visible).toBeTruthy() expect(cc.visible).toBeTruthy() expect(cc.disabled).toBeFalsy() }) test('reactions initialValue and value', () => { const form = attach( createForm({ values: { aa: { input: '111', }, }, }) ) attach( form.createObjectField({ name: 'aa', reactions: [ (field) => { field.initialValue = {} field.initialValue.input = 123 }, ], }) ) attach( form.createField({ name: 'input', basePath: 'aa', }) ) expect(form.values.aa.input).toEqual('111') }) test('field name is length in initialize', () => { const form = attach(createForm()) const field = attach( form.createField({ name: 'length', initialValue: 123, }) ) expect(field.value).toEqual(123) }) test('field name is length in dynamic assign', () => { const form = attach(createForm()) const field = attach( form.createField({ name: 'length', }) ) field.initialValue = 123 expect(field.value).toEqual(123) }) test('nested field modified', async () => { const form = attach(createForm()) const obj = attach( form.createObjectField({ name: 'object', }) ) const child = attach( form.createField({ name: 'child', basePath: 'object', }) ) await child.onInput() expect(child.modified).toBeTruthy() expect(child.selfModified).toBeTruthy() expect(obj.modified).toBeTruthy() expect(obj.selfModified).toBeFalsy() expect(form.modified).toBeTruthy() await obj.reset() expect(child.modified).toBeFalsy() expect(child.selfModified).toBeFalsy() expect(obj.modified).toBeFalsy() expect(obj.selfModified).toBeFalsy() expect(form.modified).toBeTruthy() await form.reset() expect(form.modified).toBeFalsy() }) test('field setValidator repeat call', async () => { const form = attach(createForm()) const field = attach( form.createField({ name: 'normal', }) ) const validator1 = jest.fn(() => '') const validator2 = jest.fn(() => '') const validator3 = jest.fn(() => '') field.setValidator([validator1, validator2, validator3]) await form.validate() expect(validator1).toBeCalledTimes(1) }) test('custom validator to get ctx.field', async () => { const form = attach(createForm()) let ctxField = null let ctxForm = null attach( form.createField({ name: 'aaa', validator(value, rule, ctx) { ctxField = ctx.field ctxForm = ctx.form return '' }, }) ) await form.submit() expect(!!ctxField).toBeTruthy() expect(!!ctxForm).toBeTruthy() }) test('single direction linkage effect', async () => { const form = attach(createForm()) const input1 = form.createField({ name: 'input1', reactions: (field: DataField) => { if (!field.selfModified) { return } input2.value = field.value }, }) const input2 = form.createField({ name: 'input2', }) await input1.onInput('123') expect(input2.value).toBe('123') await input2.onInput('321') expect(input2.value).toBe('321') }) test('path change will update computed value', () => { const form = attach(createForm()) const input = form.createField({ name: 'input', }) const value = jest.fn() autorun(() => { value(input.value) }) batch(() => { input.locate('select') input.value = '123' }) expect(value).nthCalledWith(2, '123') }) test('object field reset', async () => { const form = attach(createForm()) attach( form.createObjectField({ name: 'obj', }) ) const input = attach( form.createField({ name: 'input', basePath: 'obj', }) ) await form.reset() form.setValues({ obj: { input: '123', }, }) expect(input.value).toBe('123') }) test('field visible default value should work', () => { const form = attach( createForm({ effects(form) { onFieldReact('obj.input1', (field) => { field.pattern = 'disabled' }) onFieldReact('obj', (field) => { field.visible = form.values.select !== 'none' }) onFieldReact('obj.input1', (field) => { if (isField(field)) { field.initialValue = '123' } }) onFieldReact('obj.input2', (field) => { if (isField(field)) { field.value = form.values.select } }) }, }) ) const select = attach( form.createField({ name: 'select', }) ) attach( form.createObjectField({ name: 'obj', }) ) attach( form.createField({ name: 'input1', basePath: 'obj', }) ) attach( form.createField({ name: 'input2', basePath: 'obj', }) ) select.value = 'none' expect(form.values.obj?.input1).toBeUndefined() select.value = 'visible' expect(form.values.obj.input1).toBe('123') }) test('query value with sibling path syntax', () => { const form = attach(createForm()) const fn = jest.fn() attach( form.createVoidField({ name: 'void', }) ) attach( form.createObjectField({ name: 'obj', basePath: 'void', }) ) attach( form.createField({ name: 'input', basePath: 'void.obj', reactions: [ (field) => { fn( field.query('.textarea').value(), field.query('.textarea').initialValue() ) }, ], }) ) const textarea = attach( form.createField({ name: 'textarea', basePath: 'void.obj', initialValue: 'aaa', }) ) textarea.value = '123' expect(fn).toBeCalledWith('123', 'aaa') }) test('relative query with void field', () => { const form = attach(createForm()) attach( form.createVoidField({ name: 'void', }) ) const aa = attach( form.createField({ name: 'aa', basePath: 'void', }) ) attach( form.createVoidField({ name: 'mm', }) ) const bb = attach( form.createField({ name: 'bb', basePath: 'mm', }) ) expect(bb.query('.aa').take()).toBe(aa) }) test('empty string or number or null value need rewrite default value', () => { const form = attach( createForm({ values: { aa: '', bb: 0, ee: null, }, }) ) attach( form.createField({ name: 'aa', initialValue: 'test', }) ) attach( form.createField({ name: 'bb', initialValue: 123, }) ) attach( form.createField({ name: 'cc', initialValue: 'test', }) ) attach( form.createField({ name: 'dd', initialValue: 123, }) ) attach( form.createField({ name: 'ee', initialValue: 'test', }) ) expect(form.values.aa).toEqual('') expect(form.values.bb).toEqual(0) expect(form.values.cc).toEqual('test') expect(form.values.dd).toEqual(123) expect(form.values.ee).toEqual(null) }) test('destroy field need auto remove initialValues', () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', initialValue: 'test', }) ) expect(form.initialValues.aa).toEqual('test') expect(form.values.aa).toEqual('test') aa.destroy() expect(form.initialValues.aa).toBeUndefined() expect(form.values.aa).toBeUndefined() }) test('validateFirst', async () => { const form = attach( createForm({ validateFirst: false, }) ) const aaValidate = jest.fn(() => 'aaError') const aa = attach( form.createField({ name: 'aa', validateFirst: true, validator: [aaValidate, aaValidate], }) ) await aa.onInput('aa') const bbValidate = jest.fn(() => 'bbError') const bb = attach( form.createField({ name: 'bb', validator: [bbValidate, bbValidate], validateFirst: false, }) ) await bb.onInput('bb') const ccValidate = jest.fn(() => 'ccError') const cc = attach( form.createField({ name: 'cc', validator: [ccValidate, ccValidate], }) ) await cc.onInput('cc') expect(aaValidate).toBeCalledTimes(1) expect(bbValidate).toBeCalledTimes(2) expect(ccValidate).toBeCalledTimes(2) }) test('reactions should not be triggered when field destroyed', () => { const form = attach(createForm()) const handler = jest.fn() const obs = observable({ bb: 123 }) const aa = attach( form.createField({ name: 'aa', initialValue: 'test', reactions() { handler(obs.bb) }, }) ) obs.bb = 321 aa.destroy() obs.bb = 111 expect(handler).toBeCalledTimes(2) }) test('parent readPretty will overwrite self disabled or readOnly', () => { const form = attach( createForm({ readPretty: true, }) ) const aa = attach( form.createField({ name: 'aa', initialValue: 'test', disabled: true, }) ) const bb = attach( form.createField({ name: 'bb', initialValue: 'test', editable: true, }) ) expect(aa.pattern).toBe('readPretty') expect(bb.pattern).toBe('editable') }) test('conflict name for errors filter', async () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', required: true, }) ) const aa1 = attach( form.createField({ name: 'aa1', required: true, }) ) await aa1.onInput('') expect(aa.invalid).toBe(false) }) test('field destroyed can not be assign value', () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', }) ) aa.destroy() aa.initialValue = 222 aa.value = 111 expect(form.values).toEqual({}) expect(form.initialValues).toEqual({}) }) test('onInput could pass value with target', async () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', }) ) await aa.onInput({ target: '123', }) expect(aa.value).toEqual({ target: '123' }) }) test('field destroyed or display none should not be assign value from patch initialValues', () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', display: 'none', }) ) aa.initialValue = '123' expect(form.values).toEqual({}) aa.display = 'visible' expect(aa.value).toBe('123') expect(form.values).toEqual({ aa: '123' }) }) test('onFieldReact with field destroyed', () => { const fn = jest.fn() const obs = observable({ value: 123 }) const form = attach( createForm({ effects() { onFieldReact('aa', () => { fn(obs.value) }) }, }) ) const aa = attach( form.createField({ name: 'aa', }) ) obs.value = '321' expect(fn).toBeCalledTimes(2) aa.destroy() obs.value = '111' expect(fn).toBeCalledTimes(2) }) test('field actions', () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', }) ) expect(aa.actions).toEqual({}) aa.inject({ test: () => 123, }) expect(aa.invoke('test')).toEqual(123) aa.inject({ test: () => 321, }) expect(aa.invoke('test')).toEqual(321) }) test('field hidden value', () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', hidden: true, initialValue: '123', }) ) expect(form.values).toEqual({ aa: '123' }) const objectField = attach( form.createObjectField({ name: 'object', hidden: true, }) ) const arrayField = attach( form.createArrayField({ name: 'array', hidden: true, }) ) aa.setDisplay('none') objectField.setDisplay('none') arrayField.setDisplay('none') expect(aa.value).toBeUndefined() expect(objectField.value).toBeUndefined() expect(arrayField.value).toBeUndefined() aa.setDisplay('hidden') objectField.setDisplay('hidden') arrayField.setDisplay('hidden') expect(aa.value).toEqual('123') expect(objectField.value).toEqual({}) expect(arrayField.value).toEqual([]) }) test('field destructor path with display none', () => { const form = attach(createForm()) const aa = attach( form.createArrayField({ name: '[aa,bb]', }) ) aa.setDisplay('none') expect(form.values).toEqual({}) expect(aa.value).toEqual([]) }) test('onInput should ignore HTMLInputEvent propagation', async () => { const form = attach(createForm()) const mockHTMLInput = { value: '321' } const mockDomEvent = { target: mockHTMLInput, currentTarget: mockHTMLInput } const aa = attach( form.createField({ name: 'aa', }) ) await aa.onInput(mockDomEvent) expect(aa.value).toEqual('321') await aa.onInput({ target: { value: '2' }, currentTarget: { value: '4' } }) expect(aa.value).toEqual('321') // currentTarget is undefined, skip ignore await aa.onInput({ target: { value: '123' } }) expect(aa.value).toEqual('123') }) test('onFocus and onBlur with invalid target value', async () => { const form = attach(createForm()) const field = attach( form.createField({ name: 'aa', validateFirst: true, value: '111', validator: [ { triggerType: 'onFocus', format: 'date', }, { triggerType: 'onBlur', format: 'url', }, ], }) ) await field.onFocus({ target: {} }) expect(field.selfErrors).toEqual([]) await field.onBlur({ target: {} }) expect(field.selfErrors).toEqual([]) await field.onFocus() expect(field.selfErrors).toEqual([ 'The field value is not a valid date format', ]) await field.onBlur() expect(field.selfErrors).toEqual([ 'The field value is not a valid date format', 'The field value is a invalid url', ]) }) test('validatePattern and validateDisplay', async () => { const form = attach( createForm({ validatePattern: ['editable'], validateDisplay: ['visible'], }) ) const field1 = attach( form.createField({ name: 'a', required: true, }) ) const field2 = attach( form.createField({ name: 'b', required: true, validatePattern: ['readOnly'], validateDisplay: ['hidden'], }) ) const field3 = attach( form.createField({ name: 'c', required: true, validatePattern: ['readOnly', 'editable'], validateDisplay: ['hidden', 'visible'], }) ) try { await form.validate() } catch {} expect(field1.selfErrors.length).toBe(1) expect(field2.selfErrors.length).toBe(0) expect(field3.selfErrors.length).toBe(1) form.setPattern('readOnly') form.setDisplay('hidden') try { await form.validate() } catch {} expect(field1.selfErrors.length).toBe(0) expect(field2.selfErrors.length).toBe(1) expect(field3.selfErrors.length).toBe(1) }) ================================================ FILE: packages/core/src/__tests__/form.spec.ts ================================================ import { createForm } from '../' import { onFieldValidateStart, onFieldValueChange, onFormInitialValuesChange, onFormValuesChange, } from '../effects' import { attach, sleep } from './shared' import { LifeCycleTypes } from '../types' import { observable, batch } from '@formily/reactive' test('create form', () => { const form = attach(createForm()) expect(form).not.toBeUndefined() }) test('createField/createArrayField/createObjectField/createVoidField', () => { const form = attach(createForm()) const normal = attach( form.createField({ name: 'normal', basePath: 'parent', }) ) const normal2 = attach( form.createField({ name: 'normal', basePath: 'parent', }) ) const array_ = attach( form.createArrayField({ name: 'array', basePath: 'parent' }) ) const array2_ = attach( form.createArrayField({ name: 'array', basePath: 'parent' }) ) const object_ = attach( form.createObjectField({ name: 'object', basePath: 'parent' }) ) const object2_ = attach( form.createObjectField({ name: 'object', basePath: 'parent' }) ) const void_ = attach( form.createVoidField({ name: 'void', basePath: 'parent' }) ) const void2_ = attach( form.createVoidField({ name: 'void', basePath: 'parent' }) ) const children_ = attach( form.createField({ name: 'children', basePath: 'parent.void' }) ) expect(normal).not.toBeUndefined() expect(array_).not.toBeUndefined() expect(object_).not.toBeUndefined() expect(void_).not.toBeUndefined() expect(normal.address.toString()).toEqual('parent.normal') expect(normal.path.toString()).toEqual('parent.normal') expect(array_.address.toString()).toEqual('parent.array') expect(array_.path.toString()).toEqual('parent.array') expect(object_.address.toString()).toEqual('parent.object') expect(object_.path.toString()).toEqual('parent.object') expect(void_.address.toString()).toEqual('parent.void') expect(void_.path.toString()).toEqual('parent.void') expect(children_.address.toString()).toEqual('parent.void.children') expect(children_.path.toString()).toEqual('parent.children') expect(form.createField({ name: '' })).toBeUndefined() expect(form.createArrayField({ name: '' })).toBeUndefined() expect(form.createObjectField({ name: '' })).toBeUndefined() expect(form.createVoidField({ name: '' })).toBeUndefined() expect(array_ === array2_).toBeTruthy() expect(object_ === object2_).toBeTruthy() expect(void_ === void2_).toBeTruthy() expect(normal === normal2).toBeTruthy() }) test('setValues/setInitialValues', () => { const form = attach(createForm()) form.setValues({ aa: 123, cc: { kk: 321, }, }) const field = attach( form.createField({ name: 'cc.mm', initialValue: 'ooo', }) ) const field2 = attach( form.createField({ name: 'cc.pp', initialValue: 'www', }) ) expect(form.values.aa).toEqual(123) expect(form.values.cc.kk).toEqual(321) expect(form.values.cc.mm).toEqual('ooo') expect(form.initialValues.cc.mm).toEqual('ooo') expect(form.values.cc.pp).toEqual('www') expect(form.initialValues.cc.pp).toEqual('www') expect(field.value).toEqual('ooo') expect(field2.value).toEqual('www') form.setInitialValues({ bb: '123', cc: { dd: 'xxx', pp: 'www2', }, }) expect(form.values.aa).toEqual(123) expect(form.values.bb).toEqual('123') expect(form.values.cc.kk).toEqual(321) expect(form.values.cc.dd).toEqual('xxx') expect(form.initialValues.bb).toEqual('123') expect(form.initialValues.cc.kk).toBeUndefined() expect(form.initialValues.cc.dd).toEqual('xxx') expect(form.values.cc.mm).toEqual('ooo') expect(form.initialValues.cc.mm).toEqual('ooo') expect(field.value).toEqual('ooo') expect(form.values.cc.pp).toEqual('www2') expect(form.initialValues.cc.pp).toEqual('www2') expect(field2.value).toEqual('www2') form.setInitialValues({}, 'overwrite') expect(form.initialValues?.cc?.pp).toBeUndefined() form.setValues({}, 'overwrite') expect(form.values.aa).toBeUndefined() form.setInitialValues({ aa: { bb: [{ cc: 123 }] } }, 'deepMerge') expect(form.values).toEqual({ aa: { bb: [{ cc: 123 }] } }) form.setValues({ bb: { bb: [{ cc: 123 }] } }, 'deepMerge') expect(form.values).toEqual({ aa: { bb: [{ cc: 123 }] }, bb: { bb: [{ cc: 123 }] }, }) form.setInitialValues({ aa: [123] }, 'shallowMerge') expect(form.values).toEqual({ aa: [123], bb: { bb: [{ cc: 123 }] }, }) form.setValues({ bb: [123] }, 'shallowMerge') expect(form.values).toEqual({ aa: [123], bb: [123], }) }) test('no field initialValues merge', () => { const form = attach( createForm({ values: { aa: '123', }, initialValues: { aa: '333', bb: '321', }, }) ) expect(form.values).toEqual({ aa: '123', bb: '321', }) }) test('setLoading', async () => { const form = attach(createForm()) expect(form.loading).toBeFalsy() form.setLoading(true) await sleep(100) expect(form.loading).toBeTruthy() }) test('setValues with null', () => { const form = attach(createForm()) form.setInitialValues({ 'object-1': { 'array-1': null, }, 'object-2': { 'array-2': null, }, }) form.setValues({ 'object-1': { 'array-1': null, }, 'object-2': { 'array-2': null, }, }) expect(form.values).toEqual({ 'object-1': { 'array-1': null, }, 'object-2': { 'array-2': null, }, }) }) test('observable values/initialValues', () => { const values: any = observable({ aa: 123, bb: 321, }) const initialValues: any = observable({ cc: 321, dd: 444, }) const form = attach( createForm({ values, initialValues, }) ) batch(() => { values.kk = 321 }) expect(form.values.kk).toEqual(321) }) test('deleteValuesIn/deleteInitialValuesIn', () => { const form = attach( createForm<{ aa?: number bb?: number }>({ values: { aa: 123, }, initialValues: { bb: 123, }, }) ) expect(form.values.aa).toEqual(123) expect(form.values.bb).toEqual(123) form.deleteValuesIn('aa') form.deleteInitialValuesIn('bb') expect(form.existValuesIn('aa')).toBeFalsy() expect(form.existInitialValuesIn('bb')).toBeFalsy() }) test('setSubmitting/setValidating', async () => { const form = attach(createForm()) form.setSubmitting(true) expect(form.submitting).toBeFalsy() await sleep() expect(form.submitting).toBeTruthy() form.setSubmitting(false) expect(form.submitting).toBeFalsy() form.setValidating(true) expect(form.validating).toBeFalsy() await sleep() expect(form.validating).toBeTruthy() form.setValidating(false) expect(form.validating).toBeFalsy() }) test('setEffects/addEffects/removeEffects', () => { const form = attach(createForm()) const valueChange = jest.fn() const valueChange2 = jest.fn() form.addEffects('e1', () => { onFieldValueChange('aa', valueChange) }) const field = attach( form.createField({ name: 'aa', }) ) field.setValue('123') expect(valueChange).toBeCalledTimes(1) form.removeEffects('e1') field.setValue('321') expect(valueChange).toBeCalledTimes(1) form.addEffects('e2', () => { onFieldValueChange('aa', valueChange) }) field.setValue('444') expect(valueChange).toBeCalledTimes(2) form.setEffects(() => { onFieldValueChange('aa', valueChange2) }) field.setValue('555') expect(valueChange).toBeCalledTimes(3) expect(valueChange2).toBeCalledTimes(1) }) test('query', () => { const form = attach(createForm()) attach( form.createObjectField({ name: 'object', }) ) attach( form.createVoidField({ name: 'void', basePath: 'object', }) ) attach( form.createField({ name: 'normal', basePath: 'object.void', }) ) attach( form.createArrayField({ name: 'array', }) ) expect(form.query('object').take()).not.toBeUndefined() expect(form.query('object.void').take()).not.toBeUndefined() expect(form.query('object.void.normal').take()).not.toBeUndefined() expect(form.query('object.normal').take()).not.toBeUndefined() expect(form.query('object.*').map((field) => field.path.toString())).toEqual([ 'object.void', 'object.normal', ]) expect(form.query('*').map((field) => field.path.toString())).toEqual([ 'object', 'object.void', 'object.normal', 'array', ]) expect(form.query('array').take()).not.toBeUndefined() expect(form.query('*').take()).not.toBeUndefined() expect(form.query('*(oo)').take()).toBeUndefined() expect(form.query('*(oo)').map()).toEqual([]) expect(form.query('object.void').get('value')).toBeUndefined() expect(form.query('object.void').get('initialValue')).toBeUndefined() expect(form.query('object.void').get('inputValue')).toBeUndefined() expect(form.query('array').get('value')).toEqual([]) expect(form.query('array').get('initialValue')).toBeUndefined() expect(form.query('array').get('inputValue')).toBeNull() form.setFieldState('array', (state) => { state.value = [111] state.initialValue = [111] state.inputValue = [111] }) expect(form.query('array').get('value')).toEqual([111]) expect(form.query('array').get('initialValue')).toEqual([111]) expect(form.query('array').get('inputValue')).toEqual([111]) expect(form.query('array').getIn('inputValue')).toEqual([111]) expect(form.query('opo').get('value')).toBeUndefined() expect(form.query('opo').getIn('value')).toBeUndefined() expect(form.query('opo').get('initialValue')).toBeUndefined() expect(form.query('opo').get('inputValue')).toBeUndefined() }) test('notify/subscribe/unsubscribe', () => { const form = attach(createForm()) const subscribe = jest.fn() const id = form.subscribe(subscribe) expect(subscribe).toBeCalledTimes(0) form.setInitialValues({ aa: 123 }) expect(subscribe).toBeCalledTimes(2) expect(form.values).toEqual({ aa: 123 }) form.notify(LifeCycleTypes.ON_FORM_SUBMIT) expect(subscribe).toBeCalledTimes(3) form.unsubscribe(id) form.notify(LifeCycleTypes.ON_FORM_SUBMIT) expect(subscribe).toBeCalledTimes(3) }) test('setState/getState/setFormState/getFormState/setFieldState/getFieldState', () => { const form = attach(createForm()) const state = form.getState() form.setState((state) => { state.pattern = 'disabled' state.values = { aa: 123 } }) expect(form.pattern).toEqual('disabled') expect(form.disabled).toBeTruthy() expect(form.values.aa).toEqual(123) form.setState(state) expect(form.pattern).toEqual('editable') expect(form.disabled).toBeFalsy() expect(form.values.aa).toBeUndefined() form.setFormState((state) => { state.pattern = 'readOnly' state.values = { bb: 321 } }) expect(form.pattern).toEqual('readOnly') expect(form.disabled).toBeFalsy() expect(form.readOnly).toBeTruthy() expect(form.values.aa).toBeUndefined() expect(form.values.bb).toEqual(321) form.setFormState(state) expect(form.pattern).toEqual('editable') expect(form.disabled).toBeFalsy() expect(form.readOnly).toBeFalsy() expect(form.values.aa).toBeUndefined() expect(form.values.bb).toBeUndefined() attach( form.createField({ name: 'aa', }) ) const fieldState = form.getFieldState('aa') form.setFieldState('aa', (state) => { state.title = 'AA' state.description = 'This is AA' state.value = '123' }) expect(form.getFieldState('aa', (state) => state.title)).toEqual('AA') expect(form.getFieldState('aa', (state) => state.description)).toEqual( 'This is AA' ) expect(form.getFieldState('aa', (state) => state.value)).toEqual('123') form.setFieldState('aa', fieldState) expect(form.getFieldState('aa', (state) => state.title)).toBeUndefined() expect(form.getFieldState('aa', (state) => state.description)).toBeUndefined() expect(form.getFieldState('aa', (state) => state.value)).toBeUndefined() form.setState((state) => { state.display = 'none' }) expect(form.getFieldState('aa', (state) => state.visible)).toBeFalsy() const update = (value: any) => (state: any) => { state.value = value } const update2 = (state: any) => { state.value = 123 } form.setFieldState('kk', update(123)) form.setFieldState('kk', update(321)) form.setFieldState('oo', update2) form.setFieldState('oo', update2) const oo = attach( form.createField({ name: 'oo', }) ) const kk = attach( form.createField({ name: 'kk', }) ) expect(oo.value).toBeUndefined() expect(kk.value).toBeUndefined() }) test('validate/valid/invalid/errors/warnings/successes/clearErrors/clearWarnings/clearSuccesses/queryFeedbacks', async () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', required: true, validator(value) { if (value == '123') { return { type: 'success', message: 'success', } } else if (value == '321') { return { type: 'warning', message: 'warning', } } else if (value == '111') { return 'error' } }, }) ) const bb = attach( form.createField({ name: 'bb', required: true, }) ) attach( form.createVoidField({ name: 'cc', }) ) try { await form.validate() } catch {} expect(form.invalid).toBeTruthy() expect(form.valid).toBeFalsy() expect(form.errors).toEqual([ { type: 'error', address: 'aa', path: 'aa', code: 'ValidateError', triggerType: 'onInput', messages: ['The field value is required'], }, { type: 'error', address: 'bb', path: 'bb', code: 'ValidateError', triggerType: 'onInput', messages: ['The field value is required'], }, ]) await aa.onInput('123') expect(form.errors).toEqual([ { type: 'error', address: 'bb', path: 'bb', code: 'ValidateError', triggerType: 'onInput', messages: ['The field value is required'], }, ]) expect(form.successes).toEqual([ { type: 'success', address: 'aa', path: 'aa', code: 'ValidateSuccess', triggerType: 'onInput', messages: ['success'], }, ]) await aa.onInput('321') expect(form.errors).toEqual([ { type: 'error', address: 'bb', path: 'bb', code: 'ValidateError', triggerType: 'onInput', messages: ['The field value is required'], }, ]) expect(form.warnings).toEqual([ { type: 'warning', address: 'aa', path: 'aa', code: 'ValidateWarning', triggerType: 'onInput', messages: ['warning'], }, ]) await aa.onInput('111') expect(form.errors).toEqual([ { type: 'error', address: 'aa', path: 'aa', code: 'ValidateError', triggerType: 'onInput', messages: ['error'], }, { type: 'error', address: 'bb', path: 'bb', code: 'ValidateError', triggerType: 'onInput', messages: ['The field value is required'], }, ]) await aa.onInput('yes') await bb.onInput('yes') await form.validate() expect(form.invalid).toBeFalsy() expect(form.valid).toBeTruthy() expect(form.errors).toEqual([]) expect(form.successes).toEqual([]) expect(form.warnings).toEqual([]) await aa.onInput('') await bb.onInput('') try { await form.validate() } catch {} expect(form.errors).toEqual([ { type: 'error', address: 'aa', path: 'aa', code: 'ValidateError', triggerType: 'onInput', messages: ['The field value is required'], }, { type: 'error', address: 'bb', path: 'bb', code: 'ValidateError', triggerType: 'onInput', messages: ['The field value is required'], }, ]) form.clearErrors('aa') expect(form.errors).toEqual([ { type: 'error', address: 'bb', path: 'bb', code: 'ValidateError', triggerType: 'onInput', messages: ['The field value is required'], }, ]) form.clearErrors('*') expect(form.errors).toEqual([]) await aa.onInput('123') expect(form.errors).toEqual([]) expect(form.successes).toEqual([ { type: 'success', address: 'aa', path: 'aa', code: 'ValidateSuccess', triggerType: 'onInput', messages: ['success'], }, ]) form.clearSuccesses('aa') expect(form.successes).toEqual([]) await aa.onInput('321') expect(form.errors).toEqual([]) expect(form.successes).toEqual([]) expect(form.warnings).toEqual([ { type: 'warning', address: 'aa', path: 'aa', code: 'ValidateWarning', triggerType: 'onInput', messages: ['warning'], }, ]) form.clearWarnings('*') expect(form.errors).toEqual([]) expect(form.successes).toEqual([]) expect(form.warnings).toEqual([]) await aa.onInput('123') await bb.onInput('') expect( form.queryFeedbacks({ type: 'error', }).length ).toEqual(1) expect( form.queryFeedbacks({ type: 'success', }).length ).toEqual(1) expect( form.queryFeedbacks({ code: 'ValidateError', }).length ).toEqual(1) expect( form.queryFeedbacks({ code: 'ValidateSuccess', }).length ).toEqual(1) expect( form.queryFeedbacks({ code: 'EffectError', }).length ).toEqual(0) expect( form.queryFeedbacks({ code: 'EffectSuccess', }).length ).toEqual(0) expect( form.queryFeedbacks({ path: 'aa', }).length ).toEqual(1) expect( form.queryFeedbacks({ path: 'bb', }).length ).toEqual(1) expect( form.queryFeedbacks({ address: 'aa', }).length ).toEqual(1) expect( form.queryFeedbacks({ address: 'bb', }).length ).toEqual(1) aa.setValue('') bb.setValue('') form.clearErrors() form.clearSuccesses() form.clearWarnings() try { await form.validate('aa') } catch {} expect( form.queryFeedbacks({ type: 'error', }).length ).toEqual(1) try { await form.validate('*') } catch {} expect( form.queryFeedbacks({ type: 'error', }).length ).toEqual(2) }) test('setPattern/pattern/editable/readOnly/disabled/readPretty', () => { const form = attach( createForm({ pattern: 'disabled', }) ) const field = attach( form.createField({ name: 'aa', }) ) expect(form.pattern).toEqual('disabled') expect(form.disabled).toBeTruthy() expect(field.pattern).toEqual('disabled') expect(field.disabled).toBeTruthy() form.setPattern('readOnly') expect(form.pattern).toEqual('readOnly') expect(form.readOnly).toBeTruthy() expect(field.pattern).toEqual('readOnly') expect(field.readOnly).toBeTruthy() form.setPattern('readPretty') expect(form.pattern).toEqual('readPretty') expect(form.readPretty).toBeTruthy() expect(field.pattern).toEqual('readPretty') expect(field.readPretty).toBeTruthy() const form2 = attach( createForm({ editable: false, }) ) expect(form2.pattern).toEqual('readPretty') expect(form2.readPretty).toBeTruthy() const form3 = attach( createForm({ disabled: true, }) ) expect(form3.pattern).toEqual('disabled') expect(form3.disabled).toBeTruthy() const form4 = attach( createForm({ readOnly: true, }) ) expect(form4.pattern).toEqual('readOnly') expect(form4.readOnly).toBeTruthy() const form5 = attach( createForm({ readPretty: true, }) ) expect(form5.pattern).toEqual('readPretty') expect(form5.readPretty).toBeTruthy() }) test('setDisplay/display/visible/hidden', () => { const form = attach( createForm({ display: 'hidden', }) ) const field = attach( form.createField({ name: 'aa', }) ) expect(form.display).toEqual('hidden') expect(form.hidden).toBeTruthy() expect(field.display).toEqual('hidden') expect(field.hidden).toBeTruthy() form.setDisplay('visible') expect(form.display).toEqual('visible') expect(form.visible).toBeTruthy() expect(field.display).toEqual('visible') expect(field.visible).toBeTruthy() form.setDisplay('none') expect(form.display).toEqual('none') expect(form.visible).toBeFalsy() expect(field.display).toEqual('none') expect(field.visible).toBeFalsy() const form2 = attach( createForm({ hidden: true, }) ) expect(form2.display).toEqual('hidden') expect(form2.hidden).toBeTruthy() expect(form2.visible).toBeFalsy() const form3 = attach( createForm({ visible: false, }) ) expect(form3.display).toEqual('none') expect(form3.visible).toBeFalsy() }) test('submit', async () => { const form = attach(createForm()) const onSubmit = jest.fn() const field = attach( form.createField({ name: 'aa', required: true, }) ) let errors1: Error try { await form.submit(onSubmit) } catch (e) { errors1 = e } expect(errors1).not.toBeUndefined() expect(onSubmit).toBeCalledTimes(0) field.onInput('123') await form.submit(onSubmit) expect(onSubmit).toBeCalledTimes(1) let errors2: Error try { await form.submit(() => { throw new Error('xxx') }) } catch (e) { errors2 = e } expect(errors2).not.toBeUndefined() expect(form.valid).toBeTruthy() }) test('reset', async () => { const form = attach( createForm<{ aa?: number bb?: number }>({ values: { bb: 123, }, initialValues: { aa: 123, }, }) ) const field = attach( form.createField({ name: 'aa', required: true, }) ) const field2 = attach( form.createField({ name: 'bb', required: true, }) ) attach( form.createVoidField({ name: 'cc', }) ) expect(field.value).toEqual(123) expect(field2.value).toEqual(123) expect(form.values.aa).toEqual(123) expect(form.values.bb).toEqual(123) field.onInput('xxxxx') expect(form.values.aa).toEqual('xxxxx') try { await form.reset() } catch {} expect(form.valid).toBeTruthy() expect(form.values.aa).toEqual(123) expect(field.value).toEqual(123) expect(form.values.bb).toBeUndefined() expect(field2.value).toBeUndefined() field.onInput('aaa') field2.onInput('bbb') expect(form.valid).toBeTruthy() expect(form.values.aa).toEqual('aaa') expect(field.value).toEqual('aaa') expect(form.values.bb).toEqual('bbb') expect(field2.value).toEqual('bbb') try { await form.reset('*', { validate: true, }) } catch {} expect(form.valid).toBeFalsy() expect(form.values.aa).toEqual(123) expect(field.value).toEqual(123) expect(form.values.bb).toBeUndefined() expect(field2.value).toBeUndefined() field.onInput('aaa') field2.onInput('bbb') try { await form.reset('*', { forceClear: true, }) } catch {} expect(form.valid).toBeTruthy() expect(form.values.aa).toBeUndefined() expect(field.value).toBeUndefined() expect(form.values.bb).toBeUndefined() expect(field2.value).toBeUndefined() field.onInput('aaa') field2.onInput('bbb') try { await form.reset('aa', { forceClear: true, }) } catch {} expect(form.valid).toBeTruthy() expect(form.values.aa).toBeUndefined() expect(field.value).toBeUndefined() expect(form.values.bb).toEqual('bbb') expect(field2.value).toEqual('bbb') }) test('devtools', () => { // @ts-ignore window['__FORMILY_DEV_TOOLS_HOOK__'] = { inject() {}, unmount() {}, } const form = attach(createForm()) form.onUnmount() }) test('reset array field', async () => { const form = attach( createForm({ values: { array: [{ value: 123 }], }, }) ) attach( form.createArrayField({ name: 'array', required: true, }) ) expect(form.values).toEqual({ array: [{ value: 123 }], }) await form.reset('*', { forceClear: true, }) expect(form.values).toEqual({ array: [], }) }) test('reset object field', async () => { const form = attach( createForm({ values: { object: { value: 123 }, }, }) ) attach( form.createObjectField({ name: 'object', required: true, }) ) expect(form.values).toEqual({ object: { value: 123 }, }) await form.reset('*', { forceClear: true, }) expect(form.values).toEqual({ object: {}, }) }) test('initialValues merge values before create field', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) form.values.array = [{ aa: '321' }] const arr_0_aa = attach( form.createField({ name: 'aa', basePath: 'array.0', initialValue: '123', }) ) expect(array.value).toEqual([{ aa: '321' }]) expect(arr_0_aa.value).toEqual('321') }) test('no patch with empty initialValues', () => { const form = attach( createForm({ values: { array: [1, 2, 3], }, }) ) attach( form.createObjectField({ name: 'array.0.1', }) ) expect(form.values).toEqual({ array: [1, 2, 3], }) }) test('initialValues merge values after create field', () => { const form = attach(createForm()) const aa = attach( form.createArrayField({ name: 'aa', initialValue: '111', }) ) const array = attach( form.createArrayField({ name: 'array', }) ) const arr_0_aa = attach( form.createField({ name: 'aa', basePath: 'array.0', initialValue: '123', }) ) form.values.aa = '222' form.values.array = [{ aa: '321' }] expect(array.value).toEqual([{ aa: '321' }]) expect(arr_0_aa.value).toEqual('321') expect(aa.value).toEqual('222') }) test('remove property of form values with undefined value', () => { const form = attach(createForm()) const field = attach( form.createField({ name: 'aaa', initialValue: 123, }) ) expect(form.values).toMatchObject({ aaa: 123 }) field.display = 'none' expect(form.values.hasOwnProperty('aaa')).toBeFalsy() field.display = 'visible' expect(form.values.hasOwnProperty('aaa')).toBeTruthy() field.setValue(undefined) expect(form.values.hasOwnProperty('aaa')).toBeTruthy() }) test('empty array initialValues', () => { const form = attach( createForm({ initialValues: { aa: [0], bb: [''], cc: [], dd: [null], ee: [undefined], }, }) ) form.createArrayField({ name: 'aa', }) form.createArrayField({ name: 'bb', }) form.createArrayField({ name: 'cc', }) form.createArrayField({ name: 'dd', }) form.createArrayField({ name: 'ee', }) expect(form.values.aa).toEqual([0]) expect(form.values.bb).toEqual(['']) expect(form.values.cc).toEqual([]) expect(form.values.dd).toEqual([null]) expect(form.values.ee).toEqual([undefined]) }) test('form lifecycle can be triggered after call form.setXXX', () => { let initialValuesTriggerNum = 0 let valuesTriggerNum = 0 const form = attach( createForm<{ aa?: number bb?: number }>({ initialValues: { aa: 1, }, values: { bb: 1, }, }) ) form.setEffects(() => { onFormInitialValuesChange(() => { initialValuesTriggerNum++ }) onFormValuesChange(() => { valuesTriggerNum++ }) }) expect(initialValuesTriggerNum).toEqual(0) expect(valuesTriggerNum).toEqual(0) form.initialValues.aa = 2 form.values.bb = 2 expect(initialValuesTriggerNum).toEqual(1) // initialValues 会通过 applyValuesPatch 改变 values,导致 onFormValuesChange 多触发一次 expect(valuesTriggerNum).toEqual(2) form.setInitialValues({ aa: 3 }) form.setValues({ bb: 3 }) expect(initialValuesTriggerNum).toEqual(2) expect(valuesTriggerNum).toEqual(4) // 测试 form.setXXX 之后还能正常触发:https://github.com/alibaba/formily/issues/1675 form.initialValues.aa = 4 form.values.bb = 4 expect(initialValuesTriggerNum).toEqual(3) expect(valuesTriggerNum).toEqual(6) }) test('form values change with array field(default value)', async () => { const handler = jest.fn() const form = attach( createForm({ effects() { onFormValuesChange(handler) }, }) ) const array = attach( form.createArrayField({ name: 'array', initialValue: [ { hello: 'world', }, ], }) ) await array.push({}) expect(handler).toBeCalledTimes(2) }) test('setValues deep merge', () => { const form = attach( createForm({ initialValues: { aa: { bb: 123, cc: 321, dd: [11, 22, 33], }, }, }) ) expect(form.values).toEqual({ aa: { bb: 123, cc: 321, dd: [11, 22, 33], }, }) form.setValues({ aa: { bb: '', cc: '', dd: [44, 55, 66], }, }) expect(form.values).toEqual({ aa: { bb: '', cc: '', dd: [44, 55, 66], }, }) }) test('exception validate', async () => { const form = attach(createForm()) attach( form.createField({ name: 'aa', validator() { throw new Error('runtime error') }, }) ) try { await form.validate() } catch {} expect(form.invalid).toBeTruthy() expect(form.validating).toBeFalsy() }) test('designable form', () => { const form = attach( createForm({ designable: true, }) ) attach( form.createField({ name: 'bb', initialValue: 123, }) ) attach( form.createField({ name: 'bb', initialValue: 321, }) ) attach( form.createField({ name: 'aa', value: 123, }) ) attach( form.createField({ name: 'aa', value: 321, }) ) expect(form.values.aa).toEqual(321) expect(form.initialValues.bb).toEqual(321) }) test('validate will skip display none', async () => { const validateA = jest.fn() const validateB = jest.fn() const form = attach( createForm({ effects() { onFieldValidateStart('aa', validateA) onFieldValidateStart('bb', validateB) }, }) ) const validator = jest.fn() const aa = attach( form.createField({ name: 'aa', validator() { validator() return 'error' }, }) ) const bb = attach( form.createField({ name: 'bb', validator() { validator() return 'error' }, }) ) try { await form.validate() } catch (e) { expect(e).toEqual([ { triggerType: 'onInput', type: 'error', code: 'ValidateError', messages: ['error'], address: 'aa', path: 'aa', }, { triggerType: 'onInput', type: 'error', code: 'ValidateError', messages: ['error'], address: 'bb', path: 'bb', }, ]) } expect(validateA).toBeCalledTimes(1) expect(validateB).toBeCalledTimes(1) expect(aa.invalid).toBeTruthy() expect(bb.invalid).toBeTruthy() expect(validator).toBeCalledTimes(2) aa.display = 'none' try { await form.validate() } catch (e) { expect(e).toEqual([ { triggerType: 'onInput', type: 'error', code: 'ValidateError', messages: ['error'], address: 'bb', path: 'bb', }, ]) } expect(validateA).toBeCalledTimes(1) expect(validateB).toBeCalledTimes(2) expect(aa.invalid).toBeFalsy() expect(bb.invalid).toBeTruthy() expect(validator).toBeCalledTimes(3) bb.display = 'none' await form.validate() expect(validateA).toBeCalledTimes(1) expect(validateB).toBeCalledTimes(2) expect(aa.invalid).toBeFalsy() expect(bb.invalid).toBeFalsy() expect(validator).toBeCalledTimes(3) }) test('validate will skip unmounted', async () => { const validateA = jest.fn() const validateB = jest.fn() const form = attach( createForm({ effects() { onFieldValidateStart('aa', validateA) onFieldValidateStart('bb', validateB) }, }) ) const validator = jest.fn() const aa = attach( form.createField({ name: 'aa', validator() { validator() return 'error' }, }) ) const bb = attach( form.createField({ name: 'bb', validator() { validator() return 'error' }, }) ) try { await form.validate() } catch (e) { expect(e).toEqual([ { triggerType: 'onInput', type: 'error', code: 'ValidateError', messages: ['error'], address: 'aa', path: 'aa', }, { triggerType: 'onInput', type: 'error', code: 'ValidateError', messages: ['error'], address: 'bb', path: 'bb', }, ]) } expect(validateA).toBeCalledTimes(1) expect(validateB).toBeCalledTimes(1) expect(aa.invalid).toBeTruthy() expect(bb.invalid).toBeTruthy() expect(validator).toBeCalledTimes(2) aa.onUnmount() try { await form.validate() } catch (e) { expect(e).toEqual([ { triggerType: 'onInput', type: 'error', code: 'ValidateError', messages: ['error'], address: 'aa', path: 'aa', }, { triggerType: 'onInput', type: 'error', code: 'ValidateError', messages: ['error'], address: 'bb', path: 'bb', }, ]) } expect(validateA).toBeCalledTimes(2) expect(validateB).toBeCalledTimes(2) expect(aa.invalid).toBeTruthy() expect(bb.invalid).toBeTruthy() expect(validator).toBeCalledTimes(4) form.clearFormGraph('*(aa,bb)') await form.validate() expect(validateA).toBeCalledTimes(2) expect(validateB).toBeCalledTimes(2) expect(aa.invalid).toBeFalsy() expect(bb.invalid).toBeFalsy() expect(validator).toBeCalledTimes(4) }) test('validate will skip uneditable', async () => { const validateA = jest.fn() const validateB = jest.fn() const form = attach( createForm({ effects() { onFieldValidateStart('aa', validateA) onFieldValidateStart('bb', validateB) }, }) ) const validator = jest.fn() const aa = attach( form.createField({ name: 'aa', validator() { validator() return 'error' }, }) ) const bb = attach( form.createField({ name: 'bb', validator() { validator() return 'error' }, }) ) try { await form.validate() } catch (e) { expect(e).toEqual([ { triggerType: 'onInput', type: 'error', code: 'ValidateError', messages: ['error'], address: 'aa', path: 'aa', }, { triggerType: 'onInput', type: 'error', code: 'ValidateError', messages: ['error'], address: 'bb', path: 'bb', }, ]) } expect(validateA).toBeCalledTimes(1) expect(validateB).toBeCalledTimes(1) expect(aa.invalid).toBeTruthy() expect(bb.invalid).toBeTruthy() expect(validator).toBeCalledTimes(2) aa.editable = false try { await form.validate() } catch (e) { expect(e).toEqual([ { triggerType: 'onInput', type: 'error', code: 'ValidateError', messages: ['error'], address: 'bb', path: 'bb', }, ]) } expect(validateA).toBeCalledTimes(1) expect(validateB).toBeCalledTimes(2) expect(aa.invalid).toBeFalsy() expect(bb.invalid).toBeTruthy() expect(validator).toBeCalledTimes(3) bb.editable = false await form.validate() expect(validateA).toBeCalledTimes(1) expect(validateB).toBeCalledTimes(2) expect(aa.invalid).toBeFalsy() expect(bb.invalid).toBeFalsy() expect(validator).toBeCalledTimes(3) }) test('validator order with format', async () => { const form = attach(createForm()) attach( form.createField({ name: 'aa', required: true, validator: { format: 'url', message: 'custom', }, }) ) attach( form.createField({ name: 'bb', required: true, validator: (value) => { if (!value) return '' return value !== '111' ? 'custom' : '' }, }) ) const results = await form.submit(() => {}).catch((e) => e) expect(results.map(({ messages }) => messages)).toEqual([ ['The field value is required'], ['The field value is required'], ]) }) test('form unmount can not effect field values', () => { const form = attach( createForm({ values: { aa: '123', }, }) ) attach( form.createField({ name: 'aa', }) ) expect(form.values.aa).toEqual('123') form.onUnmount() expect(form.values.aa).toEqual('123') }) test('form clearFormGraph need clear field values', () => { const form = attach( createForm({ values: { aa: '123', }, }) ) attach( form.createField({ name: 'aa', }) ) expect(form.values.aa).toEqual('123') form.clearFormGraph('*') expect(form.values.aa).toBeUndefined() }) test('form clearFormGraph not clear field values', () => { const form = attach( createForm({ values: { aa: '123', }, }) ) attach( form.createField({ name: 'aa', }) ) expect(form.values.aa).toEqual('123') form.clearFormGraph('*', false) expect(form.values.aa).toEqual('123') }) test('form values auto clean with visible false', () => { const form = attach( createForm({ initialValues: { aa: '123', bb: '321', cc: 'cc', }, }) ) attach( form.createField({ name: 'aa', }) ) attach( form.createField({ name: 'bb', reactions: (field) => { field.visible = form.values.aa === '1233' }, }) ) attach( form.createField({ name: 'cc', }) ) expect(form.values).toEqual({ aa: '123', cc: 'cc', }) }) test('form values auto clean with visible false in async setInitialValues', () => { const form = attach(createForm()) attach( form.createField({ name: 'aa', }) ) attach( form.createField({ name: 'bb', reactions: (field) => { field.visible = form.values.aa === '1233' }, }) ) attach( form.createField({ name: 'cc', }) ) form.setInitialValues({ aa: '123', bb: '321', cc: 'cc', }) expect(form.values).toEqual({ aa: '123', cc: 'cc', }) }) test('form values ref should not changed with setValues', () => { const form = attach( createForm({ values: { aa: '123', }, }) ) const values = form.values form.setValues({ bb: '321', }) expect(form.values === values).toBeTruthy() }) test('form initial values ref should not changed with setInitialValues', () => { const form = attach( createForm({ initialValues: { aa: '123', }, }) ) const values = form.initialValues form.setInitialValues({ bb: '321', }) expect(form.initialValues === values).toBeTruthy() }) test('form query undefined query should not throw error', () => { const form = attach(createForm()) ;(form.fields as any)['a'] = undefined expect(() => form.query('*').take()).not.toThrowError() expect(Object.keys(form.fields)).toEqual([]) }) ================================================ FILE: packages/core/src/__tests__/graph.spec.ts ================================================ import { createForm } from '../' import { isVoidField } from '../shared/checkers' import { attach } from './shared' test('getGraph/setGraph', () => { const form = attach(createForm()) attach( form.createField({ name: 'normal', }) ) attach( form.createArrayField({ name: 'array', }) ) attach( form.createObjectField({ name: 'object', }) ) attach( form.createVoidField({ name: 'void', }) ) form.query('normal').take((field) => { if (isVoidField(field)) return field.selfErrors = ['error'] }) const graph = form.getFormGraph() form.clearFormGraph() form.setFormGraph(graph) const graph2 = form.getFormGraph() expect(graph).toEqual(graph2) form.setFormGraph({ object: { value: 123, }, }) expect(form.query('object').get('value')).toEqual(123) }) test('clearFormGraph', () => { const form = attach(createForm()) attach( form.createField({ name: 'normal', }) ) attach( form.createArrayField({ name: 'array', }) ) attach( form.createObjectField({ name: 'object', }) ) form.clearFormGraph('normal') expect(form.fields['normal']).toBeUndefined() expect(form.fields['array']).not.toBeUndefined() }) ================================================ FILE: packages/core/src/__tests__/heart.spec.ts ================================================ import { Heart, LifeCycle } from '../models' test('buildLifecycles', () => { const heart = new Heart({ lifecycles: [{} as any, [{}], 123], }) expect(heart.lifecycles.length).toEqual(0) }) test('clear heart', () => { const handler = jest.fn() const heart = new Heart({ lifecycles: [new LifeCycle('event', handler)], }) heart.publish('event') expect(handler).toBeCalledTimes(1) heart.clear() heart.publish('event') expect(handler).toBeCalledTimes(1) heart.publish({}) }) test('set lifecycles', () => { const handler = jest.fn() const heart = new Heart() heart.setLifeCycles([new LifeCycle('event', handler)]) heart.publish('event') expect(handler).toBeCalledTimes(1) heart.setLifeCycles() }) test('add/remove lifecycle', () => { const handler = jest.fn() const heart = new Heart() heart.addLifeCycles('xxx', [new LifeCycle('event', handler)]) heart.addLifeCycles('yyy') heart.publish('event') expect(handler).toBeCalledTimes(1) heart.removeLifeCycles('xxx') heart.publish('event') expect(handler).toBeCalledTimes(1) }) test('add/clear lifecycle', () => { const handler = jest.fn() const heart = new Heart() heart.addLifeCycles('xxx', [new LifeCycle('event', handler)]) heart.addLifeCycles('yyy') heart.publish('event') expect(handler).toBeCalledTimes(1) heart.clear() heart.publish('event') expect(handler).toBeCalledTimes(1) }) ================================================ FILE: packages/core/src/__tests__/internals.spec.ts ================================================ import { getValuesFromEvent, matchFeedback, patchFieldStates, deserialize, isHTMLInputEvent, } from '../shared/internals' import { createForm } from '../' import { attach } from './shared' test('getValuesFromEvent', () => { expect(getValuesFromEvent([{ target: { value: 123 } }])).toEqual([123]) expect(getValuesFromEvent([{ target: { checked: true } }])).toEqual([true]) expect(getValuesFromEvent([{ target: {} }])).toEqual([undefined]) expect(getValuesFromEvent([{ target: null }])).toEqual([{ target: null }]) expect(getValuesFromEvent([123])).toEqual([123]) expect(getValuesFromEvent([null])).toEqual([null]) }) test('empty', () => { expect(matchFeedback()).toBeFalsy() }) test('patchFieldStates', () => { const fields = {} patchFieldStates(fields, [{ type: 'update', address: 'aaa', payload: null }]) patchFieldStates(fields, [ { type: 'update3' as any, address: 'aaa', payload: null }, ]) expect(fields).toEqual({}) }) test('patchFieldStates should be sequence', () => { const form = attach(createForm()) attach( form.createArrayField({ name: 'array', }) ) attach( form.createField({ name: 'input', basePath: 'array.0', }) ) attach( form.createField({ name: 'input', basePath: 'array.1', }) ) const before = Object.keys(form.fields) form.fields['array'].move(1, 0) const after = Object.keys(form.fields) expect(after).toEqual(before) const form2 = attach(createForm()) attach( form2.createField({ name: 'field1', title: 'Field 1', }) ) attach( form2.createField({ name: 'field2', title: 'Field 1', }) ) patchFieldStates(form2.fields, [ { type: 'update', address: 'field2', oldAddress: 'field1', payload: form2.field1, }, { type: 'update', address: 'field1', oldAddress: 'field2', payload: form2.field2, }, ]) expect(Object.keys(form2.fields)).toEqual(['field1', 'field2']) }) test('deserialize', () => { expect(deserialize(null, null)).toBeUndefined() expect( deserialize( {}, { parent: null, } ) ).toEqual({}) }) test('isHTMLInputEvent', () => { expect(isHTMLInputEvent({ target: { checked: true } })).toBeTruthy() expect(isHTMLInputEvent({ target: { value: 123 } })).toBeTruthy() expect( isHTMLInputEvent({ target: { tagName: 'INPUT', value: null } }) ).toBeTruthy() expect(isHTMLInputEvent({ target: { tagName: 'INPUT' } })).toBeFalsy() expect(isHTMLInputEvent({ target: { tagName: 'DIV' } })).toBeFalsy() expect(isHTMLInputEvent({ target: {}, stopPropagation() {} })).toBeFalsy() expect(isHTMLInputEvent({})).toBeFalsy() }) ================================================ FILE: packages/core/src/__tests__/lifecycle.spec.ts ================================================ import { LifeCycle } from '../models' test('create lifecycle', () => { const handler1 = jest.fn() const lifecycle1 = new LifeCycle(handler1) lifecycle1.notify('event1') expect(handler1).toBeCalledTimes(1) expect(handler1).toBeCalledWith( { type: 'event1', payload: undefined, }, undefined ) lifecycle1.notify('event11', 'payload1') expect(handler1).toBeCalledTimes(2) expect(handler1).toBeCalledWith( { type: 'event11', payload: 'payload1', }, undefined ) const context: any = {} lifecycle1.notify('event12', 'payload11', context) expect(handler1).toBeCalledTimes(3) expect(handler1).toBeCalledWith( { type: 'event12', payload: 'payload11', }, context ) const handler2 = jest.fn() const lifecycle2 = new LifeCycle('event2', handler2) lifecycle2.notify('event1') expect(handler2).not.toBeCalled() lifecycle2.notify('event2') expect(handler2).toBeCalledTimes(1) const handler31 = jest.fn() const handler32 = jest.fn() const lifecycle3 = new LifeCycle({ event31: handler31, event32: handler32, }) lifecycle3.notify('event3') expect(handler31).not.toBeCalled() expect(handler32).not.toBeCalled() lifecycle3.notify('event31') expect(handler31).toBeCalledTimes(1) expect(handler32).not.toBeCalled() lifecycle3.notify('event32') expect(handler31).toBeCalledTimes(1) expect(handler32).toBeCalledTimes(1) }) ================================================ FILE: packages/core/src/__tests__/object.spec.ts ================================================ import { createForm } from '../' import { attach } from './shared' test('create object field', () => { const form = attach(createForm()) const object = attach( form.createObjectField({ name: 'object', }) ) expect(object.value).toEqual({}) expect(object.addProperty).toBeDefined() expect(object.removeProperty).toBeDefined() expect(object.existProperty).toBeDefined() }) test('create object field methods', () => { const form = attach(createForm()) const object = attach( form.createObjectField({ name: 'object', value: {}, }) ) expect(object.value).toEqual({}) object.addProperty('aaa', 123) expect(object.value).toEqual({ aaa: 123 }) object.removeProperty('aaa') expect(object.value).toEqual({}) expect(object.existProperty('aaa')).toBeFalsy() }) ================================================ FILE: packages/core/src/__tests__/shared.ts ================================================ export const attach = void }>(target: T): T => { target.onMount() return target } export const sleep = (duration = 100) => new Promise((resolve) => { setTimeout(resolve, duration) }) ================================================ FILE: packages/core/src/__tests__/void.spec.ts ================================================ import { createForm } from '../' import { attach } from './shared' test('create void field', () => { const form = attach(createForm()) const field = attach( form.createVoidField({ name: 'void', }) ) field.destroy() }) test('create void field props', () => { const form = attach(createForm()) const field1 = attach( form.createVoidField({ name: 'field1', title: 'Field 1', description: 'This is Field 1', }) ) expect(field1.title).toEqual('Field 1') expect(field1.description).toEqual('This is Field 1') const field2 = attach( form.createVoidField({ name: 'field2', disabled: true, hidden: true, }) ) expect(field2.pattern).toEqual('disabled') expect(field2.disabled).toBeTruthy() expect(field2.display).toEqual('hidden') expect(field2.hidden).toBeTruthy() const field3 = attach( form.createVoidField({ name: 'field3', readOnly: true, visible: false, }) ) expect(field3.pattern).toEqual('readOnly') expect(field3.readOnly).toBeTruthy() expect(field3.display).toEqual('none') expect(field3.visible).toBeFalsy() }) test('setComponent/setComponentProps', () => { const form = attach(createForm()) const field = attach( form.createVoidField({ name: 'aa', }) ) const component = () => null field.setComponent(component, { props: 123 }) expect(field.component[0]).toEqual(component) expect(field.component[1]).toEqual({ props: 123 }) field.setComponentProps({ hello: 'world', }) expect(field.component[1]).toEqual({ props: 123, hello: 'world' }) }) test('setTitle/setDescription', () => { const form = attach(createForm()) const aa = attach( form.createVoidField({ name: 'aa', }) ) aa.setTitle('AAA') aa.setDescription('This is AAA') expect(aa.title).toEqual('AAA') expect(aa.description).toEqual('This is AAA') }) test('setComponent/setComponentProps', () => { const component = () => null const form = attach(createForm()) const field = attach( form.createVoidField({ name: 'aa', }) ) field.setComponent(undefined, { props: 123 }) field.setComponent(component) expect(field.component[0]).toEqual(component) expect(field.component[1]).toEqual({ props: 123 }) field.setComponentProps({ hello: 'world', }) expect(field.component[1]).toEqual({ props: 123, hello: 'world' }) }) test('setDecorator/setDecoratorProps', () => { const component = () => null const form = attach(createForm()) const field = attach( form.createVoidField({ name: 'aa', }) ) field.setDecorator(undefined, { props: 123 }) field.setDecorator(component) expect(field.decorator[0]).toEqual(component) expect(field.decorator[1]).toEqual({ props: 123 }) field.setDecoratorProps({ hello: 'world', }) expect(field.decorator[1]).toEqual({ props: 123, hello: 'world' }) }) test('setState/getState', () => { const form = attach(createForm()) const aa = attach( form.createVoidField({ name: 'aa', }) ) const state = aa.getState() aa.setState((state) => { state.title = 'AAA' }) expect(aa.title).toEqual('AAA') aa.setState(state) expect(aa.title).toBeUndefined() aa.setState((state) => { state.hidden = false }) expect(aa.display).toEqual('visible') aa.setState((state) => { state.visible = true }) expect(aa.display).toEqual('visible') aa.setState((state) => { state.readOnly = false }) expect(aa.pattern).toEqual('editable') aa.setState((state) => { state.disabled = false }) expect(aa.pattern).toEqual('editable') aa.setState((state) => { state.editable = true }) expect(aa.pattern).toEqual('editable') aa.setState((state) => { state.editable = false }) expect(aa.pattern).toEqual('readPretty') aa.setState((state) => { state.readPretty = true }) expect(aa.pattern).toEqual('readPretty') aa.setState((state) => { state.readPretty = false }) expect(aa.pattern).toEqual('editable') expect(aa.parent).toBeUndefined() }) test('nested display/pattern', () => { const form = attach(createForm()) attach( form.createObjectField({ name: 'object', }) ) const void_ = attach( form.createVoidField({ name: 'void', basePath: 'object', }) ) const void2_ = attach( form.createVoidField({ name: 'void', basePath: 'object.void.0', }) ) const aaa = attach( form.createField({ name: 'aaa', basePath: 'object.void', }) ) const bbb = attach( form.createField({ name: 'bbb', basePath: 'object.void', }) ) void_.setPattern('readPretty') expect(void_.pattern).toEqual('readPretty') expect(aaa.pattern).toEqual('readPretty') expect(bbb.pattern).toEqual('readPretty') void_.setPattern('readOnly') expect(void_.pattern).toEqual('readOnly') expect(aaa.pattern).toEqual('readOnly') expect(bbb.pattern).toEqual('readOnly') void_.setPattern('disabled') expect(void_.pattern).toEqual('disabled') expect(aaa.pattern).toEqual('disabled') expect(bbb.pattern).toEqual('disabled') void_.setPattern() expect(void_.pattern).toEqual('editable') expect(aaa.pattern).toEqual('editable') expect(bbb.pattern).toEqual('editable') void_.setDisplay('hidden') expect(void_.display).toEqual('hidden') expect(aaa.display).toEqual('hidden') expect(bbb.display).toEqual('hidden') void_.setDisplay('none') expect(void_.display).toEqual('none') expect(aaa.display).toEqual('none') expect(bbb.display).toEqual('none') void_.setDisplay() expect(void_.display).toEqual('visible') expect(aaa.display).toEqual('visible') expect(bbb.display).toEqual('visible') void_.onUnmount() expect(void2_.parent === void_).toBeTruthy() }) test('reactions', async () => { const form = attach(createForm()) const aa = attach( form.createField({ name: 'aa', }) ) const bb = attach( form.createVoidField({ name: 'bb', reactions: [ (field) => { const aa = field.query('aa') if (aa.get('value') === '123') { field.visible = false } else { field.visible = true } if (aa.get('inputValue') === '333') { field.editable = false } else if (aa.get('inputValue') === '444') { field.editable = true } if (aa.get('initialValue') === '555') { field.readOnly = true } else if (aa.get('initialValue') === '666') { field.readOnly = false } }, null, ], }) ) expect(bb.visible).toBeTruthy() aa.setValue('123') expect(bb.visible).toBeFalsy() await aa.onInput('333') expect(bb.editable).toBeFalsy() await aa.onInput('444') expect(bb.editable).toBeTruthy() aa.setInitialValue('555') expect(bb.readOnly).toBeTruthy() aa.setInitialValue('666') expect(bb.readOnly).toBeFalsy() form.onUnmount() }) test('fault tolerance', () => { const form = attach(createForm()) form.setDisplay(null) form.setPattern(null) const field = attach( form.createVoidField({ name: 'xxx', }) ) expect(field.display).toEqual('visible') expect(field.pattern).toEqual('editable') }) test('child field reactions', () => { const form = attach(createForm()) const voidField = attach(form.createVoidField({ name: 'void' })) const field1 = attach( form.createField({ name: 'field1', basePath: voidField.address, reactions: [ (field) => { field.value = field.query('field3').getIn('value') }, ], }) ) const field2 = attach( form.createField({ name: 'field2', basePath: voidField.address, reactions: [ (field) => { field.value = field.query('.field3').getIn('value') }, ], }) ) expect(field1.value).toBeUndefined() expect(field2.value).toBeUndefined() const field3 = attach( form.createField({ name: 'field3', basePath: voidField.address, value: 1, }) ) expect(field1.value).toBe(1) expect(field2.value).toBe(1) field3.value = 2 expect(field1.value).toBe(2) expect(field2.value).toBe(2) }) ================================================ FILE: packages/core/src/effects/index.ts ================================================ export * from './onFormEffects' export * from './onFieldEffects' ================================================ FILE: packages/core/src/effects/onFieldEffects.ts ================================================ import { FormPath, isFn, toArr } from '@formily/shared' import { autorun, reaction, batch } from '@formily/reactive' import { Form } from '../models' import { LifeCycleTypes, FormPathPattern, GeneralField, DataField, IFieldState, } from '../types' import { createEffectHook, useEffectForm } from '../shared/effective' function createFieldEffect( type: LifeCycleTypes ) { return createEffectHook( type, (field: Result, form: Form) => ( pattern: FormPathPattern, callback: (field: Result, form: Form) => void ) => { if ( FormPath.parse(pattern).matchAliasGroup(field.address, field.path) ) { batch(() => { callback(field, form) }) } } ) } const _onFieldInit = createFieldEffect(LifeCycleTypes.ON_FIELD_INIT) export const onFieldMount = createFieldEffect(LifeCycleTypes.ON_FIELD_MOUNT) export const onFieldUnmount = createFieldEffect(LifeCycleTypes.ON_FIELD_UNMOUNT) export const onFieldValueChange = createFieldEffect( LifeCycleTypes.ON_FIELD_VALUE_CHANGE ) export const onFieldInitialValueChange = createFieldEffect( LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE ) export const onFieldInputValueChange = createFieldEffect( LifeCycleTypes.ON_FIELD_INPUT_VALUE_CHANGE ) export const onFieldValidateStart = createFieldEffect( LifeCycleTypes.ON_FIELD_VALIDATE_START ) export const onFieldValidateEnd = createFieldEffect( LifeCycleTypes.ON_FIELD_VALIDATE_END ) export const onFieldValidating = createFieldEffect( LifeCycleTypes.ON_FIELD_VALIDATING ) export const onFieldValidateFailed = createFieldEffect( LifeCycleTypes.ON_FIELD_VALIDATE_FAILED ) export const onFieldValidateSuccess = createFieldEffect( LifeCycleTypes.ON_FIELD_VALIDATE_SUCCESS ) export const onFieldSubmit = createFieldEffect( LifeCycleTypes.ON_FIELD_SUBMIT ) export const onFieldSubmitStart = createFieldEffect( LifeCycleTypes.ON_FIELD_SUBMIT_START ) export const onFieldSubmitEnd = createFieldEffect( LifeCycleTypes.ON_FIELD_SUBMIT_END ) export const onFieldSubmitValidateStart = createFieldEffect( LifeCycleTypes.ON_FIELD_SUBMIT_VALIDATE_START ) export const onFieldSubmitValidateEnd = createFieldEffect( LifeCycleTypes.ON_FIELD_SUBMIT_VALIDATE_END ) export const onFieldSubmitSuccess = createFieldEffect( LifeCycleTypes.ON_FIELD_SUBMIT_SUCCESS ) export const onFieldSubmitFailed = createFieldEffect( LifeCycleTypes.ON_FIELD_SUBMIT_FAILED ) export const onFieldSubmitValidateSuccess = createFieldEffect( LifeCycleTypes.ON_FIELD_SUBMIT_VALIDATE_SUCCESS ) export const onFieldSubmitValidateFailed = createFieldEffect( LifeCycleTypes.ON_FIELD_SUBMIT_VALIDATE_FAILED ) export const onFieldReset = createFieldEffect( LifeCycleTypes.ON_FIELD_RESET ) export const onFieldLoading = createFieldEffect( LifeCycleTypes.ON_FIELD_LOADING ) export function onFieldInit( pattern: FormPathPattern, callback?: (field: GeneralField, form: Form) => void ) { const form = useEffectForm() const count = form.query(pattern).reduce((count, field) => { callback(field, form) return count + 1 }, 0) if (count === 0) { _onFieldInit(pattern, callback) } } export function onFieldReact( pattern: FormPathPattern, callback?: (field: GeneralField, form: Form) => void ) { onFieldInit(pattern, (field, form) => { field.disposers.push( autorun(() => { if (isFn(callback)) callback(field, form) }) ) }) } export function onFieldChange( pattern: FormPathPattern, callback?: (field: GeneralField, form: Form) => void ): void export function onFieldChange( pattern: FormPathPattern, watches: (keyof IFieldState)[], callback?: (field: GeneralField, form: Form) => void ): void export function onFieldChange( pattern: FormPathPattern, watches: any, callback?: (field: GeneralField, form: Form) => void ): void { if (isFn(watches)) { callback = watches watches = ['value'] } else { watches = watches || ['value'] } onFieldInit(pattern, (field, form) => { if (isFn(callback)) callback(field, form) const dispose = reaction( () => { return toArr(watches).map((key) => { return field[key] }) }, () => { if (isFn(callback)) callback(field, form) } ) field.disposers.push(dispose) }) } ================================================ FILE: packages/core/src/effects/onFormEffects.ts ================================================ import { isFn } from '@formily/shared' import { autorun, batch } from '@formily/reactive' import { Form } from '../models' import { LifeCycleTypes } from '../types' import { createEffectHook } from '../shared/effective' function createFormEffect(type: LifeCycleTypes) { return createEffectHook( type, (form: Form) => (callback: (form: Form) => void) => { batch(() => { callback(form) }) } ) } export const onFormInit = createFormEffect(LifeCycleTypes.ON_FORM_INIT) export const onFormMount = createFormEffect(LifeCycleTypes.ON_FORM_MOUNT) export const onFormUnmount = createFormEffect(LifeCycleTypes.ON_FORM_UNMOUNT) export const onFormValuesChange = createFormEffect( LifeCycleTypes.ON_FORM_VALUES_CHANGE ) export const onFormInitialValuesChange = createFormEffect( LifeCycleTypes.ON_FORM_INITIAL_VALUES_CHANGE ) export const onFormInputChange = createFormEffect( LifeCycleTypes.ON_FORM_INPUT_CHANGE ) export const onFormSubmit = createFormEffect(LifeCycleTypes.ON_FORM_SUBMIT) export const onFormReset = createFormEffect(LifeCycleTypes.ON_FORM_RESET) export const onFormSubmitStart = createFormEffect( LifeCycleTypes.ON_FORM_SUBMIT_START ) export const onFormSubmitEnd = createFormEffect( LifeCycleTypes.ON_FORM_SUBMIT_END ) export const onFormSubmitSuccess = createFormEffect( LifeCycleTypes.ON_FORM_SUBMIT_SUCCESS ) export const onFormSubmitFailed = createFormEffect( LifeCycleTypes.ON_FORM_SUBMIT_FAILED ) export const onFormSubmitValidateStart = createFormEffect( LifeCycleTypes.ON_FORM_SUBMIT_VALIDATE_START ) export const onFormSubmitValidateSuccess = createFormEffect( LifeCycleTypes.ON_FORM_SUBMIT_VALIDATE_SUCCESS ) export const onFormSubmitValidateFailed = createFormEffect( LifeCycleTypes.ON_FORM_SUBMIT_VALIDATE_FAILED ) export const onFormSubmitValidateEnd = createFormEffect( LifeCycleTypes.ON_FORM_SUBMIT_VALIDATE_END ) export const onFormValidateStart = createFormEffect( LifeCycleTypes.ON_FORM_VALIDATE_START ) export const onFormValidateSuccess = createFormEffect( LifeCycleTypes.ON_FORM_VALIDATE_SUCCESS ) export const onFormValidateFailed = createFormEffect( LifeCycleTypes.ON_FORM_VALIDATE_FAILED ) export const onFormValidateEnd = createFormEffect( LifeCycleTypes.ON_FORM_VALIDATE_END ) export const onFormGraphChange = createFormEffect( LifeCycleTypes.ON_FORM_GRAPH_CHANGE ) export const onFormLoading = createFormEffect(LifeCycleTypes.ON_FORM_LOADING) export function onFormReact(callback?: (form: Form) => void) { let dispose = null onFormInit((form) => { dispose = autorun(() => { if (isFn(callback)) callback(form) }) }) onFormUnmount(() => { dispose() }) } ================================================ FILE: packages/core/src/global.d.ts ================================================ import * as Types from './types' import * as Models from './models' declare global { namespace Formily.Core { export { Types, Models } } } ================================================ FILE: packages/core/src/index.ts ================================================ export * from './shared/externals' export * from './models/types' export * from './effects' export * from './types' ================================================ FILE: packages/core/src/models/ArrayField.ts ================================================ import { isArr, move } from '@formily/shared' import { action, reaction } from '@formily/reactive' import { spliceArrayState, exchangeArrayState, cleanupArrayChildren, } from '../shared/internals' import { Field } from './Field' import { Form } from './Form' import { JSXComponent, IFieldProps, FormPathPattern } from '../types' export class ArrayField< Decorator extends JSXComponent = any, Component extends JSXComponent = any > extends Field { displayName = 'ArrayField' constructor( address: FormPathPattern, props: IFieldProps, form: Form, designable: boolean ) { super(address, props, form, designable) this.makeAutoCleanable() } protected makeAutoCleanable() { this.disposers.push( reaction( () => this.value?.length, (newLength, oldLength) => { if (oldLength && !newLength) { cleanupArrayChildren(this, 0) } else if (newLength < oldLength) { cleanupArrayChildren(this, newLength) } } ) ) } push = (...items: any[]) => { return action(() => { if (!isArr(this.value)) { this.value = [] } this.value.push(...items) return this.onInput(this.value) }) } pop = () => { if (!isArr(this.value)) return return action(() => { const index = this.value.length - 1 spliceArrayState(this, { startIndex: index, deleteCount: 1, }) this.value.pop() return this.onInput(this.value) }) } insert = (index: number, ...items: any[]) => { return action(() => { if (!isArr(this.value)) { this.value = [] } if (items.length === 0) { return } spliceArrayState(this, { startIndex: index, insertCount: items.length, }) this.value.splice(index, 0, ...items) return this.onInput(this.value) }) } remove = (index: number) => { if (!isArr(this.value)) return return action(() => { spliceArrayState(this, { startIndex: index, deleteCount: 1, }) this.value.splice(index, 1) return this.onInput(this.value) }) } shift = () => { if (!isArr(this.value)) return return action(() => { this.value.shift() return this.onInput(this.value) }) } unshift = (...items: any[]) => { return action(() => { if (!isArr(this.value)) { this.value = [] } spliceArrayState(this, { startIndex: 0, insertCount: items.length, }) this.value.unshift(...items) return this.onInput(this.value) }) } move = (fromIndex: number, toIndex: number) => { if (!isArr(this.value)) return if (fromIndex === toIndex) return return action(() => { move(this.value, fromIndex, toIndex) exchangeArrayState(this, { fromIndex, toIndex, }) return this.onInput(this.value) }) } moveUp = (index: number) => { if (!isArr(this.value)) return return this.move(index, index - 1 < 0 ? this.value.length - 1 : index - 1) } moveDown = (index: number) => { if (!isArr(this.value)) return return this.move(index, index + 1 >= this.value.length ? 0 : index + 1) } } ================================================ FILE: packages/core/src/models/BaseField.ts ================================================ import { FormPath, FormPathPattern, isValid, toArr, each, isFn, } from '@formily/shared' import { JSXComponent, LifeCycleTypes, FieldDisplayTypes, FieldPatternTypes, FieldDecorator, FieldComponent, IFieldActions, } from '../types' import { locateNode, destroy, initFieldUpdate, getArrayParent, getObjectParent, } from '../shared/internals' import { Form } from './Form' import { Query } from './Query' export class BaseField { title: TextType description: TextType selfDisplay: FieldDisplayTypes selfPattern: FieldPatternTypes initialized: boolean mounted: boolean unmounted: boolean content: any data: any decoratorType: Decorator decoratorProps: Record componentType: Component componentProps: Record designable: boolean address: FormPath path: FormPath form: Form disposers: (() => void)[] = [] actions: IFieldActions = {} locate(address: FormPathPattern) { this.form.fields[address.toString()] = this as any locateNode(this as any, address) } get indexes(): number[] { return this.path.transform(/^\d+$/, (...args) => args.map((index) => Number(index)) ) as number[] } get index() { return this.indexes[this.indexes.length - 1] ?? -1 } get records() { const array = getArrayParent(this) return array?.value } get record() { const obj = getObjectParent(this) if (obj) { return obj.value } const index = this.index const array = getArrayParent(this, index) if (array) { return array.value?.[index] } return this.form.values } get component() { return [this.componentType, this.componentProps] } set component(value: FieldComponent) { const component = toArr(value) this.componentType = component[0] this.componentProps = component[1] || {} } get decorator() { return [this.decoratorType, this.decoratorProps] } set decorator(value: FieldDecorator) { const decorator = toArr(value) this.decoratorType = decorator[0] this.decoratorProps = decorator[1] || {} } get parent() { let parent = this.address.parent() let identifier = parent.toString() while (!this.form.fields[identifier]) { parent = parent.parent() identifier = parent.toString() if (!identifier) return } return this.form.fields[identifier] } get display(): FieldDisplayTypes { const parentDisplay = (this.parent as any)?.display if (parentDisplay && parentDisplay !== 'visible') { if (this.selfDisplay && this.selfDisplay !== 'visible') return this.selfDisplay return parentDisplay } if (isValid(this.selfDisplay)) return this.selfDisplay return parentDisplay || this.form.display || 'visible' } get pattern(): FieldPatternTypes { const parentPattern: FieldPatternTypes = (this.parent as any)?.pattern || this.form.pattern || 'editable' const selfPattern = this.selfPattern if (isValid(selfPattern)) { if (parentPattern === 'readPretty' && selfPattern !== 'editable') { return parentPattern } return selfPattern } return parentPattern } get editable() { return this.pattern === 'editable' } get disabled() { return this.pattern === 'disabled' } get readOnly() { return this.pattern === 'readOnly' } get readPretty() { return this.pattern === 'readPretty' } get hidden() { return this.display === 'hidden' } get visible() { return this.display === 'visible' } get destroyed() { return !this.form.fields[this.address.toString()] } set hidden(hidden: boolean) { if (!isValid(hidden)) return if (hidden) { this.display = 'hidden' } else { this.display = 'visible' } } set visible(visible: boolean) { if (!isValid(visible)) return if (visible) { this.display = 'visible' } else { this.display = 'none' } } set editable(editable: boolean) { if (!isValid(editable)) return if (editable) { this.pattern = 'editable' } else { this.pattern = 'readPretty' } } set readOnly(readOnly: boolean) { if (!isValid(readOnly)) return if (readOnly) { this.pattern = 'readOnly' } else { this.pattern = 'editable' } } set disabled(disabled: boolean) { if (!isValid(disabled)) return if (disabled) { this.pattern = 'disabled' } else { this.pattern = 'editable' } } set readPretty(readPretty: boolean) { if (!isValid(readPretty)) return if (readPretty) { this.pattern = 'readPretty' } else { this.pattern = 'editable' } } set pattern(pattern: FieldPatternTypes) { this.selfPattern = pattern } set display(display: FieldDisplayTypes) { this.selfDisplay = display } setTitle = (title?: TextType) => { this.title = title } setDescription = (description?: TextType) => { this.description = description } setDisplay = (type?: FieldDisplayTypes) => { this.display = type } setPattern = (type?: FieldPatternTypes) => { this.pattern = type } setComponent = ( component?: C, props?: ComponentProps ) => { if (component) { this.componentType = component as any } if (props) { this.componentProps = this.componentProps || {} Object.assign(this.componentProps, props) } } setComponentProps = ( props?: ComponentProps ) => { if (props) { this.componentProps = this.componentProps || {} Object.assign(this.componentProps, props) } } setDecorator = ( component?: D, props?: ComponentProps ) => { if (component) { this.decoratorType = component as any } if (props) { this.decoratorProps = this.decoratorProps || {} Object.assign(this.decoratorProps, props) } } setDecoratorProps = ( props?: ComponentProps ) => { if (props) { this.decoratorProps = this.decoratorProps || {} Object.assign(this.decoratorProps, props) } } setData = (data: any) => { this.data = data } setContent = (content: any) => { this.content = content } onInit = () => { this.initialized = true initFieldUpdate(this as any) this.notify(LifeCycleTypes.ON_FIELD_INIT) } onMount = () => { this.mounted = true this.unmounted = false this.notify(LifeCycleTypes.ON_FIELD_MOUNT) } onUnmount = () => { this.mounted = false this.unmounted = true this.notify(LifeCycleTypes.ON_FIELD_UNMOUNT) } query = (pattern: FormPathPattern | RegExp) => { return new Query({ pattern, base: this.address, form: this.form, }) } notify = (type: LifeCycleTypes, payload?: any) => { return this.form.notify(type, payload ?? this) } dispose = () => { this.disposers.forEach((dispose) => { dispose() }) this.form.removeEffects(this) } destroy = (forceClear = true) => { destroy(this.form.fields, this.address.toString(), forceClear) } match = (pattern: FormPathPattern) => { return FormPath.parse(pattern).matchAliasGroup(this.address, this.path) } inject = (actions: IFieldActions) => { each(actions, (action, key) => { if (isFn(action)) { this.actions[key] = action } }) } invoke = (name: string, ...args: any[]) => { return this.actions[name]?.(...args) } } ================================================ FILE: packages/core/src/models/Field.ts ================================================ import { isValid, isEmpty, toArr, FormPathPattern, isArr, } from '@formily/shared' import { ValidatorTriggerType, parseValidatorDescriptions, } from '@formily/validator' import { define, observable, batch, toJS, action } from '@formily/reactive' import { JSXComponent, LifeCycleTypes, IFieldFeedback, FeedbackMessage, IFieldCaches, IFieldRequests, FieldValidator, FieldDataSource, ISearchFeedback, IFieldProps, IFieldResetOptions, IFieldState, IModelSetter, IModelGetter, } from '../types' import { updateFeedback, queryFeedbacks, allowAssignDefaultValue, queryFeedbackMessages, getValuesFromEvent, createReactions, createStateSetter, createStateGetter, isHTMLInputEvent, setValidatorRule, batchValidate, batchSubmit, batchReset, setValidating, setSubmitting, setLoading, validateSelf, modifySelf, getValidFieldDefaultValue, initializeStart, initializeEnd, createChildrenFeedbackFilter, createReaction, } from '../shared/internals' import { Form } from './Form' import { BaseField } from './BaseField' import { IFormFeedback } from '../types' export class Field< Decorator extends JSXComponent = any, Component extends JSXComponent = any, TextType = any, ValueType = any > extends BaseField { displayName = 'Field' props: IFieldProps loading: boolean validating: boolean submitting: boolean active: boolean visited: boolean selfModified: boolean modified: boolean inputValue: ValueType inputValues: any[] dataSource: FieldDataSource validator: FieldValidator feedbacks: IFieldFeedback[] caches: IFieldCaches = {} requests: IFieldRequests = {} constructor( address: FormPathPattern, props: IFieldProps, form: Form, designable: boolean ) { super() this.form = form this.props = props this.designable = designable initializeStart() this.locate(address) this.initialize() this.makeObservable() this.makeReactive() this.onInit() initializeEnd() } protected initialize() { this.initialized = false this.loading = false this.validating = false this.submitting = false this.selfModified = false this.active = false this.visited = false this.mounted = false this.unmounted = false this.inputValues = [] this.inputValue = null this.feedbacks = [] this.title = this.props.title this.description = this.props.description this.display = this.props.display this.pattern = this.props.pattern this.editable = this.props.editable this.disabled = this.props.disabled this.readOnly = this.props.readOnly this.readPretty = this.props.readPretty this.visible = this.props.visible this.hidden = this.props.hidden this.dataSource = this.props.dataSource this.validator = this.props.validator this.required = this.props.required this.content = this.props.content this.initialValue = this.props.initialValue this.value = this.props.value this.data = this.props.data this.decorator = toArr(this.props.decorator) this.component = toArr(this.props.component) } protected makeObservable() { if (this.designable) return define(this, { path: observable.ref, title: observable.ref, description: observable.ref, dataSource: observable.ref, selfDisplay: observable.ref, selfPattern: observable.ref, loading: observable.ref, validating: observable.ref, submitting: observable.ref, selfModified: observable.ref, modified: observable.ref, active: observable.ref, visited: observable.ref, initialized: observable.ref, mounted: observable.ref, unmounted: observable.ref, inputValue: observable.ref, inputValues: observable.ref, decoratorType: observable.ref, componentType: observable.ref, content: observable.ref, feedbacks: observable.ref, decoratorProps: observable, componentProps: observable, validator: observable.shallow, data: observable.shallow, component: observable.computed, decorator: observable.computed, errors: observable.computed, warnings: observable.computed, successes: observable.computed, valid: observable.computed, invalid: observable.computed, selfErrors: observable.computed, selfWarnings: observable.computed, selfSuccesses: observable.computed, selfValid: observable.computed, selfInvalid: observable.computed, validateStatus: observable.computed, value: observable.computed, initialValue: observable.computed, display: observable.computed, pattern: observable.computed, required: observable.computed, hidden: observable.computed, visible: observable.computed, disabled: observable.computed, readOnly: observable.computed, readPretty: observable.computed, editable: observable.computed, indexes: observable.computed, setDisplay: action, setTitle: action, setDescription: action, setDataSource: action, setValue: action, setPattern: action, setInitialValue: action, setLoading: action, setValidating: action, setFeedback: action, setSelfErrors: action, setSelfWarnings: action, setSelfSuccesses: action, setValidator: action, setRequired: action, setComponent: action, setComponentProps: action, setDecorator: action, setDecoratorProps: action, setData: action, setContent: action, validate: action, reset: action, onInit: batch, onInput: batch, onMount: batch, onUnmount: batch, onFocus: batch, onBlur: batch, }) } protected makeReactive() { if (this.designable) return this.disposers.push( createReaction( () => this.value, (value) => { this.notify(LifeCycleTypes.ON_FIELD_VALUE_CHANGE) if (isValid(value)) { if (this.selfModified && !this.caches.inputting) { validateSelf(this) } if (!isEmpty(value) && this.display === 'none') { this.caches.value = toJS(value) this.form.deleteValuesIn(this.path) } } } ), createReaction( () => this.initialValue, () => { this.notify(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE) } ), createReaction( () => this.display, (display) => { const value = this.value if (display !== 'none') { if (value === undefined && this.caches.value !== undefined) { this.setValue(this.caches.value) this.caches.value = undefined } } else { this.caches.value = toJS(value) ?? toJS(this.initialValue) this.form.deleteValuesIn(this.path) } if (display === 'none' || display === 'hidden') { this.setFeedback({ type: 'error', messages: [], }) } } ), createReaction( () => this.pattern, (pattern) => { if (pattern !== 'editable') { this.setFeedback({ type: 'error', messages: [], }) } } ) ) createReactions(this) } get selfErrors(): FeedbackMessage { return queryFeedbackMessages(this, { type: 'error', }) } get errors(): IFormFeedback[] { return this.form.errors.filter(createChildrenFeedbackFilter(this)) } get selfWarnings(): FeedbackMessage { return queryFeedbackMessages(this, { type: 'warning', }) } get warnings(): IFormFeedback[] { return this.form.warnings.filter(createChildrenFeedbackFilter(this)) } get selfSuccesses(): FeedbackMessage { return queryFeedbackMessages(this, { type: 'success', }) } get successes(): IFormFeedback[] { return this.form.successes.filter(createChildrenFeedbackFilter(this)) } get selfValid() { return !this.selfErrors.length } get valid() { return !this.errors.length } get selfInvalid() { return !this.selfValid } get invalid() { return !this.valid } get value(): ValueType { return this.form.getValuesIn(this.path) } get initialValue(): ValueType { return this.form.getInitialValuesIn(this.path) } get required() { const validators = isArr(this.validator) ? this.validator : parseValidatorDescriptions(this.validator) return validators.some((desc) => !!desc?.['required']) } get validateStatus() { if (this.validating) return 'validating' if (this.selfInvalid) return 'error' if (this.selfWarnings.length) return 'warning' if (this.selfSuccesses.length) return 'success' } set required(required: boolean) { if (this.required === required) return this.setValidatorRule('required', required) } set value(value: ValueType) { this.setValue(value) } set initialValue(initialValue: ValueType) { this.setInitialValue(initialValue) } set selfErrors(messages: FeedbackMessage) { this.setFeedback({ type: 'error', code: 'EffectError', messages, }) } set selfWarnings(messages: FeedbackMessage) { this.setFeedback({ type: 'warning', code: 'EffectWarning', messages, }) } set selfSuccesses(messages: FeedbackMessage) { this.setFeedback({ type: 'success', code: 'EffectSuccess', messages, }) } setDataSource = (dataSource?: FieldDataSource) => { this.dataSource = dataSource } setFeedback = (feedback?: IFieldFeedback) => { updateFeedback(this, feedback) } setSelfErrors = (messages?: FeedbackMessage) => { this.selfErrors = messages } setSelfWarnings = (messages?: FeedbackMessage) => { this.selfWarnings = messages } setSelfSuccesses = (messages?: FeedbackMessage) => { this.selfSuccesses = messages } setValidator = (validator?: FieldValidator) => { this.validator = validator } setValidatorRule = (name: string, value: any) => { setValidatorRule(this, name, value) } setRequired = (required?: boolean) => { this.required = required } setValue = (value?: ValueType) => { if (this.destroyed) return if (!this.initialized) { if (this.display === 'none') { this.caches.value = value return } value = getValidFieldDefaultValue(value, this.initialValue) if (!allowAssignDefaultValue(this.value, value) && !this.designable) { return } } this.form.setValuesIn(this.path, value) } setInitialValue = (initialValue?: ValueType) => { if (this.destroyed) return if (!this.initialized) { if ( !allowAssignDefaultValue(this.initialValue, initialValue) && !this.designable ) { return } } this.form.setInitialValuesIn(this.path, initialValue) } setLoading = (loading?: boolean) => { setLoading(this, loading) } setValidating = (validating?: boolean) => { setValidating(this, validating) } setSubmitting = (submitting?: boolean) => { setSubmitting(this, submitting) } setState: IModelSetter = createStateSetter(this) getState: IModelGetter = createStateGetter(this) onInput = async (...args: any[]) => { const isHTMLInputEventFromSelf = (args: any[]) => isHTMLInputEvent(args[0]) && 'currentTarget' in args[0] ? args[0]?.target === args[0]?.currentTarget : true const getValues = (args: any[]) => { if (args[0]?.target) { if (!isHTMLInputEvent(args[0])) return args } return getValuesFromEvent(args) } if (!isHTMLInputEventFromSelf(args)) return const values = getValues(args) const value = values[0] this.caches.inputting = true this.inputValue = value this.inputValues = values this.value = value this.modify() this.notify(LifeCycleTypes.ON_FIELD_INPUT_VALUE_CHANGE) this.notify(LifeCycleTypes.ON_FORM_INPUT_CHANGE, this.form) await validateSelf(this, 'onInput') this.caches.inputting = false } onFocus = async (...args: any[]) => { if (args[0]?.target) { if (!isHTMLInputEvent(args[0], false)) return } this.active = true this.visited = true await validateSelf(this, 'onFocus') } onBlur = async (...args: any[]) => { if (args[0]?.target) { if (!isHTMLInputEvent(args[0], false)) return } this.active = false await validateSelf(this, 'onBlur') } validate = (triggerType?: ValidatorTriggerType) => { return batchValidate(this, `${this.address}.**`, triggerType) } submit = (onSubmit?: (values: any) => Promise | void): Promise => { return batchSubmit(this, onSubmit) } reset = (options?: IFieldResetOptions) => { return batchReset(this, `${this.address}.**`, options) } queryFeedbacks = (search?: ISearchFeedback): IFieldFeedback[] => { return queryFeedbacks(this, search) } modify = () => modifySelf(this) } ================================================ FILE: packages/core/src/models/Form.ts ================================================ import { define, observable, batch, action, observe } from '@formily/reactive' import { FormPath, FormPathPattern, isValid, uid, globalThisPolyfill, merge, isPlainObj, isArr, isObj, } from '@formily/shared' import { Heart } from './Heart' import { Field } from './Field' import { JSXComponent, LifeCycleTypes, HeartSubscriber, FormPatternTypes, IFormRequests, IFormFeedback, ISearchFeedback, IFormGraph, IFormProps, IFieldResetOptions, IFormFields, IFieldFactoryProps, IVoidFieldFactoryProps, IFormState, IModelGetter, IModelSetter, IFieldStateGetter, IFieldStateSetter, FormDisplayTypes, IFormMergeStrategy, } from '../types' import { createStateGetter, createStateSetter, createBatchStateSetter, createBatchStateGetter, triggerFormInitialValuesChange, triggerFormValuesChange, batchValidate, batchReset, batchSubmit, setValidating, setSubmitting, setLoading, getValidFormValues, } from '../shared/internals' import { isVoidField } from '../shared/checkers' import { runEffects } from '../shared/effective' import { ArrayField } from './ArrayField' import { ObjectField } from './ObjectField' import { VoidField } from './VoidField' import { Query } from './Query' import { Graph } from './Graph' const DEV_TOOLS_HOOK = '__FORMILY_DEV_TOOLS_HOOK__' export class Form { displayName = 'Form' id: string initialized: boolean validating: boolean submitting: boolean loading: boolean modified: boolean pattern: FormPatternTypes display: FormDisplayTypes values: ValueType initialValues: ValueType mounted: boolean unmounted: boolean props: IFormProps heart: Heart graph: Graph fields: IFormFields = {} requests: IFormRequests = {} indexes: Record = {} disposers: (() => void)[] = [] constructor(props: IFormProps) { this.initialize(props) this.makeObservable() this.makeReactive() this.makeValues() this.onInit() } protected initialize(props: IFormProps) { this.id = uid() this.props = { ...props } this.initialized = false this.submitting = false this.validating = false this.loading = false this.modified = false this.mounted = false this.unmounted = false this.display = this.props.display || 'visible' this.pattern = this.props.pattern || 'editable' this.editable = this.props.editable this.disabled = this.props.disabled this.readOnly = this.props.readOnly this.readPretty = this.props.readPretty this.visible = this.props.visible this.hidden = this.props.hidden this.graph = new Graph(this) this.heart = new Heart({ lifecycles: this.lifecycles, context: this, }) } protected makeValues() { this.values = getValidFormValues(this.props.values) this.initialValues = getValidFormValues(this.props.initialValues) } protected makeObservable() { define(this, { fields: observable.shallow, indexes: observable.shallow, initialized: observable.ref, validating: observable.ref, submitting: observable.ref, loading: observable.ref, modified: observable.ref, pattern: observable.ref, display: observable.ref, mounted: observable.ref, unmounted: observable.ref, values: observable, initialValues: observable, valid: observable.computed, invalid: observable.computed, errors: observable.computed, warnings: observable.computed, successes: observable.computed, hidden: observable.computed, visible: observable.computed, editable: observable.computed, readOnly: observable.computed, readPretty: observable.computed, disabled: observable.computed, setValues: action, setValuesIn: action, setInitialValues: action, setInitialValuesIn: action, setPattern: action, setDisplay: action, setState: action, deleteInitialValuesIn: action, deleteValuesIn: action, setSubmitting: action, setValidating: action, reset: action, submit: action, validate: action, onMount: batch, onUnmount: batch, onInit: batch, }) } protected makeReactive() { this.disposers.push( observe( this, (change) => { triggerFormInitialValuesChange(this, change) triggerFormValuesChange(this, change) }, true ) ) } get valid() { return !this.invalid } get invalid() { return this.errors.length > 0 } get errors() { return this.queryFeedbacks({ type: 'error', }) } get warnings() { return this.queryFeedbacks({ type: 'warning', }) } get successes() { return this.queryFeedbacks({ type: 'success', }) } get lifecycles() { return runEffects(this, this.props.effects) } get hidden() { return this.display === 'hidden' } get visible() { return this.display === 'visible' } set hidden(hidden: boolean) { if (!isValid(hidden)) return if (hidden) { this.display = 'hidden' } else { this.display = 'visible' } } set visible(visible: boolean) { if (!isValid(visible)) return if (visible) { this.display = 'visible' } else { this.display = 'none' } } get editable() { return this.pattern === 'editable' } set editable(editable) { if (!isValid(editable)) return if (editable) { this.pattern = 'editable' } else { this.pattern = 'readPretty' } } get readOnly() { return this.pattern === 'readOnly' } set readOnly(readOnly) { if (!isValid(readOnly)) return if (readOnly) { this.pattern = 'readOnly' } else { this.pattern = 'editable' } } get disabled() { return this.pattern === 'disabled' } set disabled(disabled) { if (!isValid(disabled)) return if (disabled) { this.pattern = 'disabled' } else { this.pattern = 'editable' } } get readPretty() { return this.pattern === 'readPretty' } set readPretty(readPretty) { if (!isValid(readPretty)) return if (readPretty) { this.pattern = 'readPretty' } else { this.pattern = 'editable' } } /** 创建字段 **/ createField = < Decorator extends JSXComponent, Component extends JSXComponent >( props: IFieldFactoryProps ): Field => { const address = FormPath.parse(props.basePath).concat(props.name) const identifier = address.toString() if (!identifier) return if (!this.fields[identifier] || this.props.designable) { batch(() => { new Field(address, props, this, this.props.designable) }) this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE) } return this.fields[identifier] as any } createArrayField = < Decorator extends JSXComponent, Component extends JSXComponent >( props: IFieldFactoryProps ): ArrayField => { const address = FormPath.parse(props.basePath).concat(props.name) const identifier = address.toString() if (!identifier) return if (!this.fields[identifier] || this.props.designable) { batch(() => { new ArrayField( address, { ...props, value: isArr(props.value) ? props.value : [], }, this, this.props.designable ) }) this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE) } return this.fields[identifier] as any } createObjectField = < Decorator extends JSXComponent, Component extends JSXComponent >( props: IFieldFactoryProps ): ObjectField => { const address = FormPath.parse(props.basePath).concat(props.name) const identifier = address.toString() if (!identifier) return if (!this.fields[identifier] || this.props.designable) { batch(() => { new ObjectField( address, { ...props, value: isObj(props.value) ? props.value : {}, }, this, this.props.designable ) }) this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE) } return this.fields[identifier] as any } createVoidField = < Decorator extends JSXComponent, Component extends JSXComponent >( props: IVoidFieldFactoryProps ): VoidField => { const address = FormPath.parse(props.basePath).concat(props.name) const identifier = address.toString() if (!identifier) return if (!this.fields[identifier] || this.props.designable) { batch(() => { new VoidField(address, props, this, this.props.designable) }) this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE) } return this.fields[identifier] as any } /** 状态操作模型 **/ setValues = (values: any, strategy: IFormMergeStrategy = 'merge') => { if (!isPlainObj(values)) return if (strategy === 'merge' || strategy === 'deepMerge') { merge(this.values, values, { // never reach arrayMerge: (target, source) => source, assign: true, }) } else if (strategy === 'shallowMerge') { Object.assign(this.values, values) } else { this.values = values as any } } setInitialValues = ( initialValues: any, strategy: IFormMergeStrategy = 'merge' ) => { if (!isPlainObj(initialValues)) return if (strategy === 'merge' || strategy === 'deepMerge') { merge(this.initialValues, initialValues, { // never reach arrayMerge: (target, source) => source, assign: true, }) } else if (strategy === 'shallowMerge') { Object.assign(this.initialValues, initialValues) } else { this.initialValues = initialValues as any } } setValuesIn = (pattern: FormPathPattern, value: any) => { FormPath.setIn(this.values, pattern, value) } deleteValuesIn = (pattern: FormPathPattern) => { FormPath.deleteIn(this.values, pattern) } existValuesIn = (pattern: FormPathPattern) => { return FormPath.existIn(this.values, pattern) } getValuesIn = (pattern: FormPathPattern) => { return FormPath.getIn(this.values, pattern) } setInitialValuesIn = (pattern: FormPathPattern, initialValue: any) => { FormPath.setIn(this.initialValues, pattern, initialValue) } deleteInitialValuesIn = (pattern: FormPathPattern) => { FormPath.deleteIn(this.initialValues, pattern) } existInitialValuesIn = (pattern: FormPathPattern) => { return FormPath.existIn(this.initialValues, pattern) } getInitialValuesIn = (pattern: FormPathPattern) => { return FormPath.getIn(this.initialValues, pattern) } setLoading = (loading: boolean) => { setLoading(this, loading) } setSubmitting = (submitting: boolean) => { setSubmitting(this, submitting) } setValidating = (validating: boolean) => { setValidating(this, validating) } setDisplay = (display: FormDisplayTypes) => { this.display = display } setPattern = (pattern: FormPatternTypes) => { this.pattern = pattern } addEffects = (id: any, effects: IFormProps['effects']) => { if (!this.heart.hasLifeCycles(id)) { this.heart.addLifeCycles(id, runEffects(this, effects)) } } removeEffects = (id: any) => { this.heart.removeLifeCycles(id) } setEffects = (effects: IFormProps['effects']) => { this.heart.setLifeCycles(runEffects(this, effects)) } clearErrors = (pattern: FormPathPattern = '*') => { this.query(pattern).forEach((field) => { if (!isVoidField(field)) { field.setFeedback({ type: 'error', messages: [], }) } }) } clearWarnings = (pattern: FormPathPattern = '*') => { this.query(pattern).forEach((field) => { if (!isVoidField(field)) { field.setFeedback({ type: 'warning', messages: [], }) } }) } clearSuccesses = (pattern: FormPathPattern = '*') => { this.query(pattern).forEach((field) => { if (!isVoidField(field)) { field.setFeedback({ type: 'success', messages: [], }) } }) } query = (pattern: FormPathPattern): Query => { return new Query({ pattern, base: '', form: this, }) } queryFeedbacks = (search: ISearchFeedback): IFormFeedback[] => { return this.query(search.address || search.path || '*').reduce( (messages, field) => { if (isVoidField(field)) return messages return messages.concat( field .queryFeedbacks(search) .map((feedback) => ({ ...feedback, address: field.address.toString(), path: field.path.toString(), })) .filter((feedback) => feedback.messages.length > 0) ) }, [] ) } notify = (type: string, payload?: any) => { this.heart.publish(type, payload ?? this) } subscribe = (subscriber?: HeartSubscriber) => { return this.heart.subscribe(subscriber) } unsubscribe = (id: number) => { this.heart.unsubscribe(id) } /**事件钩子**/ onInit = () => { this.initialized = true this.notify(LifeCycleTypes.ON_FORM_INIT) } onMount = () => { this.mounted = true this.notify(LifeCycleTypes.ON_FORM_MOUNT) if (globalThisPolyfill[DEV_TOOLS_HOOK] && !this.props.designable) { globalThisPolyfill[DEV_TOOLS_HOOK].inject(this.id, this) } } onUnmount = () => { this.notify(LifeCycleTypes.ON_FORM_UNMOUNT) this.query('*').forEach((field) => field.destroy(false)) this.disposers.forEach((dispose) => dispose()) this.unmounted = true this.indexes = {} this.heart.clear() if (globalThisPolyfill[DEV_TOOLS_HOOK] && !this.props.designable) { globalThisPolyfill[DEV_TOOLS_HOOK].unmount(this.id) } } setState: IModelSetter> = createStateSetter(this) getState: IModelGetter> = createStateGetter(this) setFormState: IModelSetter> = createStateSetter(this) getFormState: IModelGetter> = createStateGetter(this) setFieldState: IFieldStateSetter = createBatchStateSetter(this) getFieldState: IFieldStateGetter = createBatchStateGetter(this) getFormGraph = () => { return this.graph.getGraph() } setFormGraph = (graph: IFormGraph) => { this.graph.setGraph(graph) } clearFormGraph = (pattern: FormPathPattern = '*', forceClear = true) => { this.query(pattern).forEach((field) => { field.destroy(forceClear) }) } validate = (pattern: FormPathPattern = '*') => { return batchValidate(this, pattern) } submit = ( onSubmit?: (values: ValueType) => Promise | void ): Promise => { return batchSubmit(this, onSubmit) } reset = (pattern: FormPathPattern = '*', options?: IFieldResetOptions) => { return batchReset(this, pattern, options) } } ================================================ FILE: packages/core/src/models/Graph.ts ================================================ import { define, batch } from '@formily/reactive' import { each, FormPath } from '@formily/shared' import { IFormGraph } from '../types' import { Form } from './Form' import { isFormState, isFieldState, isArrayFieldState, isObjectFieldState, } from '../shared/checkers' export class Graph { form: Form constructor(form: Form) { this.form = form define(this, { setGraph: batch, }) } getGraph = (): IFormGraph => { const graph = {} graph[''] = this.form.getState() each(this.form.fields, (field: any, identifier) => { graph[identifier] = field.getState() }) return graph } setGraph = (graph: IFormGraph) => { const form = this.form const createField = (identifier: string, state: any) => { const address = FormPath.parse(identifier) const name = address.segments[address.segments.length - 1] const basePath = address.parent() if (isFieldState(state)) { return this.form.createField({ name, basePath }) } else if (isArrayFieldState(state)) { return this.form.createArrayField({ name, basePath }) } else if (isObjectFieldState(state)) { return this.form.createObjectField({ name, basePath }) } else { return this.form.createVoidField({ name, basePath }) } } each(graph, (state, address) => { if (isFormState(state)) { form.setState(state) } else { const field = form.fields[address] if (field) { field.setState(state) } else { createField(address, state).setState(state) } } }) } } ================================================ FILE: packages/core/src/models/Heart.ts ================================================ import { isStr, isArr, Subscribable } from '@formily/shared' import { LifeCycle } from './LifeCycle' import { IHeartProps } from '../types' export class Heart extends Subscribable { lifecycles: LifeCycle[] = [] outerLifecycles: Map[]> = new Map() context: Context constructor({ lifecycles, context }: IHeartProps = {}) { super() this.lifecycles = this.buildLifeCycles(lifecycles || []) this.context = context } buildLifeCycles = (lifecycles: LifeCycle[]) => { return lifecycles.reduce((buf, item) => { if (item instanceof LifeCycle) { return buf.concat(item) } else { if (isArr(item)) { return this.buildLifeCycles(item) } else if (typeof item === 'object') { this.context = item return buf } return buf } }, []) } addLifeCycles = (id: any, lifecycles: LifeCycle[] = []) => { const observers = this.buildLifeCycles(lifecycles) if (observers.length) { this.outerLifecycles.set(id, observers) } } hasLifeCycles = (id: any) => { return this.outerLifecycles.has(id) } removeLifeCycles = (id: any) => { this.outerLifecycles.delete(id) } setLifeCycles = (lifecycles: LifeCycle[] = []) => { this.lifecycles = this.buildLifeCycles(lifecycles) } publish = (type: any, payload?: P, context?: C) => { if (isStr(type)) { this.lifecycles.forEach((lifecycle) => { lifecycle.notify(type, payload, context || this.context) }) this.outerLifecycles.forEach((lifecycles) => { lifecycles.forEach((lifecycle) => { lifecycle.notify(type, payload, context || this.context) }) }) this.notify({ type, payload, }) } } clear = () => { this.lifecycles = [] this.outerLifecycles.clear() this.unsubscribe() } } ================================================ FILE: packages/core/src/models/LifeCycle.ts ================================================ import { isFn, isStr, each } from '@formily/shared' import { LifeCycleHandler, LifeCyclePayload } from '../types' type LifeCycleParams = Array< | string | LifeCycleHandler | { [key: string]: LifeCycleHandler } > export class LifeCycle { private listener: LifeCyclePayload constructor(...params: LifeCycleParams) { this.listener = this.buildListener(params) } buildListener = (params: any[]) => { return function (payload: { type: string; payload: Payload }, ctx: any) { for (let index = 0; index < params.length; index++) { let item = params[index] if (isFn(item)) { item.call(this, payload, ctx) } else if (isStr(item) && isFn(params[index + 1])) { if (item === payload.type) { params[index + 1].call(this, payload.payload, ctx) } index++ } else { each(item, (handler, type) => { if (isFn(handler) && isStr(type)) { if (type === payload.type) { handler.call(this, payload.payload, ctx) return false } } }) } } } } notify = (type: any, payload?: Payload, ctx?: any) => { if (isStr(type)) { this.listener.call(ctx, { type, payload }, ctx) } } } ================================================ FILE: packages/core/src/models/ObjectField.ts ================================================ import { reaction } from '@formily/reactive' import { cleanupObjectChildren } from '../shared/internals' import { JSXComponent, IFieldProps, FormPathPattern } from '../types' import { Field } from './Field' import { Form } from './Form' export class ObjectField< Decorator extends JSXComponent = any, Component extends JSXComponent = any > extends Field> { displayName = 'ObjectField' private additionalProperties: string[] = [] constructor( address: FormPathPattern, props: IFieldProps, form: Form, designable: boolean ) { super(address, props, form, designable) this.makeAutoCleanable() } protected makeAutoCleanable() { this.disposers.push( reaction( () => Object.keys(this.value || {}), (newKeys) => { const filterKeys = this.additionalProperties.filter( (key) => !newKeys.includes(key) ) cleanupObjectChildren(this, filterKeys) } ) ) } addProperty = (key: string, value: any) => { this.form.setValuesIn(this.path.concat(key), value) this.additionalProperties.push(key) return this.onInput(this.value) } removeProperty = (key: string) => { this.form.deleteValuesIn(this.path.concat(key)) this.additionalProperties.splice(this.additionalProperties.indexOf(key), 1) return this.onInput(this.value) } existProperty = (key: string) => { return this.form.existValuesIn(this.path.concat(key)) } } ================================================ FILE: packages/core/src/models/Query.ts ================================================ import { FormPath, isFn, each, FormPathPattern } from '@formily/shared' import { buildDataPath } from '../shared/internals' import { GeneralField, IGeneralFieldState, IQueryProps } from '../types' import { Form } from './Form' const output = ( field: GeneralField, taker: (field: GeneralField, address: FormPath) => any ) => { if (!field) return if (isFn(taker)) { return taker(field, field.address) } return field } const takeMatchPattern = (form: Form, pattern: FormPath) => { const identifier = pattern.toString() const indexIdentifier = form.indexes[identifier] const absoluteField = form.fields[identifier] const indexField = form.fields[indexIdentifier] if (absoluteField) { return identifier } else if (indexField) { return indexIdentifier } } export class Query { private pattern: FormPath private addresses: string[] = [] private form: Form constructor(props: IQueryProps) { this.pattern = FormPath.parse(props.pattern, props.base) this.form = props.form if (!this.pattern.isMatchPattern) { const matched = takeMatchPattern( this.form, this.pattern.haveRelativePattern ? buildDataPath(props.form.fields, this.pattern) : this.pattern ) if (matched) { this.addresses = [matched] } } else { each(this.form.fields, (field, address) => { if (!field) { delete this.form.fields[address] return } if (field.match(this.pattern)) { this.addresses.push(address) } }) } } take(): GeneralField | undefined take( getter: (field: GeneralField, address: FormPath) => Result ): Result take(taker?: any): any { return output(this.form.fields[this.addresses[0]], taker) } map(): GeneralField[] map( iterator?: (field: GeneralField, address: FormPath) => Result ): Result[] map(iterator?: any): any { return this.addresses.map((address) => output(this.form.fields[address], iterator) ) } forEach( iterator: (field: GeneralField, address: FormPath) => Result ) { return this.addresses.forEach((address) => output(this.form.fields[address], iterator) ) } reduce( reducer: (value: Result, field: GeneralField, address: FormPath) => Result, initial?: Result ): Result { return this.addresses.reduce( (value, address) => output(this.form.fields[address], (field, address) => reducer(value, field, address) ), initial ) } get(key: K): IGeneralFieldState[K] { const results: any = this.take() if (results) { return results[key] } } getIn(pattern?: FormPathPattern) { return FormPath.getIn(this.take(), pattern) } value() { return this.get('value') } initialValue() { return this.get('initialValue') } } ================================================ FILE: packages/core/src/models/VoidField.ts ================================================ import { toArr, FormPathPattern } from '@formily/shared' import { define, observable, batch, action } from '@formily/reactive' import { createReactions, createStateSetter, createStateGetter, initializeStart, initializeEnd, } from '../shared/internals' import { IModelSetter, IModelGetter, IVoidFieldProps, IVoidFieldState, } from '../types' import { Form } from './Form' import { BaseField } from './BaseField' export class VoidField< Decorator = any, Component = any, TextType = any > extends BaseField { displayName: 'VoidField' = 'VoidField' props: IVoidFieldProps constructor( address: FormPathPattern, props: IVoidFieldProps, form: Form, designable: boolean ) { super() this.form = form this.props = props this.designable = designable initializeStart() this.locate(address) this.initialize() this.makeObservable() this.makeReactive() this.onInit() initializeEnd() } protected initialize() { this.mounted = false this.unmounted = false this.initialized = false this.title = this.props.title this.description = this.props.description this.pattern = this.props.pattern this.display = this.props.display this.hidden = this.props.hidden this.editable = this.props.editable this.disabled = this.props.disabled this.readOnly = this.props.readOnly this.readPretty = this.props.readPretty this.visible = this.props.visible this.content = this.props.content this.data = this.props.data this.decorator = toArr(this.props.decorator) this.component = toArr(this.props.component) } protected makeObservable() { if (this.designable) return define(this, { path: observable.ref, title: observable.ref, description: observable.ref, selfDisplay: observable.ref, selfPattern: observable.ref, initialized: observable.ref, mounted: observable.ref, unmounted: observable.ref, decoratorType: observable.ref, componentType: observable.ref, content: observable.ref, data: observable.shallow, decoratorProps: observable, componentProps: observable, display: observable.computed, pattern: observable.computed, hidden: observable.computed, visible: observable.computed, disabled: observable.computed, readOnly: observable.computed, readPretty: observable.computed, editable: observable.computed, component: observable.computed, decorator: observable.computed, indexes: observable.computed, setTitle: action, setDescription: action, setDisplay: action, setPattern: action, setComponent: action, setComponentProps: action, setDecorator: action, setDecoratorProps: action, setData: action, setContent: action, onInit: batch, onMount: batch, onUnmount: batch, }) } protected makeReactive() { if (this.designable) return createReactions(this) } setState: IModelSetter = createStateSetter(this) getState: IModelGetter = createStateGetter(this) } ================================================ FILE: packages/core/src/models/index.ts ================================================ export * from './Heart' export * from './LifeCycle' export * from './Graph' export * from './Query' export * from './Form' export * from './Field' export * from './ArrayField' export * from './ObjectField' export * from './VoidField' ================================================ FILE: packages/core/src/models/types.ts ================================================ export type { Form } from './Form' export type { Field } from './Field' export type { Query } from './Query' export type { Heart } from './Heart' export type { Graph } from './Graph' export type { LifeCycle } from './LifeCycle' export type { ArrayField } from './ArrayField' export type { ObjectField } from './ObjectField' export type { VoidField } from './VoidField' ================================================ FILE: packages/core/src/shared/checkers.ts ================================================ import { isFn } from '@formily/shared' import { DataField, JSXComponent } from '..' import { Form, Field, ArrayField, ObjectField, VoidField, Query, } from '../models' import { IFormState, IFieldState, IVoidFieldState, GeneralField, IGeneralFieldState, } from '../types' export const isForm = (node: any): node is Form => { return node instanceof Form } export const isGeneralField = (node: any): node is GeneralField => { return node instanceof Field || node instanceof VoidField } export const isField = < Decorator extends JSXComponent = any, Component extends JSXComponent = any, TextType = any, ValueType = any >( node: any ): node is Field => { return node instanceof Field } export const isArrayField = < Decorator extends JSXComponent = any, Component extends JSXComponent = any >( node: any ): node is ArrayField => { return node instanceof ArrayField } export const isObjectField = < Decorator extends JSXComponent = any, Component extends JSXComponent = any >( node: any ): node is ObjectField => { return node instanceof ObjectField } export const isVoidField = ( node: any ): node is VoidField => { return node instanceof VoidField } export const isFormState = = any>( state: any ): state is IFormState => { if (isFn(state?.initialize)) return false return state?.displayName === 'Form' } export const isFieldState = (state: any): state is IFieldState => { if (isFn(state?.initialize)) return false return state?.displayName === 'Field' } export const isGeneralFieldState = (node: any): node is IGeneralFieldState => { if (isFn(node?.initialize)) return false return node?.displayName?.indexOf('Field') > -1 } export const isArrayFieldState = (state: any): state is IFieldState => { if (isFn(state?.initialize)) return false return state?.displayName === 'ArrayField' } export const isDataField = (node: any): node is DataField => { return isField(node) || isArrayField(node) || isObjectField(node) } export const isDataFieldState = (node: any) => { return ( isFieldState(node) || isObjectFieldState(node) || isArrayFieldState(node) ) } export const isObjectFieldState = (state: any): state is IFieldState => { if (isFn(state?.initialize)) return false return state?.displayName === 'ObjectField' } export const isVoidFieldState = (state: any): state is IVoidFieldState => { if (isFn(state?.initialize)) return false return state?.displayName === 'VoidField' } export const isQuery = (query: any): query is Query => { return query && query instanceof Query } ================================================ FILE: packages/core/src/shared/constants.ts ================================================ export const ReservedProperties = { form: true, parent: true, props: true, caches: true, requests: true, disposers: true, heart: true, graph: true, indexes: true, fields: true, lifecycles: true, componentType: true, componentProps: true, decoratorType: true, decoratorProps: true, } export const ReadOnlyProperties = { address: true, path: true, valid: true, invalid: true, selfValid: true, selfInvalid: true, errors: true, successes: true, warnings: true, validateStatus: true, } const SELF_DISPLAY = 'selfDisplay' const SELF_PATTERN = 'selfPattern' export const MutuallyExclusiveProperties = { pattern: SELF_PATTERN, editable: SELF_PATTERN, readOnly: SELF_PATTERN, readPretty: SELF_PATTERN, disabled: SELF_PATTERN, display: SELF_DISPLAY, hidden: SELF_DISPLAY, visible: SELF_DISPLAY, } export const RESPONSE_REQUEST_DURATION = 100 export const GlobalState = { lifecycles: [], context: [], effectStart: false, effectEnd: false, initializing: false, } export const NumberIndexReg = /^\.(\d+)/ ================================================ FILE: packages/core/src/shared/effective.ts ================================================ import { isFn, isValid } from '@formily/shared' import { LifeCycle, Form } from '../models' import { AnyFunction } from '../types' import { isForm } from './checkers' import { GlobalState } from './constants' export const createEffectHook = < F extends (payload: any, ...ctxs: any[]) => AnyFunction >( type: string, callback?: F ) => { return (...args: Parameters>) => { if (GlobalState.effectStart) { GlobalState.lifecycles.push( new LifeCycle(type, (payload, ctx) => { if (isFn(callback)) { callback(payload, ctx, ...GlobalState.context)(...args) } }) ) } else { throw new Error( 'Effect hooks cannot be used in asynchronous function body' ) } } } export const createEffectContext = (defaultValue?: T) => { let index: number return { provide(value?: T) { if (GlobalState.effectStart) { index = GlobalState.context.length GlobalState.context[index] = isValid(value) ? value : defaultValue } else { throw new Error( 'Provide method cannot be used in asynchronous function body' ) } }, consume(): T { if (!GlobalState.effectStart) { throw new Error( 'Consume method cannot be used in asynchronous function body' ) } return GlobalState.context[index] }, } } const FormEffectContext = createEffectContext
() export const useEffectForm = FormEffectContext.consume export const runEffects = ( context?: Context, ...args: ((context: Context) => void)[] ): LifeCycle[] => { GlobalState.lifecycles = [] GlobalState.context = [] GlobalState.effectStart = true GlobalState.effectEnd = false if (isForm(context)) { FormEffectContext.provide(context) } args.forEach((effects) => { if (isFn(effects)) { effects(context) } }) GlobalState.context = [] GlobalState.effectStart = false GlobalState.effectEnd = true return GlobalState.lifecycles } ================================================ FILE: packages/core/src/shared/externals.ts ================================================ import { FormPath } from '@formily/shared' import { Form } from '../models' import { IFormProps } from '../types' import { getValidateLocaleIOSCode, getLocaleByPath, setValidateLanguage, registerValidateFormats, registerValidateLocale, registerValidateMessageTemplateEngine, registerValidateRules, } from '@formily/validator' import { createEffectHook, createEffectContext, useEffectForm, } from './effective' import { isArrayField, isArrayFieldState, isDataField, isDataFieldState, isField, isFieldState, isForm, isFormState, isGeneralField, isGeneralFieldState, isObjectField, isObjectFieldState, isQuery, isVoidField, isVoidFieldState, } from './checkers' const createForm = (options?: IFormProps) => { return new Form(options) } export { FormPath, createForm, isArrayField, isArrayFieldState, isDataField, isDataFieldState, isField, isFieldState, isForm, isFormState, isGeneralField, isGeneralFieldState, isObjectField, isObjectFieldState, isQuery, isVoidField, isVoidFieldState, getValidateLocaleIOSCode, getLocaleByPath, setValidateLanguage, registerValidateFormats, registerValidateLocale, registerValidateMessageTemplateEngine, registerValidateRules, createEffectHook, createEffectContext, useEffectForm, } ================================================ FILE: packages/core/src/shared/internals.ts ================================================ import { FormPath, FormPathPattern, each, pascalCase, isFn, isValid, isUndef, isEmpty, isPlainObj, isNumberLike, clone, toArr, } from '@formily/shared' import { ValidatorTriggerType, validate, parseValidatorDescriptions, } from '@formily/validator' import { autorun, batch, contains, toJS, isObservable, DataChange, reaction, untracked, } from '@formily/reactive' import { Field, ArrayField, Form, ObjectField } from '../models' import { ISpliceArrayStateProps, IExchangeArrayStateProps, IFieldResetOptions, ISearchFeedback, IFieldFeedback, INodePatch, GeneralField, IFormFeedback, LifeCycleTypes, FieldMatchPattern, FieldFeedbackTypes, } from '../types' import { isArrayField, isObjectField, isGeneralField, isDataField, isForm, isQuery, isVoidField, } from './externals' import { RESPONSE_REQUEST_DURATION, ReservedProperties, MutuallyExclusiveProperties, NumberIndexReg, GlobalState, ReadOnlyProperties, } from './constants' import { BaseField } from '../models/BaseField' const hasOwnProperty = Object.prototype.hasOwnProperty const notify = ( target: Form | Field, formType: LifeCycleTypes, fieldType: LifeCycleTypes ) => { if (isForm(target)) { target.notify(formType) } else { target.notify(fieldType) } } export const isHTMLInputEvent = (event: any, stopPropagation = true) => { if (event?.target) { if ( typeof event.target === 'object' && ('value' in event.target || 'checked' in event.target) ) return true if (stopPropagation) event.stopPropagation?.() } return false } export const getValuesFromEvent = (args: any[]) => { return args.map((event) => { if (event?.target) { if (isValid(event.target.value)) return event.target.value if (isValid(event.target.checked)) return event.target.checked return } return event }) } export const getTypedDefaultValue = (field: Field) => { if (isArrayField(field)) return [] if (isObjectField(field)) return {} } export const buildFieldPath = (field: GeneralField) => { return buildDataPath(field.form.fields, field.address) } export const buildDataPath = ( fields: Record, pattern: FormPath ) => { let prevArray = false const segments = pattern.segments const path = segments.reduce((path: string[], key: string, index: number) => { const currentPath = path.concat(key) const currentAddress = segments.slice(0, index + 1) const current = fields[currentAddress.join('.')] if (prevArray) { if (!isVoidField(current)) { prevArray = false } return path } if (index >= segments.length - 1) { return currentPath } if (isVoidField(current)) { const parentAddress = segments.slice(0, index) const parent = fields[parentAddress.join('.')] if (isArrayField(parent) && isNumberLike(key)) { prevArray = true return currentPath } return path } else { prevArray = false } return currentPath }, []) return new FormPath(path) } export const locateNode = (field: GeneralField, address: FormPathPattern) => { field.address = FormPath.parse(address) field.path = buildFieldPath(field) field.form.indexes[field.path.toString()] = field.address.toString() return field } export const patchFieldStates = ( target: Record, patches: INodePatch[] ) => { patches.forEach(({ type, address, oldAddress, payload }) => { if (type === 'remove') { destroy(target, address, false) } else if (type === 'update') { if (payload) { target[address] = payload if (target[oldAddress] === payload) { target[oldAddress] = undefined } } if (address && payload) { locateNode(payload, address) } } }) } export const destroy = ( target: Record, address: string, forceClear = true ) => { const field = target[address] field?.dispose() if (isDataField(field) && forceClear) { const form = field.form const path = field.path form.deleteValuesIn(path) form.deleteInitialValuesIn(path) } delete target[address] } export const patchFormValues = ( form: Form, path: Array, source: any ) => { const update = (path: Array, source: any) => { if (path.length) { form.setValuesIn(path, clone(source)) } else { Object.assign(form.values, clone(source)) } } const patch = (source: any, path: Array = []) => { const targetValue = form.getValuesIn(path) const targetField = form.query(path).take() const isUnVoidField = targetField && !isVoidField(targetField) if (isUnVoidField && targetField.display === 'none') { targetField.caches.value = clone(source) return } if (allowAssignDefaultValue(targetValue, source)) { update(path, source) } else { if (isEmpty(source)) return if (GlobalState.initializing) return if (isPlainObj(targetValue) && isPlainObj(source)) { each(source, (value, key) => { patch(value, path.concat(key)) }) } else { if (targetField) { if (isUnVoidField && !targetField.selfModified) { update(path, source) } } else if (form.initialized) { update(path, source) } } } } patch(source, path) } export const matchFeedback = ( search?: ISearchFeedback, feedback?: IFormFeedback ) => { if (!search || !feedback) return false if (search.type && search.type !== feedback.type) return false if (search.code && search.code !== feedback.code) return false if (search.path && feedback.path) { if (!FormPath.parse(search.path).match(feedback.path)) return false } if (search.address && feedback.address) { if (!FormPath.parse(search.address).match(feedback.address)) return false } if (search.triggerType && search.triggerType !== feedback.triggerType) return false return true } export const queryFeedbacks = (field: Field, search?: ISearchFeedback) => { return field.feedbacks.filter((feedback) => { if (!feedback.messages?.length) return false return matchFeedback(search, { ...feedback, address: field.address?.toString(), path: field.path?.toString(), }) }) } export const queryFeedbackMessages = ( field: Field, search: ISearchFeedback ) => { if (!field.feedbacks.length) return [] return queryFeedbacks(field, search).reduce( (buf, info) => (isEmpty(info.messages) ? buf : buf.concat(info.messages)), [] ) } export const updateFeedback = (field: Field, feedback?: IFieldFeedback) => { if (!feedback) return return batch(() => { if (!field.feedbacks.length) { if (!feedback.messages?.length) { return } field.feedbacks = [feedback] } else { const searched = queryFeedbacks(field, feedback) if (searched.length) { field.feedbacks = field.feedbacks.reduce((buf, item) => { if (searched.includes(item)) { if (feedback.messages?.length) { item.messages = feedback.messages return buf.concat(item) } else { return buf } } else { return buf.concat(item) } }, []) return } else if (feedback.messages?.length) { field.feedbacks = field.feedbacks.concat(feedback) } } }) } export const validateToFeedbacks = async ( field: Field, triggerType: ValidatorTriggerType = 'onInput' ) => { const results = await validate(field.value, field.validator, { triggerType, validateFirst: field.props.validateFirst ?? field.form.props.validateFirst, context: { field, form: field.form }, }) batch(() => { each(results, (messages, type: FieldFeedbackTypes) => { field.setFeedback({ triggerType, type, code: pascalCase(`validate-${type}`), messages: messages, }) }) }) return results } export const setValidatorRule = (field: Field, name: string, value: any) => { if (!isValid(value)) return const validators = parseValidatorDescriptions(field.validator) const hasRule = validators.some((desc) => name in desc) const rule = { [name]: value, } if (hasRule) { field.validator = validators.map((desc: any) => { if (isPlainObj(desc) && hasOwnProperty.call(desc, name)) { desc[name] = value return desc } return desc }) } else { if (name === 'required') { field.validator = [rule].concat(validators) } else { field.validator = validators.concat(rule) } } } export const spliceArrayState = ( field: ArrayField, props?: ISpliceArrayStateProps ) => { const { startIndex, deleteCount, insertCount } = { startIndex: 0, deleteCount: 0, insertCount: 0, ...props, } const address = field.address.toString() const addrLength = address.length const form = field.form const fields = form.fields const fieldPatches: INodePatch[] = [] const offset = insertCount - deleteCount const isArrayChildren = (identifier: string) => { return identifier.indexOf(address) === 0 && identifier.length > addrLength } const isAfterNode = (identifier: string) => { const afterStr = identifier.substring(addrLength) const number = afterStr.match(NumberIndexReg)?.[1] if (number === undefined) return false const index = Number(number) return index > startIndex + deleteCount - 1 } const isInsertNode = (identifier: string) => { const afterStr = identifier.substring(addrLength) const number = afterStr.match(NumberIndexReg)?.[1] if (number === undefined) return false const index = Number(number) return index >= startIndex && index < startIndex + insertCount } const isDeleteNode = (identifier: string) => { const preStr = identifier.substring(0, addrLength) const afterStr = identifier.substring(addrLength) const number = afterStr.match(NumberIndexReg)?.[1] if (number === undefined) return false const index = Number(number) return ( (index > startIndex && !fields[ `${preStr}${afterStr.replace(/^\.\d+/, `.${index + deleteCount}`)}` ]) || index === startIndex ) } const moveIndex = (identifier: string) => { if (offset === 0) return identifier const preStr = identifier.substring(0, addrLength) const afterStr = identifier.substring(addrLength) const number = afterStr.match(NumberIndexReg)?.[1] if (number === undefined) return identifier const index = Number(number) + offset return `${preStr}${afterStr.replace(/^\.\d+/, `.${index}`)}` } batch(() => { each(fields, (field, identifier) => { if (isArrayChildren(identifier)) { if (isAfterNode(identifier)) { const newIdentifier = moveIndex(identifier) fieldPatches.push({ type: 'update', address: newIdentifier, oldAddress: identifier, payload: field, }) } if (isInsertNode(identifier) || isDeleteNode(identifier)) { fieldPatches.push({ type: 'remove', address: identifier }) } } }) patchFieldStates(fields, fieldPatches) }) field.form.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE) } export const exchangeArrayState = ( field: ArrayField, props: IExchangeArrayStateProps ) => { const { fromIndex, toIndex } = { fromIndex: 0, toIndex: 0, ...props, } const address = field.address.toString() const fields = field.form.fields const addrLength = address.length const fieldPatches: INodePatch[] = [] const isArrayChildren = (identifier: string) => { return identifier.indexOf(address) === 0 && identifier.length > addrLength } const isDown = fromIndex < toIndex const isMoveNode = (identifier: string) => { const afterStr = identifier.slice(address.length) const number = afterStr.match(NumberIndexReg)?.[1] if (number === undefined) return false const index = Number(number) return isDown ? index > fromIndex && index <= toIndex : index < fromIndex && index >= toIndex } const isFromNode = (identifier: string) => { const afterStr = identifier.substring(addrLength) const number = afterStr.match(NumberIndexReg)?.[1] if (number === undefined) return false const index = Number(number) return index === fromIndex } const moveIndex = (identifier: string) => { const preStr = identifier.substring(0, addrLength) const afterStr = identifier.substring(addrLength) const number = afterStr.match(NumberIndexReg)[1] const current = Number(number) let index = current if (index === fromIndex) { index = toIndex } else { index += isDown ? -1 : 1 } return `${preStr}${afterStr.replace(/^\.\d+/, `.${index}`)}` } batch(() => { each(fields, (field, identifier) => { if (isArrayChildren(identifier)) { if (isMoveNode(identifier) || isFromNode(identifier)) { const newIdentifier = moveIndex(identifier) fieldPatches.push({ type: 'update', address: newIdentifier, oldAddress: identifier, payload: field, }) if (!fields[newIdentifier]) { fieldPatches.push({ type: 'remove', address: identifier, }) } } } }) patchFieldStates(fields, fieldPatches) }) field.form.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE) } export const cleanupArrayChildren = (field: ArrayField, start: number) => { const address = field.address.toString() const fields = field.form.fields const isArrayChildren = (identifier: string) => { return ( identifier.indexOf(address) === 0 && identifier.length > address.length ) } const isNeedCleanup = (identifier: string) => { const afterStr = identifier.slice(address.length) const numStr = afterStr.match(NumberIndexReg)?.[1] if (numStr === undefined) return false const index = Number(numStr) return index >= start } batch(() => { each(fields, (field, identifier) => { if (isArrayChildren(identifier) && isNeedCleanup(identifier)) { field.destroy() } }) }) } export const cleanupObjectChildren = (field: ObjectField, keys: string[]) => { if (keys.length === 0) return const address = field.address.toString() const fields = field.form.fields const isObjectChildren = (identifier: string) => { return ( identifier.indexOf(address) === 0 && identifier.length > address.length ) } const isNeedCleanup = (identifier: string) => { const afterStr = identifier.slice(address.length) const key = afterStr.match(/^\.([^.]+)/)?.[1] if (key === undefined) return false return keys.includes(key) } batch(() => { each(fields, (field, identifier) => { if (isObjectChildren(identifier) && isNeedCleanup(identifier)) { field.destroy() } }) }) } export const initFieldUpdate = batch.scope.bound((field: GeneralField) => { const form = field.form const updates = FormPath.ensureIn(form, 'requests.updates', []) const indexes = FormPath.ensureIn(form, 'requests.updateIndexes', {}) for (let index = 0; index < updates.length; index++) { const { pattern, callbacks } = updates[index] let removed = false if (field.match(pattern)) { callbacks.forEach((callback) => { field.setState(callback) }) if (!pattern.isWildMatchPattern && !pattern.isMatchPattern) { updates.splice(index--, 1) removed = true } } if (!removed) { indexes[pattern.toString()] = index } else { delete indexes[pattern.toString()] } } }) export const subscribeUpdate = ( form: Form, pattern: FormPath, callback: (...args: any[]) => void ) => { const updates = FormPath.ensureIn(form, 'requests.updates', []) const indexes = FormPath.ensureIn(form, 'requests.updateIndexes', {}) const id = pattern.toString() const current = indexes[id] if (isValid(current)) { if ( updates[current] && !updates[current].callbacks.some((fn: any) => fn.toString() === callback.toString() ? fn === callback : false ) ) { updates[current].callbacks.push(callback) } } else { indexes[id] = updates.length updates.push({ pattern, callbacks: [callback], }) } } export const deserialize = (model: any, setter: any) => { if (!model) return if (isFn(setter)) { setter(model) } else { for (let key in setter) { if (!hasOwnProperty.call(setter, key)) continue if (ReadOnlyProperties[key] || ReservedProperties[key]) continue const MutuallyExclusiveKey = MutuallyExclusiveProperties[key] if ( MutuallyExclusiveKey && hasOwnProperty.call(setter, MutuallyExclusiveKey) && !isValid(setter[MutuallyExclusiveKey]) ) continue const value = setter[key] if (isFn(value)) continue model[key] = value } } return model } export const serialize = (model: any, getter?: any) => { if (isFn(getter)) { return getter(model) } else { const results = {} for (let key in model) { if (!hasOwnProperty.call(model, key)) continue if (ReservedProperties[key]) continue if (key === 'address' || key === 'path') { results[key] = model[key].toString() continue } const value = model[key] if (isFn(value)) continue results[key] = toJS(value) } return results } } export const createChildrenFeedbackFilter = (field: Field) => { const identifier = field.address?.toString() return ({ address }: IFormFeedback) => { return address === identifier || address.indexOf(identifier + '.') === 0 } } export const createStateSetter = (model: any) => { return batch.bound((setter?: any) => deserialize(model, setter)) } export const createStateGetter = (model: any) => { return (getter?: any) => serialize(model, getter) } export const createBatchStateSetter = (form: Form) => { return batch.bound((pattern: FieldMatchPattern, payload?: any) => { if (isQuery(pattern)) { pattern.forEach((field) => { field.setState(payload) }) } else if (isGeneralField(pattern)) { pattern.setState(payload) } else { let matchCount = 0, path = FormPath.parse(pattern) form.query(path).forEach((field) => { field.setState(payload) matchCount++ }) if (matchCount === 0 || path.isWildMatchPattern) { subscribeUpdate(form, path, payload) } } }) } export const createBatchStateGetter = (form: Form) => { return (pattern: FieldMatchPattern, payload?: any) => { if (isQuery(pattern)) { return pattern.take(payload) } else if (isGeneralField(pattern)) { return (pattern as any).getState(payload) } else { return form.query(pattern).take((field: any) => { return field.getState(payload) }) } } } export const triggerFormInitialValuesChange = ( form: Form, change: DataChange ) => { if (Array.isArray(change.object) && change.key === 'length') return if ( contains(form.initialValues, change.object) || form.initialValues === change.value ) { if (change.type === 'add' || change.type === 'set') { patchFormValues(form, change.path.slice(1), change.value) } if (form.initialized) { form.notify(LifeCycleTypes.ON_FORM_INITIAL_VALUES_CHANGE) } } } export const triggerFormValuesChange = (form: Form, change: DataChange) => { if (Array.isArray(change.object) && change.key === 'length') return if ( (contains(form.values, change.object) || form.values === change.value) && form.initialized ) { form.notify(LifeCycleTypes.ON_FORM_VALUES_CHANGE) } } export const setValidating = (target: Form | Field, validating: boolean) => { clearTimeout(target.requests.validate) if (validating) { target.requests.validate = setTimeout(() => { batch(() => { target.validating = validating notify( target, LifeCycleTypes.ON_FORM_VALIDATING, LifeCycleTypes.ON_FIELD_VALIDATING ) }) }, RESPONSE_REQUEST_DURATION) notify( target, LifeCycleTypes.ON_FORM_VALIDATE_START, LifeCycleTypes.ON_FIELD_VALIDATE_START ) } else { if (target.validating !== validating) { target.validating = validating } notify( target, LifeCycleTypes.ON_FORM_VALIDATE_END, LifeCycleTypes.ON_FIELD_VALIDATE_END ) } } export const setSubmitting = (target: Form | Field, submitting: boolean) => { clearTimeout(target.requests.submit) if (submitting) { target.requests.submit = setTimeout(() => { batch(() => { target.submitting = submitting notify( target, LifeCycleTypes.ON_FORM_SUBMITTING, LifeCycleTypes.ON_FIELD_SUBMITTING ) }) }, RESPONSE_REQUEST_DURATION) notify( target, LifeCycleTypes.ON_FORM_SUBMIT_START, LifeCycleTypes.ON_FIELD_SUBMIT_START ) } else { if (target.submitting !== submitting) { target.submitting = submitting } notify( target, LifeCycleTypes.ON_FORM_SUBMIT_END, LifeCycleTypes.ON_FIELD_SUBMIT_END ) } } export const setLoading = (target: Form | Field, loading: boolean) => { clearTimeout(target.requests.loading) if (loading) { target.requests.loading = setTimeout(() => { batch(() => { target.loading = loading notify( target, LifeCycleTypes.ON_FORM_LOADING, LifeCycleTypes.ON_FIELD_LOADING ) }) }, RESPONSE_REQUEST_DURATION) } else if (target.loading !== loading) { target.loading = loading } } export const batchSubmit = async ( target: Form | Field, onSubmit?: (values: any) => Promise | void ): Promise => { const getValues = (target: Form | Field) => { if (isForm(target)) { return toJS(target.values) } return toJS(target.value) } target.setSubmitting(true) try { notify( target, LifeCycleTypes.ON_FORM_SUBMIT_VALIDATE_START, LifeCycleTypes.ON_FIELD_SUBMIT_VALIDATE_START ) await target.validate() notify( target, LifeCycleTypes.ON_FORM_SUBMIT_VALIDATE_SUCCESS, LifeCycleTypes.ON_FIELD_SUBMIT_VALIDATE_SUCCESS ) } catch (e) { notify( target, LifeCycleTypes.ON_FORM_SUBMIT_VALIDATE_FAILED, LifeCycleTypes.ON_FIELD_SUBMIT_VALIDATE_FAILED ) } notify( target, LifeCycleTypes.ON_FORM_SUBMIT_VALIDATE_END, LifeCycleTypes.ON_FIELD_SUBMIT_VALIDATE_END ) let results: any try { if (target.invalid) { throw target.errors } if (isFn(onSubmit)) { results = await onSubmit(getValues(target)) } else { results = getValues(target) } notify( target, LifeCycleTypes.ON_FORM_SUBMIT_SUCCESS, LifeCycleTypes.ON_FIELD_SUBMIT_SUCCESS ) } catch (e) { target.setSubmitting(false) notify( target, LifeCycleTypes.ON_FORM_SUBMIT_FAILED, LifeCycleTypes.ON_FIELD_SUBMIT_FAILED ) notify( target, LifeCycleTypes.ON_FORM_SUBMIT, LifeCycleTypes.ON_FIELD_SUBMIT ) throw e } target.setSubmitting(false) notify(target, LifeCycleTypes.ON_FORM_SUBMIT, LifeCycleTypes.ON_FIELD_SUBMIT) return results } const shouldValidate = (field: Field) => { const validatePattern = field.props.validatePattern ?? field.form.props.validatePattern ?? ['editable'] const validateDisplay = field.props.validateDisplay ?? field.form.props.validateDisplay ?? ['visible'] return ( validatePattern.includes(field.pattern) && validateDisplay.includes(field.display) ) } export const batchValidate = async ( target: Form | Field, pattern: FormPathPattern, triggerType?: ValidatorTriggerType ) => { if (isForm(target)) target.setValidating(true) else { if (!shouldValidate(target)) return } const tasks = [] target.query(pattern).forEach((field) => { if (!isVoidField(field)) { tasks.push(validateSelf(field, triggerType, field === target)) } }) await Promise.all(tasks) if (isForm(target)) target.setValidating(false) if (target.invalid) { notify( target, LifeCycleTypes.ON_FORM_VALIDATE_FAILED, LifeCycleTypes.ON_FIELD_VALIDATE_FAILED ) throw target.errors } notify( target, LifeCycleTypes.ON_FORM_VALIDATE_SUCCESS, LifeCycleTypes.ON_FIELD_VALIDATE_SUCCESS ) } export const batchReset = async ( target: Form | Field, pattern: FormPathPattern, options?: IFieldResetOptions ) => { const tasks = [] target.query(pattern).forEach((field) => { if (!isVoidField(field)) { tasks.push(resetSelf(field, options, target === field)) } }) if (isForm(target)) { target.modified = false } notify(target, LifeCycleTypes.ON_FORM_RESET, LifeCycleTypes.ON_FIELD_RESET) await Promise.all(tasks) } export const validateSelf = batch.bound( async (target: Field, triggerType?: ValidatorTriggerType, noEmit = false) => { const start = () => { setValidating(target, true) } const end = () => { setValidating(target, false) if (noEmit) return if (target.selfValid) { target.notify(LifeCycleTypes.ON_FIELD_VALIDATE_SUCCESS) } else { target.notify(LifeCycleTypes.ON_FIELD_VALIDATE_FAILED) } } if (!shouldValidate(target)) return {} start() if (!triggerType) { const allTriggerTypes = parseValidatorDescriptions( target.validator ).reduce( (types, desc) => types.indexOf(desc.triggerType) > -1 ? types : types.concat(desc.triggerType), [] ) const results = {} for (let i = 0; i < allTriggerTypes.length; i++) { const payload = await validateToFeedbacks(target, allTriggerTypes[i]) each(payload, (result, key) => { results[key] = results[key] || [] results[key] = results[key].concat(result) }) } end() return results } const results = await validateToFeedbacks(target, triggerType) end() return results } ) export const resetSelf = batch.bound( async (target: Field, options?: IFieldResetOptions, noEmit = false) => { const typedDefaultValue = getTypedDefaultValue(target) target.modified = false target.selfModified = false target.visited = false target.feedbacks = [] target.inputValue = typedDefaultValue target.inputValues = [] target.caches = {} if (!isUndef(target.value)) { if (options?.forceClear) { target.value = typedDefaultValue } else { const initialValue = target.initialValue target.value = toJS( !isUndef(initialValue) ? initialValue : typedDefaultValue ) } } if (!noEmit) { target.notify(LifeCycleTypes.ON_FIELD_RESET) } if (options?.validate) { return await validateSelf(target) } } ) export const modifySelf = (target: Field) => { if (target.selfModified) return target.selfModified = true target.modified = true let parent = target.parent while (parent) { if (isDataField(parent)) { if (parent.modified) return parent.modified = true } parent = parent.parent } target.form.modified = true } export const getValidFormValues = (values: any) => { if (isObservable(values)) return values return clone(values || {}) } export const getValidFieldDefaultValue = (value: any, initialValue: any) => { if (allowAssignDefaultValue(value, initialValue)) return clone(initialValue) return value } export const allowAssignDefaultValue = (target: any, source: any) => { const isValidTarget = !isUndef(target) const isValidSource = !isUndef(source) if (!isValidTarget) { return isValidSource } if (typeof target === typeof source) { if (target === '') return false if (target === 0) return false } const isEmptyTarget = target !== null && isEmpty(target, true) const isEmptySource = source !== null && isEmpty(source, true) if (isEmptyTarget) { return !isEmptySource } return false } export const createReactions = (field: GeneralField) => { const reactions = toArr(field.props.reactions) field.form.addEffects(field, () => { reactions.forEach((reaction) => { if (isFn(reaction)) { field.disposers.push( autorun( batch.scope.bound(() => { if (field.destroyed) return reaction(field) }) ) ) } }) }) } export const createReaction = ( tracker: () => T, scheduler?: (value: T) => void ) => { return reaction(tracker, untracked.bound(scheduler)) } export const initializeStart = () => { GlobalState.initializing = true } export const initializeEnd = () => { batch.endpoint(() => { GlobalState.initializing = false }) } export const getArrayParent = (field: BaseField, index = field.index) => { if (index > -1) { let parent: any = field.parent while (parent) { if (isArrayField(parent)) return parent if (parent === field.form) return parent = parent.parent } } } export const getObjectParent = (field: BaseField) => { let parent: any = field.parent while (parent) { if (isArrayField(parent)) return if (isObjectField(parent)) return parent if (parent === field.form) return parent = parent.parent } } ================================================ FILE: packages/core/src/types.ts ================================================ import { IValidatorRules, Validator, ValidatorTriggerType, } from '@formily/validator' import { FormPath } from '@formily/shared' import { Form, Field, LifeCycle, ArrayField, VoidField, ObjectField, Query, } from './models' export type NonFunctionPropertyNames = { [K in keyof T]: T[K] extends (...args: any) => any ? never : K }[keyof T] export type NonFunctionProperties = Pick> export type AnyFunction = (...args: any[]) => any export type JSXComponent = any export type LifeCycleHandler = (payload: T, context: any) => void export type LifeCyclePayload = ( params: { type: string payload: T }, context: any ) => void export enum LifeCycleTypes { /** * Form LifeCycle **/ ON_FORM_INIT = 'onFormInit', ON_FORM_MOUNT = 'onFormMount', ON_FORM_UNMOUNT = 'onFormUnmount', ON_FORM_INPUT_CHANGE = 'onFormInputChange', ON_FORM_VALUES_CHANGE = 'onFormValuesChange', ON_FORM_INITIAL_VALUES_CHANGE = 'onFormInitialValuesChange', ON_FORM_SUBMIT = 'onFormSubmit', ON_FORM_RESET = 'onFormReset', ON_FORM_SUBMIT_START = 'onFormSubmitStart', ON_FORM_SUBMITTING = 'onFormSubmitting', ON_FORM_SUBMIT_END = 'onFormSubmitEnd', ON_FORM_SUBMIT_VALIDATE_START = 'onFormSubmitValidateStart', ON_FORM_SUBMIT_VALIDATE_SUCCESS = 'onFormSubmitValidateSuccess', ON_FORM_SUBMIT_VALIDATE_FAILED = 'onFormSubmitValidateFailed', ON_FORM_SUBMIT_VALIDATE_END = 'onFormSubmitValidateEnd', ON_FORM_SUBMIT_SUCCESS = 'onFormSubmitSuccess', ON_FORM_SUBMIT_FAILED = 'onFormSubmitFailed', ON_FORM_VALIDATE_START = 'onFormValidateStart', ON_FORM_VALIDATING = 'onFormValidating', ON_FORM_VALIDATE_SUCCESS = 'onFormValidateSuccess', ON_FORM_VALIDATE_FAILED = 'onFormValidateFailed', ON_FORM_VALIDATE_END = 'onFormValidateEnd', ON_FORM_GRAPH_CHANGE = 'onFormGraphChange', ON_FORM_LOADING = 'onFormLoading', /** * Field LifeCycle **/ ON_FIELD_INIT = 'onFieldInit', ON_FIELD_INPUT_VALUE_CHANGE = 'onFieldInputValueChange', ON_FIELD_VALUE_CHANGE = 'onFieldValueChange', ON_FIELD_INITIAL_VALUE_CHANGE = 'onFieldInitialValueChange', ON_FIELD_SUBMIT = 'onFieldSubmit', ON_FIELD_SUBMIT_START = 'onFieldSubmitStart', ON_FIELD_SUBMITTING = 'onFieldSubmitting', ON_FIELD_SUBMIT_END = 'onFieldSubmitEnd', ON_FIELD_SUBMIT_VALIDATE_START = 'onFieldSubmitValidateStart', ON_FIELD_SUBMIT_VALIDATE_SUCCESS = 'onFieldSubmitValidateSuccess', ON_FIELD_SUBMIT_VALIDATE_FAILED = 'onFieldSubmitValidateFailed', ON_FIELD_SUBMIT_VALIDATE_END = 'onFieldSubmitValidateEnd', ON_FIELD_SUBMIT_SUCCESS = 'onFieldSubmitSuccess', ON_FIELD_SUBMIT_FAILED = 'onFieldSubmitFailed', ON_FIELD_VALIDATE_START = 'onFieldValidateStart', ON_FIELD_VALIDATING = 'onFieldValidating', ON_FIELD_VALIDATE_SUCCESS = 'onFieldValidateSuccess', ON_FIELD_VALIDATE_FAILED = 'onFieldValidateFailed', ON_FIELD_VALIDATE_END = 'onFieldValidateEnd', ON_FIELD_LOADING = 'onFieldLoading', ON_FIELD_RESET = 'onFieldReset', ON_FIELD_MOUNT = 'onFieldMount', ON_FIELD_UNMOUNT = 'onFieldUnmount', } export type HeartSubscriber = ({ type, payload, }: { type: string payload: any }) => void export interface INodePatch { type: 'remove' | 'update' address: string oldAddress?: string payload?: T } export interface IHeartProps { lifecycles?: LifeCycle[] context?: Context } export interface IFieldFeedback { triggerType?: FieldFeedbackTriggerTypes type?: FieldFeedbackTypes code?: FieldFeedbackCodeTypes messages?: FeedbackMessage } export type IFormFeedback = IFieldFeedback & { path?: string address?: string } export interface ISearchFeedback { triggerType?: FieldFeedbackTriggerTypes type?: FieldFeedbackTypes code?: FieldFeedbackCodeTypes address?: FormPathPattern path?: FormPathPattern messages?: FeedbackMessage } export type FeedbackMessage = any[] export type IFieldUpdate = { pattern: FormPath callbacks: ((...args: any[]) => any)[] } export interface IFormRequests { validate?: number submit?: number loading?: number updates?: IFieldUpdate[] updateIndexes?: Record } export type IFormFields = Record export type FieldFeedbackTypes = 'error' | 'success' | 'warning' export type FieldFeedbackTriggerTypes = ValidatorTriggerType export type FieldFeedbackCodeTypes = | 'ValidateError' | 'ValidateSuccess' | 'ValidateWarning' | 'EffectError' | 'EffectSuccess' | 'EffectWarning' | (string & {}) export type FormPatternTypes = | 'editable' | 'readOnly' | 'disabled' | 'readPretty' | ({} & string) export type FormDisplayTypes = 'none' | 'hidden' | 'visible' | ({} & string) export type FormPathPattern = | string | number | Array | FormPath | RegExp | (((address: Array) => boolean) & { path: FormPath }) type OmitState

= Omit< P, | 'selfDisplay' | 'selfPattern' | 'originValues' | 'originInitialValues' | 'id' | 'address' | 'path' | 'lifecycles' | 'disposers' | 'requests' | 'fields' | 'graph' | 'heart' | 'indexes' | 'props' | 'displayName' | 'setState' | 'getState' | 'getFormGraph' | 'setFormGraph' | 'setFormState' | 'getFormState' > export type IFieldState = Partial< Pick< Field, NonFunctionPropertyNames>> > > export type IVoidFieldState = Partial< Pick< VoidField, NonFunctionPropertyNames>> > > export type IFormState = any> = Pick< Form, NonFunctionPropertyNames>> > export type IFormGraph = Record export interface IFormProps { values?: Partial initialValues?: Partial pattern?: FormPatternTypes display?: FormDisplayTypes hidden?: boolean visible?: boolean editable?: boolean disabled?: boolean readOnly?: boolean readPretty?: boolean effects?: (form: Form) => void validateFirst?: boolean validatePattern?: FormPatternTypes[] validateDisplay?: FormDisplayTypes[] designable?: boolean } export type IFormMergeStrategy = | 'overwrite' | 'merge' | 'deepMerge' | 'shallowMerge' export interface IFieldFactoryProps< Decorator extends JSXComponent, Component extends JSXComponent, TextType = any, ValueType = any > extends IFieldProps { name: FormPathPattern basePath?: FormPathPattern } export interface IVoidFieldFactoryProps< Decorator extends JSXComponent, Component extends JSXComponent, TextType = any > extends IVoidFieldProps { name: FormPathPattern basePath?: FormPathPattern } export interface IFieldRequests { validate?: number submit?: number loading?: number batch?: () => void } export interface IFieldCaches { value?: any initialValue?: any inputting?: boolean } export type FieldDisplayTypes = 'none' | 'hidden' | 'visible' | ({} & string) export type FieldPatternTypes = | 'editable' | 'readOnly' | 'disabled' | 'readPretty' | ({} & string) export type FieldValidatorContext = IValidatorRules & { field?: Field form?: Form value?: any } export type FieldValidator = Validator export type FieldDataSource = { label?: any value?: any title?: any key?: any text?: any children?: FieldDataSource [key: string]: any }[] export type FieldComponent< Component extends JSXComponent, ComponentProps = any > = [Component] | [Component, ComponentProps] | boolean | any[] export type FieldDecorator< Decorator extends JSXComponent, ComponentProps = any > = [Decorator] | [Decorator, ComponentProps] | boolean | any[] export type FieldReaction = (field: Field) => void export interface IFieldProps< Decorator extends JSXComponent = any, Component extends JSXComponent = any, TextType = any, ValueType = any > { name: FormPathPattern basePath?: FormPathPattern title?: TextType description?: TextType value?: ValueType initialValue?: ValueType required?: boolean display?: FieldDisplayTypes pattern?: FieldPatternTypes hidden?: boolean visible?: boolean editable?: boolean disabled?: boolean readOnly?: boolean readPretty?: boolean dataSource?: FieldDataSource validateFirst?: boolean validatePattern?: FieldPatternTypes[] validateDisplay?: FieldDisplayTypes[] validator?: FieldValidator decorator?: FieldDecorator component?: FieldComponent reactions?: FieldReaction[] | FieldReaction content?: any data?: any } export interface IVoidFieldProps< Decorator extends JSXComponent = any, Component extends JSXComponent = any, TextType = any > { name: FormPathPattern basePath?: FormPathPattern title?: TextType description?: TextType display?: FieldDisplayTypes pattern?: FieldPatternTypes hidden?: boolean visible?: boolean editable?: boolean disabled?: boolean readOnly?: boolean readPretty?: boolean decorator?: FieldDecorator component?: FieldComponent reactions?: FieldReaction[] | FieldReaction content?: any data?: any } export interface IFieldResetOptions { forceClear?: boolean validate?: boolean } export type IGeneralFieldState = IFieldState & IVoidFieldState export type GeneralField = Field | VoidField | ArrayField | ObjectField export type DataField = Field | ArrayField | ObjectField export interface ISpliceArrayStateProps { startIndex?: number deleteCount?: number insertCount?: number } export interface IExchangeArrayStateProps { fromIndex?: number toIndex?: number } export interface IQueryProps { pattern: FormPathPattern base: FormPathPattern form: Form } export interface IModelSetter

{ (setter: (state: P) => void): void (setter: Partial

): void (): void } export interface IModelGetter

{ any>(getter: Getter): ReturnType (): P } export type FieldMatchPattern = FormPathPattern | Query | GeneralField export interface IFieldStateSetter { (pattern: FieldMatchPattern, setter: (state: IFieldState) => void): void (pattern: FieldMatchPattern, setter: Partial): void } export interface IFieldStateGetter { any>( pattern: FieldMatchPattern, getter: Getter ): ReturnType (pattern: FieldMatchPattern): IGeneralFieldState } export interface IFieldActions { [key: string]: (...args: any[]) => any } ================================================ FILE: packages/core/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./lib", "paths": { "@formily/*": ["packages/*", "devtools/*"] }, "declaration": true, "types": [] } } ================================================ FILE: packages/core/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["./src/**/*.ts", "./src/**/*.tsx"], "exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"], "compilerOptions": { "lib": ["ESNext", "DOM"], "types": [] } } ================================================ FILE: packages/element/.npmignore ================================================ node_modules *.log build docs doc-site __tests__ .eslintrc jest.config.js vue.config.js tsconfig.json .umi src ================================================ FILE: packages/element/README.md ================================================ # @formily/element ### Requirement vue^2.6.0 + @vue/composition-api^1.0.0-beta.1 ### Install ```bash npm install --save @formily/element ``` ================================================ FILE: packages/element/build-style.ts ================================================ import { build } from '../../scripts/build-style' build({ esStr: 'element/es/', libStr: 'element/lib/', allStylesOutputFile: 'dist/element.css', }) ================================================ FILE: packages/element/create-style.ts ================================================ import glob from 'glob' import path from 'path' import fs from 'fs-extra' glob( './*/style.scss', { cwd: path.resolve(__dirname, './src') }, (err, files) => { if (err) return console.error(err) fs.writeFile( path.resolve(__dirname, './src/style.ts'), `// auto generated code ${files .map((path) => { return `import '${path}'\n` }) .join('')}`, 'utf8' ) } ) ================================================ FILE: packages/element/docs/.vuepress/components/createCodeSandBox.js ================================================ import { getParameters } from 'codesandbox/lib/api/define' const CodeSandBoxHTML = '

' const CodeSandBoxJS = ` import Vue from 'vue' import App from './App.vue' import Element from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; Vue.config.productionTip = false Vue.use(Element, { size: 'small' }); new Vue({ render: h => h(App), }).$mount('#app')` const createForm = ({ method, action, data }) => { const form = document.createElement('form') // 构造 form form.style.display = 'none' // 设置为不显示 form.target = '_blank' // 指向 iframe // 构造 formdata Object.keys(data).forEach((key) => { const input = document.createElement('input') // 创建 input input.name = key // 设置 name input.value = data[key] // 设置 value form.appendChild(input) }) form.method = method // 设置方法 form.action = action // 设置地址 document.body.appendChild(form) // 对该 form 执行提交 form.submit() document.body.removeChild(form) } export function createCodeSandBox(codeStr) { const parameters = getParameters({ files: { 'sandbox.config.json': { content: { template: 'node', infiniteLoopProtection: true, hardReloadOnChange: false, view: 'browser', container: { port: 8080, node: '14', }, }, }, 'package.json': { content: { scripts: { serve: 'vue-cli-service serve', build: 'vue-cli-service build', lint: 'vue-cli-service lint', }, dependencies: { '@formily/core': 'latest', '@formily/vue': 'latest', '@formily/element': 'latest', axios: '^0.21.1', 'core-js': '^3.6.5', 'element-ui': 'latest', 'vue-demi': 'latest', vue: '^2.6.11', }, devDependencies: { '@vue/cli-plugin-babel': '~4.5.0', '@vue/cli-service': '~4.5.0', '@vue/composition-api': 'latest', 'vue-template-compiler': '^2.6.11', sass: '^1.34.1', 'sass-loader': '^8.0.2', }, babel: { presets: [ [ '@vue/babel-preset-jsx', { vModel: false, compositionAPI: true, }, ], ], }, vue: { devServer: { host: '0.0.0.0', disableHostCheck: true, // 必须 }, }, }, }, 'src/App.vue': { content: codeStr, }, 'src/main.js': { content: CodeSandBoxJS, }, 'public/index.html': { content: CodeSandBoxHTML, }, }, }) createForm({ method: 'post', action: 'https://codesandbox.io/api/v1/sandboxes/define', data: { parameters, query: 'file=/src/App.vue', }, }) } ================================================ FILE: packages/element/docs/.vuepress/components/dumi-previewer.vue ================================================ ================================================ FILE: packages/element/docs/.vuepress/components/highlight.js ================================================ const prism = require('prismjs') const escapeHtml = require('escape-html') const loadLanguages = require('prismjs/components/index') function wrap(code, lang) { if (lang === 'text') { code = escapeHtml(code) } return `
${code}
` } function getLangCodeFromExtension(extension) { const extensionMap = { vue: 'markup', html: 'markup', md: 'markdown', rb: 'ruby', ts: 'typescript', py: 'python', sh: 'bash', yml: 'yaml', styl: 'stylus', kt: 'kotlin', rs: 'rust', } return extensionMap[extension] || extension } module.exports = (str, lang) => { if (!lang) { return wrap(str, 'text') } lang = lang.toLowerCase() const rawLang = lang lang = getLangCodeFromExtension(lang) if (!prism.languages[lang]) { try { loadLanguages([lang]) } catch (e) { console.warn( `[vuepress] Syntax highlight for language "${lang}" is not supported.` ) } } if (prism.languages[lang]) { const code = prism.highlight(str, prism.languages[lang], lang) return wrap(code, rawLang) } return wrap(str, 'text') } ================================================ FILE: packages/element/docs/.vuepress/config.js ================================================ const path = require('path') const utils = require('./util') const componentFiles = utils .getFiles(path.resolve(__dirname, '../guide')) .map((item) => item.replace(/(\.md)/g, '')) .filter((item) => !['el-form', 'el-form-item', 'index'].includes(item)) module.exports = { title: 'Element', description: 'Alibaba unified front-end form solution', dest: './doc-site', theme: '@vuepress-dumi/dumi', head: [ [ 'link', { rel: 'icon', href: '//img.alicdn.com/imgextra/i3/O1CN01XtT3Tv1Wd1b5hNVKy_!!6000000002810-55-tps-360-360.svg', }, ], [ 'link', { rel: 'stylesheet', href: 'https://esm.sh/element-ui/lib/theme-chalk/index.css', }, ], ], themeConfig: { logo: '//img.alicdn.com/imgextra/i2/O1CN01Kq3OHU1fph6LGqjIz_!!6000000004056-55-tps-1141-150.svg', nav: [ { text: 'Element', link: '/guide/', }, { text: '主站', link: 'https://formilyjs.org', }, { text: 'GITHUB', link: 'https://github.com/alibaba/formily', }, ], sidebar: { '/guide/': ['', ...componentFiles], }, lastUpdated: 'Last Updated', smoothScroll: true, }, plugins: [ 'vuepress-plugin-typescript', '@vuepress/back-to-top', '@vuepress/last-updated', '@vuepress-dumi/dumi-previewer', [ '@vuepress/medium-zoom', { selector: '.content__default :not(a) > img', }, ], ], configureWebpack: (config, isServer) => { return { resolve: { alias: { '@formily/element': path.resolve(__dirname, '../../src'), vue$: 'vue/dist/vue.esm.js', }, }, } }, chainWebpack: (config, isServer) => { config.module .rule('js') // Find the rule. .use('babel-loader') // Find the loader .tap((options) => Object.assign(options, { // Modifying options presets: [ [ '@vue/babel-preset-jsx', { vModel: false, compositionAPI: true, }, ], ], }) ) }, } ================================================ FILE: packages/element/docs/.vuepress/enhanceApp.js ================================================ import pageComponents from '@internal/page-components' import Element from 'element-ui' import '@formily/element/style.ts' export default ({ Vue }) => { for (const [name, component] of Object.entries(pageComponents)) { Vue.component(name, component) } Vue.use(Element, { size: 'small' }) } ================================================ FILE: packages/element/docs/.vuepress/styles/index.styl ================================================ .navbar { padding: 0 28px !important; } .navbar .logo { height: auto !important; width: 150px !important; } .navbar .site-name { // display: none; } .navbar .sidebar-button { padding: 0; } .home .feature { margin-bottom: 40px; text-align: center; } .theme-dumi-content:not(.custom) { max-width: 100%; } .page .page-nav { max-width: 100%; } .dumi-previewer .dumi-previewer-actions .dumi-previewer-actions__icon { padding: 0 !important; } .page .page-edit { max-width 100% } .sidebar-group .sidebar-heading { color: #454d64; font-size: 16px; } .sidebar-group a.sidebar-link { font-size: 0.9em; } .theme-dumi-content .custom-block.warning { padding: 10px 20px; border-color: #FFC11F; box-shadow: 0 6px 16px -2px rgba(0,0,0,.06); background: rgba(255,229,100,0.1); } .theme-dumi-content .custom-block.danger { padding: 10px 20px; p { margin: 0; } } .theme-dumi-content:not(.custom) > h1, .theme-dumi-content:not(.custom) > h2, .theme-dumi-content:not(.custom) > h3, .theme-dumi-content:not(.custom) > h4, .theme-dumi-content:not(.custom) > h5, .theme-dumi-content:not(.custom) > h6 { margin-bottom: 18px; } .theme-dumi-content p { margin: 1em 0; } .custom-block.warning p { margin: 0; } // .theme-dumi-content div[class*="language-"] { // background-color: #f9fafb; // } // .theme-dumi-content pre[class*="language-"] code { // color: #000; // } .dumi-previewer .dumi-previewer-source, .dumi-previewer .dumi-previewer-demo { overflow: auto; } @media (max-width: 719px) { .sidebar-button + .home-link { margin-left: 20px; } } @media (max-width: 419px) { .theme-dumi-content div[class*="language-"] { margin: 0; border-radius: 0; } } ================================================ FILE: packages/element/docs/.vuepress/util.js ================================================ const fs = require('fs') module.exports = { getFiles(dir) { return fs.readdirSync(dir) }, } ================================================ FILE: packages/element/docs/README.md ================================================ --- home: true heroText: Formily Element tagline: 基于 Element UI 封装的Formily2.x组件体系 actionText: 组件文档 actionLink: /guide/ features: - title: 更易用 details: 开箱即用,案例丰富 - title: 更高效 details: 傻瓜写法,超高性能 - title: 更专业 details: 完备,灵活,优雅 footer: Open-source MIT Licensed | Copyright © 2019-present --- ## 安装 vue2: ```bash $ npm install --save element-ui $ npm install --save @formily/core @formily/vue @vue/composition-api @formily/element ``` main.js 中添加 element 样式 ```javascript import 'element-ui/lib/theme-chalk/index.css' ``` ## 快速开始 ================================================ FILE: packages/element/docs/demos/guide/array-cards/effects-json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/array-cards/effects-markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/array-cards/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/array-cards/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/array-collapse/effects-json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/array-collapse/effects-markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/array-collapse/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/array-collapse/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/array-items/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/array-items/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/array-table/effects-json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/array-table/effects-markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/array-table/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/array-table/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/array-tabs/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/array-tabs/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/cascader/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/cascader/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/cascader/template.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/checkbox/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/checkbox/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/checkbox/template.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/date-picker/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/date-picker/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/date-picker/template.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/editable/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/editable/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/editable/template.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-button-group.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-collapse/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-collapse/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-dialog/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-dialog/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-dialog/template.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-drawer/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-drawer/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-drawer/template.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-grid/form.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-grid/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-grid/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-grid/native.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-item/bordered-none.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-item/common.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-item/feedback.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-item/inset.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-item/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-item/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-item/size.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-item/template.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-layout/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-layout/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-layout/template.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-step/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-step/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-tab/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form-tab/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/form.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/input/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/input/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/input/template.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/input-number/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/input-number/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/input-number/template.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/password/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/password/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/password/template.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/preview-text/base.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/preview-text/extend.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/radio/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/radio/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/radio/template.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/reset/base.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/reset/force.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/reset/validate.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/select/json-schema-async.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/select/json-schema-sync.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/select/markup-schema-async-search.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/select/markup-schema-async.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/select/markup-schema-sync.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/select/template-async.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/select/template-sync.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/space/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/space/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/space/template.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/submit/base.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/submit/loading.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/switch/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/switch/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/switch/template.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/time-picker/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/time-picker/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/time-picker/template.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/transfer/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/transfer/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/transfer/template.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/upload/json-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/upload/markup-schema.vue ================================================ ================================================ FILE: packages/element/docs/demos/guide/upload/template.vue ================================================ ================================================ FILE: packages/element/docs/demos/index.vue ================================================ ================================================ FILE: packages/element/docs/guide/array-cards.md ================================================ # ArrayCards > 卡片列表,对于每行字段数量较多,联动较多的场景比较适合使用 ArrayCards > > 注意:该组件只适用于 Schema 场景 ## Markup Schema 案例 ## JSON Schema 案例 ## Effects 联动案例 ## JSON Schema 联动案例 ## API ### ArrayCards > 表格组件 参考 [https://element.eleme.io/#/zh-CN/component/card](https://element.eleme.io/#/zh-CN/component/card) ### ArrayCards.Addition > 添加按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------------ | ------- | ---------- | -------- | -------- | | title | string | 文案 | | | method | `'push' | 'unshift'` | 添加方式 | `'push'` | | defaultValue | any | 默认值 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCards.Remove > 删除按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | ------ | ---- | ------ | | title | string | 文案 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCards.MoveDown > 下移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | ------ | ---- | ------ | | title | string | 文案 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCards.MoveUp > 上移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | ------ | ---- | ------ | | title | string | 文案 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCards.Index > 索引渲染器 无属性 ### ArrayCards.useIndex > 读取当前渲染行索引的 Hook ### ArrayCards.useRecord > 读取当前渲染记录的 Hook ================================================ FILE: packages/element/docs/guide/array-collapse.md ================================================ # ArrayCollapse > 折叠面板,对于每行字段数量较多,联动较多的场景比较适合使用 ArrayCollapse > > 注意:该组件只适用于 Schema 场景 ## Markup Schema 案例 ## JSON Schema 案例 ## Effects 联动案例 ## JSON Schema 联动案例 ## API ### ArrayCollapse 参考 [https://element.eleme.io/#/zh-CN/component/collapse](https://element.eleme.io/#/zh-CN/component/collapse) ### ArrayCollapse.Item 参考 [https://element.eleme.io/#/zh-CN/component/collapse](https://element.eleme.io/#/zh-CN/component/collapse) ### ArrayCollapse.Addition > 添加按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------------ | ------- | ---------- | -------- | -------- | | title | string | 文案 | | | method | `'push' | 'unshift'` | 添加方式 | `'push'` | | defaultValue | any | 默认值 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCollapse.Remove > 删除按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | ------ | ---- | ------ | | title | string | 文案 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCollapse.MoveDown > 下移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | ------ | ---- | ------ | | title | string | 文案 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCollapse.MoveUp > 上移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | ------ | ---- | ------ | | title | string | 文案 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCollapse.Index > 索引渲染器 无属性 ### ArrayCollapse.useIndex > 读取当前渲染行索引的 Hook ### ArrayCollapse.useRecord > 读取当前渲染记录的 Hook ================================================ FILE: packages/element/docs/guide/array-items.md ================================================ # ArrayItems > 自增列表,对于简单的自增编辑场景比较适合,或者对于空间要求高的场景比较适合 > > 注意:该组件只适用于 Schema 场景 ## Markup Schema 案例 ## JSON Schema 案例 ## API ### ArrayItems 继承 HTMLDivElement Props ### ArrayItems.Item > 列表区块 继承 HTMLDivElement Props ### ArrayItems.SortHandle > 拖拽手柄 参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) ### ArrayItems.Addition > 添加按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------------ | ------- | ---------- | -------- | -------- | | title | string | 文案 | | | method | `'push' | 'unshift'` | 添加方式 | `'push'` | | defaultValue | any | 默认值 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayItems.Remove > 删除按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | ------ | ---- | ------ | | title | string | 文案 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayItems.MoveDown > 下移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | ------ | ---- | ------ | | title | string | 文案 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayItems.MoveUp > 上移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | ------ | ---- | ------ | | title | string | 文案 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayItems.Index > 索引渲染器 无属性 ### ArrayItems.useIndex > 读取当前渲染行索引的 Hook ### ArrayItems.useRecord > 读取当前渲染记录的 Hook ================================================ FILE: packages/element/docs/guide/array-table.md ================================================ # ArrayTable > 自增表格,对于数据量超大的场景比较适合使用该组件,虽然数据量大到一定程度会有些许卡顿,但是不会影响基本操作 > > 注意:该组件只适用于 Schema 场景,且只能是对象数组 ## Markup Schema 案例 ## JSON Schema 案例 ## Effects 联动案例 ## JSON Schema 联动案例 ## API ### ArrayTable > 表格组件 参考 [https://element.eleme.io/#/zh-CN/component/table](https://element.eleme.io/#/zh-CN/component/table) ### ArrayTable.Column > 表格列 参考 [https://element.eleme.io/#/zh-CN/component/table](https://element.eleme.io/#/zh-CN/component/table) 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | -------- | ------- | -------- | ------ | | asterisk | boolean | 星号显示 | true | > ArrayTableColumn 会自动检查内部的 FormItem 是否必填,并自动在表头加上红色星号。如果不希望显示,可通过 `asterisk` 属性进行覆盖。 ### ArrayTable.Addition > 添加按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------ | ------- | ---------- | -------- | -------- | | title | string | 文案 | | | method | `'push' | 'unshift'` | 添加方式 | `'push'` | 其余参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayTable.Remove > 删除按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | ------ | ---- | ------ | | title | string | 文案 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayTable.MoveDown > 下移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | ------ | ---- | ------ | | title | string | 文案 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayTable.MoveUp > 上移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | ------ | ---- | ------ | | title | string | 文案 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayTable.Index > 索引渲染器 无属性 ### ArrayTable.useIndex > 读取当前渲染行索引的 Hook ### ArrayTable.useRecord > 读取当前渲染记录的 Hook ================================================ FILE: packages/element/docs/guide/array-tabs.md ================================================ # ArrayTabs > 自增选项卡,对于纵向空间要求较高的场景可以考虑使用该组件 > > 注意:该组件只适用于 Schema 场景 ## Markup Schema 案例 ## JSON Schema 案例 ## API ### ArrayTabs 参考 [https://element.eleme.io/#/zh-CN/component/tab](https://element.eleme.io/#/zh-CN/component/tab) ================================================ FILE: packages/element/docs/guide/cascader.md ================================================ # Cascader > 级联选择器 ## Markup Schema 案例 ## JSON Schema 案例 ## Template 案例 ## API 参考 [https://element.eleme.io/#/zh-CN/component/cascader](https://element.eleme.io/#/zh-CN/component/cascader) ================================================ FILE: packages/element/docs/guide/checkbox.md ================================================ # Checkbox > 复选框 ## Markup Schema 案例 ## JSON Schema 案例 ## Template 案例 ## API 参考 [https://element.eleme.io/#/zh-CN/component/checkbox](https://element.eleme.io/#/zh-CN/component/checkbox) ### 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ---------- | ------------------------------------------------------------------------------------------ | -------- | ------- | | options | [CheckboxProps](https://element.eleme.io/#/zh-CN/component/checkbox#checkbox-attributes)[] | 选项 | [] | | optionType | default/button | 样式类型 | default | ================================================ FILE: packages/element/docs/guide/date-picker.md ================================================ # DatePicker > 日期选择器 ## Markup Schema 案例 ## JSON Schema 案例 ## Template 案例 ## API 参考 [https://element.eleme.io/#/zh-CN/component/date-picker](https://element.eleme.io/#/zh-CN/component/date-picker) ================================================ FILE: packages/element/docs/guide/editable.md ================================================ # Editable > 局部编辑器,对于一些空间要求较高的表单区域可以使用该组件 > > Editable 组件相当于是 FormItem 组件的变体,所以通常放在 decorator 中 ## Markup Schema 案例 ## JSON Schema 案例 ## Template 案例 ## API ### Editable > 内联编辑 参考 [https://element.formilyjs.org/guide/form-item.html#api](https://element.formilyjs.org/guide/form-item.html#api) ### Editable.Popover > 浮层编辑 参考 [https://element.eleme.io/#/zh-CN/component/popover](https://element.eleme.io/#/zh-CN/component/popover) ================================================ FILE: packages/element/docs/guide/form-button-group.md ================================================ # FormButtonGroup > 表单按钮组布局组件 ## 使用案例 ## API | 属性名 | 类型 | 描述 | 默认值 | | ------------- | ------- | ------------- | -------- | -------- | -------- | | gutter | number | 间隙大小 | 8px | | align | `'left' | 'center' | 'right'` | 对齐方式 | `'left'` | | alignFormItem | boolean | 对齐 FormItem | `false` | ================================================ FILE: packages/element/docs/guide/form-collapse.md ================================================ # FormCollapse > 折叠面板,通常用在布局空间要求较高的表单场景 > > 注意:只能用在 Schema 场景 ## Markup Schema 案例 ## JSON Schema 案例 ## API ### FormCollapse | 属性名 | 类型 | 描述 | 默认值 | | ------------ | ------------- | ---------------------------------------------------------- | ------ | | formCollapse | IFormCollapse | 传入通过 createFormCollapse/useFormCollapse 创建出来的模型 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/collapse](https://element.eleme.io/#/zh-CN/component/collapse) ### FormCollapse.Item 参考 [https://element.eleme.io/#/zh-CN/component/collapse](https://element.eleme.io/#/zh-CN/component/collapse) ### FormCollapse.createFormCollapse ```ts pure type ActiveKey = string | number type ActiveKeys = string | number | Array interface createFormCollapse { (defaultActiveKeys?: ActiveKeys): IFormCollpase } interface IFormCollapse { //激活主键列表 activeKeys: ActiveKeys //是否存在该激活主键 hasActiveKey(key: ActiveKey): boolean //设置激活主键列表 setActiveKeys(keys: ActiveKeys): void //添加激活主键 addActiveKey(key: ActiveKey): void //删除激活主键 removeActiveKey(key: ActiveKey): void //开关切换激活主键 toggleActiveKey(key: ActiveKey): void } ``` ================================================ FILE: packages/element/docs/guide/form-dialog.md ================================================ # FormDialog > 弹窗表单,主要用在简单的事件打开表单场景 ## Markup Schema 案例 以下例子演示了 FormDialog 的几个能力: - 快速打开,关闭能力 - 中间件能力,自动出现加载态 - 渲染函数内可以响应式能力 - 上下文共享能力 ## JSON Schema 案例 ## Template 案例 ## API ### FormDialog ```ts pure import { IFormProps, Form } from '@formily/core' type FormDialogContentProps = { form: Form } type FormDialogContent = Component | ((props: FormDialogContentProps) => VNode) type DialogTitle = string | number | Component | VNode | (() => VNode) type IFormDialogProps = Omit & { title?: DialogTitle footer?: null | Component | VNode | (() => VNode) cancelText?: string | Component | VNode | (() => VNode) cancelButtonProps?: ButtonProps okText?: string | Component | VNode | (() => VNode) okButtonProps?: ButtonProps onOpen?: () => void onOpened?: () => void onClose?: () => void onClosed?: () => void onCancel?: () => void onOK?: () => void loadingText?: string } interface IFormDialog { forOpen( middleware: ( props: IFormProps, next: (props?: IFormProps) => Promise ) => any ): IFormDialog forConfirm( middleware: (props: Form, next: (props?: Form) => Promise) => any ): IFormDialog forCancel( middleware: (props: Form, next: (props?: Form) => Promise) => any ): IFormDialog open(props?: IFormProps): Promise close(): void } interface FormDialog { (title: IFormDialogProps, id: string, content: FormDialogContent): IFormDialog (title: IFormDialogProps, id: FormDialogContent): IFormDialog (title: DialogTitle, id: string, content: FormDialogContent): IFormDialog (title: DialogTitle, id: FormDialogContent): IFormDialog } ``` `DialogProps`类型定义参考 [Element-UI Dialog API](https://element.eleme.io/#/zh-CN/component/dialog#attributes) ### FormDialog.Footer 无属性,只接收子节点 ### FormDialog.Portal 接收可选的 id 属性,默认值为 form-dialog,如果一个应用存在多个 prefixCls,不同区域的弹窗内部 prefixCls 不一样,那推荐指定 id 为区域级 id ================================================ FILE: packages/element/docs/guide/form-drawer.md ================================================ # FormDrawer > 抽屉表单,主要用在简单的事件打开表单场景 ## Markup Schema 案例 ## JSON Schema 案例 ## Template 案例 ## API ### FormDrawer ```ts pure import { IFormProps, Form } from '@formily/core' type FormDrawerContentProps = { form: Form } type FormDrawerContent = Component | ((props: FormDrawerContentProps) => VNode) type DrawerTitle = string | number | Component | VNode | (() => VNode) type IFormDrawerProps = Omit & { title?: DrawerTitle footer?: null | Component | VNode | (() => VNode) cancelText?: string | Component | VNode | (() => VNode) cancelButtonProps?: ButtonProps okText?: string | Component | VNode | (() => VNode) okButtonProps?: ButtonProps onOpen?: () => void onOpened?: () => void onClose?: () => void onClosed?: () => void onCancel?: () => void onOK?: () => void loadingText?: string } interface FormDrawer { (title: IFormDrawerProps, id: string, content: FormDrawerContent): IFormDrawer (title: IFormDrawerProps, id: FormDrawerContent): IFormDrawer (title: DrawerTitle, id: string, content: FormDrawerContent): IFormDrawer (title: DrawerTitle, id: FormDrawerContent): IFormDrawer } ``` `DrawerProps`类型定义参考 [Element-UI Drawer API](https://element.eleme.io/#/zh-CN/component/drawer#attributes) ### FormDrawer.Footer 无属性,只接收子节点 ### FormDrawer.Portal 接收可选的 id 属性,默认值为 form-dialog,如果一个应用存在多个 prefixCls,不同区域的弹窗内部 prefixCls 不一样,那推荐指定 id 为区域级 id ================================================ FILE: packages/element/docs/guide/form-grid.md ================================================ # FormGrid > FormGrid 组件 ## Markup Schema 案例 ## JSON Schema 案例 ## 原生案例 ## 查询表单实现案例 ## API ### FormGrid | 属性名 | 类型 | 描述 | 默认值 | | ------------- | ---------------------- | -------------------------------------------------------------- | ----------------- | | minWidth | `number / number[]` | 元素最小宽度 | 100 | | maxWidth | `number / number[]` | 元素最大宽度 | - | | minColumns | `number / number[]` | 最小列数 | 0 | | maxColumns | `number / number[]` | 最大列数 | - | | breakpoints | number[] | 容器尺寸断点 | `[720,1280,1920]` | | columnGap | number | 列间距 | 8 | | rowGap | number | 行间距 | 4 | | colWrap | boolean | 自动换行 | true | | strictAutoFit | boolean | GridItem 宽度是否严格受限于 maxWidth,不受限的话会自动占满容器 | false | | shouldVisible | `(node,grid)=>boolean` | 是否需要显示当前节点 | `()=>true` | | grid | `Grid` | 外部传入 Grid 实例,用于实现更复杂的布局逻辑 | - | 注意: - minWidth 生效优先级高于 minColumn - maxWidth 优先级高于 maxColumn - minWidth/maxWidth/minColumns/maxColumns 的数组格式代表与断点数组映射 ### FormGrid.GridColumn | 属性名 | 类型 | 描述 | 默认值 | | -------- | ------ | ---------------------------------------------------- | ------ | | gridSpan | number | 元素所跨列数,如果为-1,那么会自动反向跨列填补单元格 | 1 | ### FormGrid.createFormGrid 从上下文中读取 Grid 实例 ```ts interface createFormGrid { (props: IGridProps): Grid } ``` - IGridProps 参考 FormGrid 属性 - Grid 实例属性方法参考 https://github.com/alibaba/formily/tree/formily_next/packages/grid ### FormGrid.useFormGrid 从上下文中读取 Grid 实例 ```ts interface useFormGrid { (): Grid } ``` - Grid 实例属性方法参考 https://github.com/alibaba/formily/tree/formily_next/packages/grid ================================================ FILE: packages/element/docs/guide/form-item.md ================================================ # FormItem > 全新的 FormItem 组件,相比于 Element 的 FormItem,它支持的功能更多,同时它的定位是纯样式组件,不管理表单状态,所以也会更轻量,更方便定制 ## Markup Schema 案例 ## JSON Schema 案例 ## Template 案例 ## 常用属性案例 ## 无边框案例 设置去除组件边框 ## 内嵌模式案例 设置表单组件为内嵌模式 ## 反馈信息定制案例 可通过 `feedbackIcon` 传入指定反馈的按钮 ## 尺寸控制案例 ## API ### FormItem | 属性名 | 类型 | 描述 | 默认值 | | -------------- | ------------------------------------------------------ | ------------------------------------------- | ---------- | -------- | | style | CSSProperties | 样式 | - | | label | String \| Vue Component | 标签 | - | | labelStyle | CSSProperties | 标签样式 | - | | wrapperStyle | CSSProperties | 组件容器样式 | - | | className | string | 组件样式类名 | - | | colon | boolean | 冒号 | - | | tooltip | String \| Vue Component | 问号提示 | - | | tooltipLayout | `"icon" | "text"` | 问提示布局 | `"icon"` | | labelAlign | `"left"` \| `"right"` | 标签文本对齐方式 | `"right"` | | labelWrap | boolean | 标签换⾏,否则出现省略号,hover 有 tooltip | false | | labelWidth | `number` | 标签固定宽度 | - | | wrapperWidth | `number` | 内容固定宽度 | - | | labelCol | number | 标签⽹格所占列数,和内容列数加起来总和为 24 | - | | wrapperCol | number | 内容⽹格所占列数,和标签列数加起来总和为 24 | - | | wrapperAlign | `"left"` \| `"right"` | 内容文本对齐方式⻬ | `"left"` | | wrapperWrap | boolean | 内容换⾏,否则出现省略号,hover 有 tooltip | false | | fullness | boolean | 内容撑满 | false | | addonBefore | String \| Vue Component | 前缀内容 | - | | addonAfter | String \| Vue Component | 后缀内容 | - | | size | `"small"` \| `"default"` \| `"large"` | 尺⼨ | - | | extra | ReactNode | 扩展描述⽂案 | - | | feedbackText | ReactNode | 反馈⽂案 | - | | feedbackLayout | `"loose"` \| `"terse"` \| `"popover"` \| `"none"` | 反馈布局 | - | | feedbackStatus | `"error"` \| `"warning"` \| `"success"` \| `"pending"` | 反馈布局 | - | | feedbackIcon | string | 反馈图标 | - | | required | boolean | 星号提醒 | - | | asterisk | boolean | 星号提醒 | - | | gridSpan | number | ⽹格布局占宽 | - | ### FormItem.BaseItem 纯样式组件,属性与 FormItem 一样,与 Formily Core 不做状态桥接,主要用于一些需要依赖 FormItem 的样式布局能力,但不希望接入 Field 状态的场景 ================================================ FILE: packages/element/docs/guide/form-layout.md ================================================ # FormLayout > 区块级布局批量控制组件,借助该组件,我们可以轻松的控制被 FormLayout 圈住的所有 FormItem 组件的布局模式 ## Markup Schema 案例 ## JSON Schema 案例 ## Template 案例 ## API | 属性名 | 类型 | 描述 | 默认值 | | -------------- | ------------- | ----------------- | ----------------------- | ----------- | ---------------- | ------------ | -------- | ---------- | | style | CSSProperties | 样式 | - | | className | string | 类名 | - | | colon | boolean | 是否有冒号 | true | | labelAlign | `'right' | 'left' | ('right' | 'left')[]` | 标签内容对齐 | - | | wrapperAlign | `'right' | 'left' | ('right' | 'left')[]` | 组件容器内容对齐 | - | | labelWrap | boolean | 标签内容换行 | false | | labelWidth | number | 标签宽度(px) | - | | wrapperWidth | number | 组件容器宽度(px) | - | | wrapperWrap | boolean | 组件容器换行 | false | | labelCol | `number | number[]` | 标签宽度(24 column) | - | | wrapperCol | `number | number[]` | 组件容器宽度(24 column) | - | | fullness | boolean | 组件容器宽度 100% | false | | size | `'small' | 'default' | 'large'` | 组件尺寸 | default | | layout | `'vertical' | 'horizontal' | 'inline' | ('vertical' | 'horizontal' | 'inline')[]` | 布局模式 | horizontal | | direction | `'rtl' | 'ltr'` | 方向(暂不支持) | ltr | | inset | boolean | 内联布局 | false | | shallow | boolean | 上下文浅层传递 | true | | feedbackLayout | `'loose' | 'terse' | 'popover' | 'none'` | 反馈布局 | true | | tooltipLayout | `'icon'` | `'text'` | 问提示布局 | `"icon"` | | bordered | boolean | 是否有边框 | true | | breakpoints | number[] | 容器尺寸断点 | - | | gridColumnGap | number | 网格布局列间距 | 8 | | gridRowGap | number | 网格布局行间距 | 4 | | spaceGap | number | 弹性间距 | 8 | ================================================ FILE: packages/element/docs/guide/form-step.md ================================================ # FormStep > 分步表单组件 > > 注意:该组件只能用在 Schema 场景 ## Markup Schema 案例 ## JSON Schema 案例 ## API ### FormStep | 属性名 | 类型 | 描述 | 默认值 | | -------- | --------- | -------------------------------------- | ------ | | formStep | IFormStep | 传入通过 createFormStep 创建出来的模型 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/steps](https://element.eleme.io/#/zh-CN/component/steps) ### FormStep.StepPane 参考 [https://element.eleme.io/#/zh-CN/component/steps](https://element.eleme.io/#/zh-CN/component/steps) ### FormStep.createFormStep ```ts pure interface createFormStep { (current?: number): IFormStep } interface IFormStep { //当前索引 current: number //是否允许向后 allowNext: boolean //是否允许向前 allowBack: boolean //设置当前索引 setCurrent(key: number): void //提交表单 submit: Formily.Core.Models.Form['submit'] //向后 next(): void //向前 back(): void } ``` ================================================ FILE: packages/element/docs/guide/form-tab.md ================================================ # FormTab > 选项卡表单 > > 注意:该组件只适用于 Schema 场景 ## Markup Schema 案例 ## JSON Schema 案例 ## API ### FormTab | 属性名 | 类型 | 描述 | 默认值 | | ------- | -------- | ------------------------------------- | ------ | | formTab | IFormTab | 传入通过 createFormTab 创建出来的模型 | | 其余参考 [https://element.eleme.io/#/zh-CN/component/tabs](https://element.eleme.io/#/zh-CN/component/tabs) ### FormTab.TabPane 参考 [https://element.eleme.io/#/zh-CN/component/tabs](https://element.eleme.io/#/zh-CN/component/tabs) ### FormTab.createFormTab ```ts pure type ActiveKey = string | number interface createFormTab { (defaultActiveKey?: ActiveKey): IFormTab } interface IFormTab { //激活主键 activeKey: ActiveKey //设置激活主键 setActiveKey(key: ActiveKey): void } ``` ================================================ FILE: packages/element/docs/guide/form.md ================================================ # Form > FormProvider + FormLayout + form 标签的组合组件,可以帮助我们快速实现带回车提交的且能批量布局的表单 ## 使用案例 > 注意:想要实现回车提交,我们在使用 Submit 组件的时候不能给其传 submit 事件,否则回车提交会失效,这样做的目的是为了防止用户同时在多处写 submit 事件监听器,处理逻辑不一致的话,提交时很难定位问题。 ## API 布局相关的 API 属性,我们参考 [FormLayout](./form-layout) 即可,剩下是 Form 组件独有的 API 属性 | 属性名 | 类型 | 描述 | 默认值 | | ---------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------- | ------ | | form | [Form](https://core.formilyjs.org/api/models/form) | Form 实例 | - | | component | string | 渲染组件,可以指定为自定义组件渲染 | `form` | | previewTextPlaceholder | string \| Vue Component | 预览态占位符 | `N/A` | | onAutoSubmit | `(values:any)=>any` | 回车提交事件回调 | - | | onAutoSubmitFailed | (feedbacks: [IFormFeedback](https://core.formilyjs.org/api/models/form#iformfeedback)[]) => void | 回车提交校验失败事件回调 | - | ================================================ FILE: packages/element/docs/guide/index.md ================================================ # Element-UI ## 介绍 @formily/element 是基于 Element UI 封装的针对表单场景专业级(Professional)组件库,它主要有以下几个特点: - 更丰富的组件体系 - 布局组件 - FormLayout - FormItem - FormGrid - FormButtonGroup - Space - Submit - Reset - 输入控件 - Input - Password - Select - DatePicker - TimePicker - InputNumber - Transfer - Cascader - Radio - Checkbox - Upload - Switch - 场景组件 - ArrayCards - ArrayItems - ArrayTable - ArrayTabs - FormCollapse - FormStep - FormTab - FormDialog - FormDrawer - Editable - 阅读态组件 - PreviewText - 主题定制能力 - follow 组件库的样式体系,更方便定制主题 - 支持二次封装 - 所有组件都能二次封装 - 支持阅读态 - 提供了 PreviewText 组件,用户可以基于它自己做阅读态封装,灵活性更强 - 类型更加友好 - 每个组件都有着极其完整的类型定义,用户在实际开发过程中,可以感受到前所未有的智能提示体验 - 更完备的布局控制能力 - 基于 FormLayout、FormItem、FormGrid 组件,提供更智能的布局能力。 - 更优雅易用的 API - FormStep,用户只需要关注 FormStep Reactive Model 即可,通过 createFormStep 就可以创建出 Reactive Model,传给 FormStep 组件即可快速通讯。同理,FormTab/FormCollapse 也是一样的通讯模式 - 弹窗表单,抽屉表单,想必过去,用户几乎每次都得在这两个场景上写大量的代码,这次直接提供了极其简易的 API 让用户使用,最大化提升开发效率 ## 注意 因为 Element UI 是基于 Sass 构建的,如果你用 Webpack 配置请使用以下两个 Sass 工具 ``` "sass": "^1.32.11", "sass-loader": "^8.0.2" ``` ## 安装 ```bash $ npm install --save element-ui $ npm install --save @formily/core @formily/vue @vue/composition-api @formily/element ``` ## 按需打包 `Element-UI` 按需引入参见 [https://element.eleme.io/#/zh-CN/component/quickstart#an-xu-yin-ru](https://element.eleme.io/#/zh-CN/component/quickstart#an-xu-yin-ru) `@formily/element`按需引入需借助 `babel-plugin-import` #### 安装 `babel-plugin-import` ```shell npm install babel-plugin-import --save-dev ``` 或者 ```shell yarn add babel-plugin-import --dev ``` 修改 `.babelrc` ```json { "plugins": [ [ "component", { "libraryName": "element-ui", "styleLibraryName": "theme-chalk" } ], [ "import", { "libraryName": "@formily/element", "libraryDirectory": "esm", "style": true } ] ] } ``` ## Q/A 问:我想自己封装一套组件库,该怎么做? 答:如果是开源组件库,可以直接参与项目共建,提供 PR,如果是企业内私有组件库,参考源码即可,源码并没有太多复杂逻辑。 问:为什么 ArrayCards/ArrayTable/FormStep 这类组件只支持 Schema 模式,不支持纯 Template 模式? 答:这就是 Schema 模式的核心优势,借助协议,我们可以做场景化抽象,相反,纯 Template 模式,受限于 Template 的不可解析性,我们很难做到 UI 级别的场景化抽象,更多的只是抽象 Hook。 ================================================ FILE: packages/element/docs/guide/input-number.md ================================================ # InputNumber > 数字输入框 ## Markup Schema 案例 ## JSON Schema 案例 ## Template 案例 ## API 参考 [https://element.eleme.io/#/zh-CN/component/input-number](https://element.eleme.io/#/zh-CN/component/input-number) ================================================ FILE: packages/element/docs/guide/input.md ================================================ # Input > 文本输入框 ## Markup Schema 案例 ## JSON Schema 案例 ## Template 案例 ## API 参考 [https://element.eleme.io/#/zh-CN/component/input](https://element.eleme.io/#/zh-CN/component/input) ================================================ FILE: packages/element/docs/guide/password.md ================================================ # Password > 密码输入框 ## Markup Schema 案例 ## JSON Schema 案例 ## Template 案例 ## API 参考 [https://element.eleme.io/#/zh-CN/component/input](https://element.eleme.io/#/zh-CN/component/input) ================================================ FILE: packages/element/docs/guide/preview-text.md ================================================ # PreviewText > 阅读态组件,主要用来实现类 Input,类 DatePicker 这些组件的阅读态 ## 简单案例 ## 扩展案例 ## API ### PreviewText.Input 参考 [https://element.eleme.io/#/zh-CN/component/input](https://element.eleme.io/#/zh-CN/component/input) ### PreviewText.Select 参考 [https://element.eleme.io/#/zh-CN/component/select](https://element.eleme.io/#/zh-CN/component/select) ### PreviewText.Cascader 参考 [https://element.eleme.io/#/zh-CN/component/cascader](https://element.eleme.io/#/zh-CN/component/cascader) ### PreviewText.DatePicker 参考 [https://element.eleme.io/#/zh-CN/component/date-picker](https://element.eleme.io/#/zh-CN/component/date-picker) ### PreviewText.TimePicker 参考 [https://element.eleme.io/#/zh-CN/component/time-picker](https://element.eleme.io/#/zh-CN/component/time-picker) ### PreviewText | 属性名 | 类型 | 描述 | 默认值 | | ------ | ------ | ---------- | ------ | | value | stirng | 缺省占位符 | N/A | ### PreviewText.Placeholder | 属性名 | 类型 | 描述 | 默认值 | | ------ | ------ | ---------- | ------ | | value | stirng | 缺省占位符 | N/A | ### PreviewText.usePlaceholder ```ts pure interface usePreviewTextPlaceholder { (): string } ``` ================================================ FILE: packages/element/docs/guide/radio.md ================================================ # Radio > 单选框 ## Markup Schema 案例 ## JSON Schema 案例 ## Template 案例 ## API 参考 [https://element.eleme.io/#/zh-CN/component/radio](https://element.eleme.io/#/zh-CN/component/radio) ### 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ---------- | --------------------------------------------------------------------------------- | -------- | ------- | | options | [RadioProps](https://element.eleme.io/#/zh-CN/component/radio#radio-attributes)[] | 选项 | [] | | optionType | default/button | 样式类型 | default | ================================================ FILE: packages/element/docs/guide/reset.md ================================================ # Reset > 重置按钮 ## 普通重置 > 有默认值的控件无法清空 ## 强制清空重置 ## 强制清空重置并校验 ## API 按钮相关的 API 属性,我们参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 即可,剩下是 Reset 组件独有的 API 属性 ### 事件 | 属性名 | 类型 | 描述 | 默认值 | | ---------------------- | ------------------------------------------------------------------------------------------------ | ---------------- | ------------------------------------- | --- | | onClick | `(event: MouseEvent) => void | boolean` | 点击事件,如果返回 false 可以阻塞重置 | - | | onResetValidateSuccess | (payload: any) => void | 重置校验成功事件 | - | | onResetValidateFailed | (feedbacks: [IFormFeedback](https://core.formilyjs.org/api/models/form#iformfeedback)[]) => void | 重置校验失败事件 | - | ================================================ FILE: packages/element/docs/guide/select.md ================================================ # Select > 下拉框组件 ## Markup Schema 同步数据源案例 ## Markup Schema 异步搜索案例 ## Markup Schema 异步联动数据源案例 ## JSON Schema 同步数据源案例 ## JSON Schema 异步联动数据源案例 ## Template 同步数据源案例 ## Template 异步联动数据源案例 ## API 参考 [https://element.eleme.io/#/zh-CN/component/select](https://element.eleme.io/#/zh-CN/component/select) ### 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------- | ------------------------------------------------------------------------------------------ | ---- | ------ | | options | [SelectOptionProps](https://element.eleme.io/#/zh-CN/component/select#option-attributes)[] | 选项 | [] | ================================================ FILE: packages/element/docs/guide/space.md ================================================ # Space > 超级便捷的 Flex 布局组件,可以帮助用户快速实现任何元素的并排紧挨布局 ## Markup Schema 案例 ## JSON Schema 案例 ## Template 案例 ## API 参考 [https://ant.design/components/space-cn/](https://ant.design/components/space-cn/) ================================================ FILE: packages/element/docs/guide/submit.md ================================================ # Submit > 提交按钮 ## 普通提交 ## 防重提交 ## API 按钮相关的 API 属性,我们参考 [https://element.eleme.io/#/zh-CN/component/button](https://element.eleme.io/#/zh-CN/component/button) 即可,剩下是 Submit 组件独有的 API 属性 | 属性名 | 类型 | 描述 | 默认值 | | --------------- | ------------------------------------------------------------------------------------------------ | -------------------- | ------------------------------------- | --- | | onClick | `(event: MouseEvent) => void | boolean` | 点击事件,如果返回 false 可以阻塞提交 | - | | onSubmit | `(values: any) => Promise | any` | 提交事件回调 | - | | onSubmitSuccess | (payload: any) => void | 提交成功响应事件 | - | | onSubmitFailed | (feedbacks: [IFormFeedback](https://core.formilyjs.org/api/models/form#iformfeedback)[]) => void | 提交校验失败事件回调 | - | ================================================ FILE: packages/element/docs/guide/switch.md ================================================ # Switch > 开关组件 ## Markup Schema 案例 ## JSON Schema 案例 ## Template 案例 ## API 参考 [https://element.eleme.io/#/zh-CN/component/switch](https://element.eleme.io/#/zh-CN/component/switch) ================================================ FILE: packages/element/docs/guide/time-picker.md ================================================ # TimePicker > 时间选择器 ## Markup Schema 案例 ## JSON Schema 案例 ## Template 案例 ## API 参考 [https://element.eleme.io/#/zh-CN/component/time-picker](https://element.eleme.io/#/zh-CN/component/time-picker) ================================================ FILE: packages/element/docs/guide/transfer.md ================================================ # Transfer > 穿梭框 ## Markup Schema 案例 ## JSON Schema 案例 ## Template 案例 ## API 参考 [https://element.eleme.io/#/zh-CN/component/transfer](https://element.eleme.io/#/zh-CN/component/transfer) ================================================ FILE: packages/element/docs/guide/upload.md ================================================ # Upload > 上传组件 > > 注意:使用上传组件,推荐用户进行二次封装,用户无需关心上传组件与 Formily 的数据通信,只需要处理样式与基本上传配置即可。 ## Markup Schema 案例 ## JSON Schema 案例 ## Template 案例 ## API 参考 [https://element.eleme.io/#/zh-CN/component/upload](https://element.eleme.io/#/zh-CN/component/upload) ================================================ FILE: packages/element/package.json ================================================ { "name": "@formily/element", "version": "2.3.7", "license": "MIT", "main": "lib", "module": "esm", "umd:main": "dist/formily.element.umd.production.js", "unpkg": "dist/formily.element.umd.production.js", "jsdelivr": "dist/formily.element.umd.production.js", "jsnext:main": "esm", "types": "lib/index.d.ts", "engines": { "npm": ">=3.0.0" }, "sideEffects": [ "dist/*", "esm/*.js", "lib/*.js", "src/*.ts", "*.scss" ], "scripts": { "start": "vuepress dev docs", "build": "rimraf -rf lib esm dist && npm run create:style && npm run build:cjs && npm run build:esm && npm run build:umd && npm run build:style", "create:style": "ts-node create-style", "build:style": "ts-node build-style", "build:cjs": "ttsc --declaration", "build:esm": "ttsc --declaration --module es2015 --outDir esm", "build:umd": "rollup --config", "build:docs": "vuepress build docs" }, "devDependencies": { "@vue/composition-api": "^1.0.0-rc.7", "@vuepress-dumi/vuepress-plugin-dumi-previewer": "0.3.3", "@vuepress-dumi/vuepress-theme-dumi": "0.3.3", "@vuepress/plugin-back-to-top": "^1.8.2", "@vuepress/plugin-medium-zoom": "^1.8.2", "codesandbox": "^2.2.3", "core-js": "^2.4.0", "element-ui": "^2.15.7", "sass": "^1.34.1", "ts-import-plugin": "^2.0.0", "ttypescript": "^1.5.15", "vue": "^2.6.0", "vuepress": "^1.8.2", "vuepress-plugin-typescript": "^0.3.1" }, "dependencies": { "@formily/core": "2.3.7", "@formily/grid": "2.3.7", "@formily/json-schema": "2.3.7", "@formily/reactive": "2.3.7", "@formily/reactive-vue": "2.3.7", "@formily/shared": "2.3.7", "@formily/vue": "2.3.7", "portal-vue": "^2.1.7", "vue-demi": ">=0.13.6", "vue-slicksort": "^1.2.0" }, "peerDependencies": { "@vue/composition-api": "^1.0.0-beta.1", "element-ui": "^2.14.0", "vue": "^2.6.0" }, "peerDependenciesMeta": { "@vue/composition-api": { "optional": true } }, "publishConfig": { "access": "public" }, "gitHead": "ac79c196ae9324889aca5e0501146f9b37b04283" } ================================================ FILE: packages/element/rollup.config.js ================================================ import baseConfig, { removeImportStyleFromInputFilePlugin, } from '../../scripts/rollup.base.js' export default baseConfig( 'formily.element', 'Formily.Element', removeImportStyleFromInputFilePlugin() ) ================================================ FILE: packages/element/src/__builtins__/configs/index.ts ================================================ export const stylePrefix = 'formily-element' ================================================ FILE: packages/element/src/__builtins__/index.ts ================================================ export * from './configs' export * from './shared' ================================================ FILE: packages/element/src/__builtins__/shared/create-context.ts ================================================ import type { Component } from 'vue' import { defineComponent, inject, InjectionKey, provide, readonly, ref, Ref, toRef, } from 'vue-demi' export type CreateContext = { Provider: Component Consumer: Component injectKey: InjectionKey> } export const createContext = (defaultValue?: T): CreateContext => { const injectKey: InjectionKey> = Symbol() return { Provider: defineComponent({ name: 'ContextProvider', props: { value: { type: null, default() { return defaultValue ?? null }, }, }, setup(props, { slots }) { const value = toRef(props, 'value') provide(injectKey, readonly(value)) return () => slots?.default?.() }, }), Consumer: defineComponent({ name: 'ContextConsumer', setup(_props, { slots }) { const value = inject(injectKey) return () => slots?.default?.(value) }, }), injectKey, } } export const useContext = (context: CreateContext) => { const key = context.injectKey return inject(key, ref(null)) } ================================================ FILE: packages/element/src/__builtins__/shared/index.ts ================================================ export * from './transform-component' export * from './resolve-component' export * from './create-context' export * from './utils' export * from './portal' export * from './loading' export * from './types' ================================================ FILE: packages/element/src/__builtins__/shared/loading.ts ================================================ import { Loading } from 'element-ui' export const loading = async ( loadingText = 'Loading...', processor: () => Promise ) => { let loadingInstance = null let loading = setTimeout(() => { loadingInstance = Loading.service({ text: loadingText, background: 'transparent', }) }, 100) try { return await processor() } finally { loadingInstance?.close() clearTimeout(loading) } } ================================================ FILE: packages/element/src/__builtins__/shared/portal.ts ================================================ import { Fragment, h } from '@formily/vue' import { defineComponent, onBeforeUnmount } from 'vue-demi' export interface IPortalProps { id?: string | symbol } const PortalMap = new Map() export const createPortalProvider = (id: string | symbol) => { const Portal = defineComponent({ name: 'ProtalProvider', props: { id: { type: [String, Symbol], default: id, }, }, setup(props) { onBeforeUnmount(() => { const { id } = props if (id && PortalMap.has(id)) { PortalMap.delete(id) } }) }, render() { const { id } = this if (id && !PortalMap.has(id)) { PortalMap.set(id, this) } return h(Fragment, {}, this.$scopedSlots) }, }) return Portal } export function getProtalContext(id: string | symbol) { return PortalMap.get(id) } ================================================ FILE: packages/element/src/__builtins__/shared/resolve-component.ts ================================================ import { Component } from 'vue' import { h, toRaw } from 'vue-demi' import { SlotTypes } from '.' import { isVnode } from './utils' export const resolveComponent = ( child?: SlotTypes, props?: Record ) => { if (child) { if (typeof child === 'string' || typeof child === 'number') { return child } else if (typeof child === 'function') { return (child as Function)(props) } else if (isVnode(child)) { return child } else { return h(toRaw(child as Component), { props }) } } return null } ================================================ FILE: packages/element/src/__builtins__/shared/transform-component.ts ================================================ import type { Component } from 'vue' import { merge } from '@formily/shared' import { h } from '@formily/vue' import { isVue2, defineComponent } from 'vue-demi' type ListenersTransformRules = Record const noop = () => {} export const transformComponent = >( tag: any, transformRules?: ListenersTransformRules, defaultProps?: Partial ): Component | any => { if (isVue2) { return defineComponent({ setup(props, { attrs, slots, listeners }) { return () => { const data = { attrs: { ...attrs, }, on: { ...listeners, }, } if (transformRules) { const transformListeners = transformRules Object.keys(transformListeners).forEach((extract) => { if (data.on !== undefined) { data.on[transformListeners[extract]] = listeners[extract] || noop } }) } if (defaultProps) { data.attrs = merge(defaultProps, data.attrs) } return h(tag, data, slots) } }, }) } else { return defineComponent({ setup(props, { attrs, slots }) { return () => { let data = { ...attrs, } if (transformRules) { const listeners = transformRules Object.keys(listeners).forEach((extract) => { const event = listeners[extract] data[`on${event[0].toUpperCase()}${event.slice(1)}`] = attrs[`on${extract[0].toUpperCase()}${extract.slice(1)}`] || noop }) } if (defaultProps) { data = merge(defaultProps, data) } return h(tag, data, slots) } }, }) } } ================================================ FILE: packages/element/src/__builtins__/shared/types.ts ================================================ import { Component, VNode } from 'vue' export type SlotTypes = | Component | string | number | ((props: Record) => VNode[] | VNode) | VNode ================================================ FILE: packages/element/src/__builtins__/shared/utils.ts ================================================ import { onMounted, ref } from 'vue-demi' export function isValidElement(element) { return ( isVueOptions(element) || (element && typeof element === 'object' && 'componentOptions' in element && 'context' in element && element.tag !== undefined) ) // remove text node } export function isVnode(element: any): boolean { return ( element && typeof element === 'object' && 'componentOptions' in element && 'context' in element && element.tag !== undefined ) } export function isVueOptions(options) { return ( options && (typeof options.template === 'string' || typeof options.render === 'function') ) } export function composeExport( s0: T0, s1: T1 ): T0 & T1 { return Object.assign(s0, s1) } /** * 处理 vue 2.6 和 2.7 的 ref 兼容问题 * composition-api 不支持 setup ref * @param refs * @returns */ export function useCompatRef(refs?: { [key: string]: Vue | Element | Vue[] | Element[] }) { const elRef = ref(null) const elRefBinder = Math.random().toString(36).slice(-8) onMounted(() => { if (refs) { elRef.value = refs[elRefBinder] } }) return { elRef, elRefBinder: refs ? elRefBinder : elRef, } } ================================================ FILE: packages/element/src/__builtins__/styles/common.scss ================================================ $formily-prefix: 'formily-element'; $namespace: 'el'; @import '~element-ui/packages/theme-chalk/src/common/var.scss'; @mixin active { border-color: $--color-primary; outline: 0; border-right-width: $--border-width-base !important; } @mixin hover { border-color: $--border-color-hover; outline: 0; border-right-width: $--border-width-base !important; } ================================================ FILE: packages/element/src/array-base/index.ts ================================================ import { ArrayField } from '@formily/core' import { clone, isValid, uid } from '@formily/shared' import { ExpressionScope, Fragment, h, useField, useFieldSchema, } from '@formily/vue' import { defineComponent, inject, InjectionKey, onBeforeUnmount, PropType, provide, Ref, ref, toRefs, } from 'vue-demi' import { stylePrefix } from '../__builtins__/configs' import type { Schema } from '@formily/json-schema' import type { Button as ButtonProps } from 'element-ui' import { Button } from 'element-ui' import { HandleDirective } from 'vue-slicksort' import { composeExport } from '../__builtins__/shared' export interface IArrayBaseAdditionProps extends ButtonProps { title?: string method?: 'push' | 'unshift' defaultValue?: any } export type ArrayBaseMixins = { Addition?: typeof ArrayBaseAddition Remove?: typeof ArrayBaseRemove MoveUp?: typeof ArrayBaseMoveUp MoveDown?: typeof ArrayBaseMoveDown SortHandle?: typeof ArrayBaseSortHandle Index?: typeof ArrayBaseIndex useArray?: typeof useArray useIndex?: typeof useIndex useRecord?: typeof useRecord } export interface IArrayBaseProps { disabled?: boolean keyMap?: WeakMap | String[] | null } export interface IArrayBaseItemProps { index: number record: any } export interface IArrayBaseContext { field: Ref schema: Ref props: IArrayBaseProps listeners: { [key in string]?: Function } keyMap?: WeakMap | String[] | null } const ArrayBaseSymbol: InjectionKey = Symbol('ArrayBaseContext') const ItemSymbol: InjectionKey = Symbol('ItemContext') const useArray = () => { return inject(ArrayBaseSymbol, null) } const useIndex = (index?: number) => { const { index: indexRef } = toRefs(inject(ItemSymbol)) return indexRef ?? ref(index) } const useRecord = (record?: number) => { const { record: recordRef } = toRefs(inject(ItemSymbol)) return recordRef ?? ref(record) } const isObjectValue = (schema: Schema) => { if (Array.isArray(schema?.items)) return isObjectValue(schema.items[0]) if (schema?.items?.type === 'array' || schema?.items?.type === 'object') { return true } return false } const useKey = (schema: Schema) => { const isObject = isObjectValue(schema) let keyMap: WeakMap | String[] | null = null if (isObject) { keyMap = new WeakMap() } else { keyMap = [] } onBeforeUnmount(() => { keyMap = null }) return { keyMap, getKey: (record: any, index?: number) => { if (keyMap instanceof WeakMap) { if (!keyMap.has(record)) { keyMap.set(record, uid()) } return `${keyMap.get(record)}-${index}` } if (!keyMap[index]) { keyMap[index] = uid() } return `${keyMap[index]}-${index}` }, } } const getDefaultValue = (defaultValue: any, schema: Schema): any => { if (isValid(defaultValue)) return clone(defaultValue) if (Array.isArray(schema?.items)) return getDefaultValue(defaultValue, schema.items[0]) if (schema?.items?.type === 'array') return [] if (schema?.items?.type === 'boolean') return true if (schema?.items?.type === 'date') return '' if (schema?.items?.type === 'datetime') return '' if (schema?.items?.type === 'number') return 0 if (schema?.items?.type === 'object') return {} if (schema?.items?.type === 'string') return '' return null } const ArrayBaseInner = defineComponent({ name: 'ArrayBase', props: { disabled: { type: Boolean, default: false, }, keyMap: { type: [WeakMap, Array] as PropType | String[]>, }, }, setup(props, { slots, listeners }) { const field = useField() const schema = useFieldSchema() provide(ArrayBaseSymbol, { field, schema, props, listeners, keyMap: props.keyMap, }) return () => { return h(Fragment, {}, slots) } }, }) const ArrayBaseItem = defineComponent({ name: 'ArrayBaseItem', props: ['index', 'record'], setup(props: IArrayBaseItemProps, { slots }) { provide(ItemSymbol, props) return () => { return h( ExpressionScope, { props: { value: { $record: props.record, $index: props.index } } }, { default: () => h(Fragment, {}, slots), } ) } }, }) const ArrayBaseSortHandle = defineComponent({ name: 'ArrayBaseSortHandle', props: ['index'], directives: { handle: HandleDirective, }, setup(props, { attrs }) { const array = useArray() const prefixCls = `${stylePrefix}-array-base` return () => { if (!array) return null if (array.field.value?.pattern !== 'editable') return null return h( Button, { directives: [{ name: 'handle' }], class: [`${prefixCls}-sort-handle`], attrs: { size: 'mini', type: 'text', icon: 'el-icon-rank', ...attrs, }, }, {} ) } }, }) const ArrayBaseIndex = defineComponent({ name: 'ArrayBaseIndex', setup(props, { attrs }) { const index = useIndex() const prefixCls = `${stylePrefix}-array-base` return () => { return h( 'span', { class: `${prefixCls}-index`, attrs, }, { default: () => [`#${index.value + 1}.`], } ) } }, }) const ArrayBaseAddition = defineComponent({ name: 'ArrayBaseAddition', props: ['title', 'method', 'defaultValue'], setup(props: IArrayBaseAdditionProps, { listeners }) { const self = useField() const array = useArray() const prefixCls = `${stylePrefix}-array-base` return () => { if (!array) return null if (array?.field.value.pattern !== 'editable') return null return h( Button, { class: `${prefixCls}-addition`, attrs: { type: 'ghost', icon: 'qax-icon-Alone-Plus', ...props, }, on: { ...listeners, click: (e) => { if (array.props?.disabled) return const defaultValue = getDefaultValue( props.defaultValue, array?.schema.value ) if (props.method === 'unshift') { array?.field?.value.unshift(defaultValue) array.listeners?.add?.(0) } else { array?.field?.value.push(defaultValue) array.listeners?.add?.(array?.field?.value?.value?.length - 1) } if (listeners.click) { listeners.click(e) } }, }, }, { default: () => [self.value.title || props.title], } ) } }, }) const ArrayBaseRemove = defineComponent< ButtonProps & { title?: string; index?: number } >({ name: 'ArrayBaseRemove', props: ['title', 'index'], setup(props, { attrs, listeners }) { const indexRef = useIndex(props.index) const base = useArray() const prefixCls = `${stylePrefix}-array-base` return () => { if (base?.field.value.pattern !== 'editable') return null return h( Button, { class: `${prefixCls}-remove`, attrs: { type: 'text', size: 'mini', icon: 'el-icon-delete', ...attrs, }, on: { ...listeners, click: (e: MouseEvent) => { e.stopPropagation() if (Array.isArray(base?.keyMap)) { base?.keyMap?.splice(indexRef.value, 1) } base?.field.value.remove(indexRef.value as number) base?.listeners?.remove?.(indexRef.value as number) if (listeners.click) { listeners.click(e) } }, }, }, { default: () => [props.title], } ) } }, }) const ArrayBaseMoveDown = defineComponent< ButtonProps & { title?: string; index?: number } >({ name: 'ArrayBaseMoveDown', props: ['title', 'index'], setup(props, { attrs, listeners }) { const indexRef = useIndex(props.index) const base = useArray() const prefixCls = `${stylePrefix}-array-base` return () => { if (base?.field.value.pattern !== 'editable') return null return h( Button, { class: `${prefixCls}-move-down`, attrs: { size: 'mini', type: 'text', icon: 'el-icon-arrow-down', ...attrs, }, on: { ...listeners, click: (e: MouseEvent) => { e.stopPropagation() if (Array.isArray(base?.keyMap)) { base.keyMap.splice( indexRef.value + 1, 0, base.keyMap.splice(indexRef.value, 1)[0] ) } base?.field.value.moveDown(indexRef.value as number) base?.listeners?.moveDown?.(indexRef.value as number) if (listeners.click) { listeners.click(e) } }, }, }, { default: () => [props.title], } ) } }, }) const ArrayBaseMoveUp = defineComponent< ButtonProps & { title?: string; index?: number } >({ name: 'ArrayBaseMoveUp', props: ['title', 'index'], setup(props, { attrs, listeners }) { const indexRef = useIndex(props.index) const base = useArray() const prefixCls = `${stylePrefix}-array-base` return () => { if (base?.field.value.pattern !== 'editable') return null return h( Button, { class: `${prefixCls}-move-up`, attrs: { size: 'mini', type: 'text', icon: 'el-icon-arrow-up', ...attrs, }, on: { ...listeners, click: (e: MouseEvent) => { e.stopPropagation() if (Array.isArray(base?.keyMap)) { base.keyMap.splice( indexRef.value - 1, 0, base.keyMap.splice(indexRef.value, 1)[0] ) } base?.field.value.moveUp(indexRef.value as number) base?.listeners?.moveUp?.(indexRef.value as number) if (listeners.click) { listeners.click(e) } }, }, }, { default: () => [props.title], } ) } }, }) export const ArrayBase = composeExport(ArrayBaseInner, { Index: ArrayBaseIndex, Item: ArrayBaseItem, SortHandle: ArrayBaseSortHandle, Addition: ArrayBaseAddition, Remove: ArrayBaseRemove, MoveDown: ArrayBaseMoveDown, MoveUp: ArrayBaseMoveUp, useArray: useArray, useIndex: useIndex, useKey: useKey, useRecord: useRecord, }) export default ArrayBase ================================================ FILE: packages/element/src/array-base/style.scss ================================================ @import '../__builtins__/styles/common.scss'; $array-base-prefix-cls: '#{$formily-prefix}-array-base'; .#{$array-base-prefix-cls}-addition { transition: $--all-transition; } .#{$array-base-prefix-cls}-remove { i { font-size: $--font-size-base; } } .#{$array-base-prefix-cls}-move-down { i { font-size: $--font-size-base; } } .#{$array-base-prefix-cls}-move-up { i { font-size: $--font-size-base; } } .#{$array-base-prefix-cls}-sort-handle { i { font-size: $--font-size-base; cursor: move; } } ================================================ FILE: packages/element/src/array-base/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/button.scss' import './style.scss' ================================================ FILE: packages/element/src/array-cards/index.ts ================================================ import { ArrayField } from '@formily/core' import { ISchema } from '@formily/json-schema' import { observer } from '@formily/reactive-vue' import { h, RecursionField, useField, useFieldSchema } from '@formily/vue' import type { Card as CardProps } from 'element-ui' import { Card, Empty, Row } from 'element-ui' import { defineComponent } from 'vue-demi' import { ArrayBase } from '../array-base' import { stylePrefix } from '../__builtins__/configs' import { composeExport } from '../__builtins__/shared' const isAdditionComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Addition') > -1 } const isIndexComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Index') > -1 } const isRemoveComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Remove') > -1 } const isMoveUpComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('MoveUp') > -1 } const isMoveDownComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('MoveDown') > -1 } const isOperationComponent = (schema: ISchema) => { return ( isAdditionComponent(schema) || isRemoveComponent(schema) || isMoveDownComponent(schema) || isMoveUpComponent(schema) ) } const ArrayCardsInner = observer( defineComponent({ name: 'FArrayCards', props: [], setup(props, { attrs }) { const fieldRef = useField() const schemaRef = useFieldSchema() const prefixCls = `${stylePrefix}-array-cards` const { getKey, keyMap } = ArrayBase.useKey(schemaRef.value) return () => { const field = fieldRef.value const schema = schemaRef.value const dataSource = Array.isArray(field.value) ? field.value : [] if (!schema) throw new Error('can not found schema object') const renderItems = () => { return dataSource?.map((item, index) => { const items = Array.isArray(schema.items) ? schema.items[index] || schema.items[0] : schema.items const title = h( 'span', {}, { default: () => [ h( RecursionField, { props: { schema: items, name: index, filterProperties: (schema) => { if (!isIndexComponent(schema)) return false return true }, onlyRenderProperties: true, }, }, {} ), attrs.title || field.title, ], } ) const extra = h( 'span', {}, { default: () => [ h( RecursionField, { props: { schema: items, name: index, filterProperties: (schema) => { if (!isOperationComponent(schema)) return false return true }, onlyRenderProperties: true, }, }, {} ), attrs.extra, ], } ) const content = h( RecursionField, { props: { schema: items, name: index, filterProperties: (schema) => { if (isIndexComponent(schema)) return false if (isOperationComponent(schema)) return false return true }, }, }, {} ) return h( ArrayBase.Item, { key: getKey(item, index), props: { index, record: item, }, }, { default: () => h( Card, { class: [`${prefixCls}-item`], attrs: { shadow: 'never', ...attrs, }, }, { default: () => [content], header: () => h( Row, { props: { type: 'flex', justify: 'space-between', }, }, { default: () => [title, extra], } ), } ), } ) }) } const renderAddition = () => { return schema.reduceProperties((addition, schema) => { if (isAdditionComponent(schema)) { return h( RecursionField, { props: { schema, name: 'addition', }, }, {} ) } return addition }, null) } const renderEmpty = () => { if (dataSource?.length) return return h( Card, { class: [`${prefixCls}-item`], attrs: { shadow: 'never', ...attrs, header: attrs.title || field.title, }, }, { default: () => h( Empty, { props: { description: 'No Data', imageSize: 100 } }, {} ), } ) } return h( 'div', { class: [prefixCls], }, { default: () => h( ArrayBase, { props: { keyMap, }, }, { default: () => [ renderEmpty(), renderItems(), renderAddition(), ], } ), } ) } }, }) ) export const ArrayCards = composeExport(ArrayCardsInner, { Index: ArrayBase.Index, SortHandle: ArrayBase.SortHandle, Addition: ArrayBase.Addition, Remove: ArrayBase.Remove, MoveDown: ArrayBase.MoveDown, MoveUp: ArrayBase.MoveUp, useArray: ArrayBase.useArray, useIndex: ArrayBase.useIndex, useRecord: ArrayBase.useRecord, }) export default ArrayCards ================================================ FILE: packages/element/src/array-cards/style.scss ================================================ @import '../__builtins__/styles/common.scss'; $array-table-prefix-cls: '#{$formily-prefix}-array-cards'; .#{$array-table-prefix-cls} { .el-card__header { padding-top: 12.5px; padding-bottom: 12.5px; } .el-empty { padding: 0; } .#{$array-table-prefix-cls}-item { margin-bottom: 10px; } .#{$formily-prefix}-array-base-addition { width: 100%; border: $--border-width-base dashed $--border-color-base; &:hover { background-color: $--color-white; border-color: $--border-color-hover; } &:active, &:focus { background-color: $--color-white; border-color: $--color-primary; } } } ================================================ FILE: packages/element/src/array-cards/style.ts ================================================ import './style.scss' import 'element-ui/packages/theme-chalk/src/card.scss' import 'element-ui/packages/theme-chalk/src/empty.scss' import 'element-ui/packages/theme-chalk/src/row.scss' // 依赖 import '../array-base/style' ================================================ FILE: packages/element/src/array-collapse/index.ts ================================================ import { ArrayField } from '@formily/core' import { ISchema } from '@formily/json-schema' import { observer } from '@formily/reactive-vue' import { Fragment, h, RecursionField, useField, useFieldSchema, } from '@formily/vue' import type { Collapse as CollapseProps, CollapseItem as CollapseItemProps, } from 'element-ui' import { Badge, Card, Collapse, CollapseItem, Empty, Row } from 'element-ui' import { defineComponent, ref, Ref, watchEffect } from 'vue-demi' import { ArrayBase } from '../array-base' import { stylePrefix } from '../__builtins__/configs' import { composeExport } from '../__builtins__/shared' export interface IArrayCollapseProps extends CollapseProps { defaultOpenPanelCount?: number } const isAdditionComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Addition') > -1 } const isIndexComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Index') > -1 } const isRemoveComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Remove') > -1 } const isMoveUpComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('MoveUp') > -1 } const isMoveDownComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('MoveDown') > -1 } const isOperationComponent = (schema: ISchema) => { return ( isAdditionComponent(schema) || isRemoveComponent(schema) || isMoveDownComponent(schema) || isMoveUpComponent(schema) ) } const range = (count: number) => Array.from({ length: count }).map((_, i) => i) const takeDefaultActiveKeys = ( dataSourceLength: number, defaultOpenPanelCount: number, accordion = false ) => { if (accordion) { return 0 } if (dataSourceLength < defaultOpenPanelCount) return range(dataSourceLength) return range(defaultOpenPanelCount) } const insertActiveKeys = ( activeKeys: number[] | number, index: number, accordion = false ) => { if (accordion) return index if ((activeKeys as number[]).length <= index) return (activeKeys as number[]).concat(index) return (activeKeys as number[]).reduce((buf, key) => { if (key < index) return buf.concat(key) if (key === index) return buf.concat([key, key + 1]) return buf.concat(key + 1) }, []) } export const ArrayCollapseInner = observer( defineComponent({ name: 'FArrayCollapse', props: { defaultOpenPanelCount: { type: Number, default: 5, }, }, setup(props, { attrs }) { const fieldRef = useField() const schemaRef = useFieldSchema() const prefixCls = `${stylePrefix}-array-collapse` const activeKeys: Ref = ref([]) watchEffect(() => { const field = fieldRef.value const dataSource = Array.isArray(field.value) ? field.value.slice() : [] if (!field.modified && dataSource.length) { activeKeys.value = takeDefaultActiveKeys( dataSource.length, props.defaultOpenPanelCount, attrs.accordion as boolean ) } }) const { getKey, keyMap } = ArrayBase.useKey(schemaRef.value) return () => { const field = fieldRef.value const schema = schemaRef.value const dataSource = Array.isArray(field.value) ? field.value.slice() : [] if (!schema) throw new Error('can not found schema object') const renderItems = () => { if (!dataSource.length) { return null } const items = dataSource?.map((item, index) => { const items = Array.isArray(schema.items) ? schema.items[index] || schema.items[0] : schema.items const key = getKey(item, index) const panelProps = field .query(`${field.address}.${index}`) .get('componentProps') const props: CollapseItemProps = items['x-component-props'] const headerTitle = panelProps?.title || props.title || field.title const path = field.address.concat(index) const errors = field.form.queryFeedbacks({ type: 'error', address: `${path}.**`, }) const title = h( ArrayBase.Item, { props: { index, record: item, }, }, { default: () => [ h( RecursionField, { props: { schema: items, name: index, filterProperties: (schema) => { if (!isIndexComponent(schema)) return false return true }, onlyRenderProperties: true, }, }, {} ), errors.length ? h( Badge, { class: [`${prefixCls}-errors-badge`], props: { value: errors.length, }, }, { default: () => headerTitle } ) : headerTitle, ], } ) const extra = h( ArrayBase.Item, { props: { index, record: item, }, }, { default: () => [ h( RecursionField, { props: { schema: items, name: index, filterProperties: (schema) => { if (!isOperationComponent(schema)) return false return true }, onlyRenderProperties: true, }, }, {} ), ], } ) const content = h( RecursionField, { props: { schema: items, name: index, filterProperties: (schema) => { if (isIndexComponent(schema)) return false if (isOperationComponent(schema)) return false return true }, }, }, {} ) return h( CollapseItem, { attrs: { ...props, ...panelProps, name: index, }, key, }, { default: () => [ h( ArrayBase.Item, { props: { index, record: item, }, }, { default: () => [content], } ), ], title: () => h( Row, { style: { flex: 1 }, props: { type: 'flex', justify: 'space-between', }, }, { default: () => [ h('span', {}, { default: () => title }), h('span', {}, { default: () => extra }), ], } ), } ) }) return h( Collapse, { class: [`${prefixCls}-item`], attrs: { ...attrs, value: activeKeys.value, }, on: { change: (keys: number[] | number) => { activeKeys.value = keys }, }, }, { default: () => [items], } ) } const renderAddition = () => { return schema.reduceProperties((addition, schema) => { if (isAdditionComponent(schema)) { return h( RecursionField, { props: { schema, name: 'addition', }, }, {} ) } return addition }, null) } const renderEmpty = () => { if (dataSource?.length) return return h( Card, { class: [`${prefixCls}-item`], attrs: { shadow: 'never', ...attrs, header: attrs.title || field.title, }, }, { default: () => h( Empty, { props: { description: 'No Data', imageSize: 100 } }, {} ), } ) } return h( 'div', { class: [prefixCls], }, { default: () => h( ArrayBase, { props: { keyMap, }, on: { add: (index: number) => { activeKeys.value = insertActiveKeys( activeKeys.value, index, attrs.accordion as boolean ) }, }, }, { default: () => [ renderEmpty(), renderItems(), renderAddition(), ], } ), } ) } }, }) ) export const ArrayCollapseItem = defineComponent({ name: 'FArrayCollapseItem', setup(_props, { slots }) { return () => h(Fragment, {}, slots) }, }) export const ArrayCollapse = composeExport(ArrayCollapseInner, { Item: ArrayCollapseItem, Index: ArrayBase.Index, SortHandle: ArrayBase.SortHandle, Addition: ArrayBase.Addition, Remove: ArrayBase.Remove, MoveDown: ArrayBase.MoveDown, MoveUp: ArrayBase.MoveUp, useArray: ArrayBase.useArray, useIndex: ArrayBase.useIndex, useRecord: ArrayBase.useRecord, }) export default ArrayCollapse ================================================ FILE: packages/element/src/array-collapse/style.scss ================================================ @import '../__builtins__/styles/common.scss'; $array-table-prefix-cls: '#{$formily-prefix}-array-collapse'; .#{$array-table-prefix-cls} { .el-card__header { padding-top: 12.5px; padding-bottom: 12.5px; } .el-empty { padding: 0; } .#{$array-table-prefix-cls}-item { margin-bottom: 10px; } .#{$array-table-prefix-cls}-errors-badge { line-height: 1; vertical-align: initial; } .#{$formily-prefix}-array-base-addition { width: 100%; border: $--border-width-base dashed $--border-color-base; &:hover { background-color: $--color-white; border-color: $--border-color-hover; } &:active, &:focus { background-color: $--color-white; border-color: $--color-primary; } } } ================================================ FILE: packages/element/src/array-collapse/style.ts ================================================ import './style.scss' import 'element-ui/packages/theme-chalk/src/empty.scss' import 'element-ui/packages/theme-chalk/src/row.scss' import 'element-ui/packages/theme-chalk/src/collapse.scss' import 'element-ui/packages/theme-chalk/src/collapse-item.scss' import 'element-ui/packages/theme-chalk/src/card.scss' import 'element-ui/packages/theme-chalk/src/badge.scss' // 依赖 import '../array-base/style' ================================================ FILE: packages/element/src/array-items/index.ts ================================================ import { ArrayField } from '@formily/core' import { ISchema } from '@formily/json-schema' import { observer } from '@formily/reactive-vue' import { h, RecursionField, useField, useFieldSchema } from '@formily/vue' import { defineComponent } from 'vue-demi' import { SlickItem, SlickList } from 'vue-slicksort' import { ArrayBase } from '../array-base' import { stylePrefix } from '../__builtins__/configs' import { composeExport } from '../__builtins__/shared' const isAdditionComponent = (schema: ISchema) => { return schema['x-component']?.indexOf('Addition') > -1 } export interface IArrayItemsItemProps { type?: 'card' | 'divide' } const ArrayItemsInner = observer( defineComponent({ name: 'FArrayItems', setup() { const fieldRef = useField() const schemaRef = useFieldSchema() const prefixCls = `${stylePrefix}-array-items` const { getKey, keyMap } = ArrayBase.useKey(schemaRef.value) return () => { const field = fieldRef.value const schema = schemaRef.value const dataSource = Array.isArray(field.value) ? field.value.slice() : [] const renderItems = () => { const items = dataSource?.map((item, index) => { const items = Array.isArray(schema.items) ? schema.items[index] || schema.items[0] : schema.items const key = getKey(item, index) return h( ArrayBase.Item, { key, props: { index, record: item, }, }, { default: () => h( SlickItem, { class: [`${prefixCls}-item-inner`], props: { index, }, key, }, { default: () => h( RecursionField, { props: { schema: items, name: index, }, }, {} ), } ), } ) }) return h( SlickList, { class: [`${prefixCls}-list`], props: { useDragHandle: true, lockAxis: 'y', helperClass: `${prefixCls}-sort-helper`, value: [], }, on: { 'sort-end': ({ oldIndex, newIndex }) => { if (Array.isArray(keyMap)) { keyMap.splice(newIndex, 0, keyMap.splice(oldIndex, 1)[0]) } field.move(oldIndex, newIndex) }, }, }, { default: () => items } ) } const renderAddition = () => { return schema.reduceProperties((addition, schema) => { if (isAdditionComponent(schema)) { return h( RecursionField, { props: { schema, name: 'addition', }, }, {} ) } return addition }, null) } return h( ArrayBase, { props: { keyMap, }, }, { default: () => h( 'div', { class: [prefixCls], on: { change: () => {}, }, }, { default: () => [renderItems(), renderAddition()], } ), } ) } }, }) ) const ArrayItemsItem = defineComponent({ name: 'FArrayItemsItem', props: ['type'], setup(props, { attrs, slots }) { const prefixCls = `${stylePrefix}-array-items` return () => h( 'div', { class: [`${prefixCls}-${props.type || 'card'}`], attrs: { ...attrs, }, on: { change: () => {}, }, }, slots ) }, }) export const ArrayItems = composeExport(ArrayItemsInner, { Item: ArrayItemsItem, Index: ArrayBase.Index, SortHandle: ArrayBase.SortHandle, Addition: ArrayBase.Addition, Remove: ArrayBase.Remove, MoveDown: ArrayBase.MoveDown, MoveUp: ArrayBase.MoveUp, useArray: ArrayBase.useArray, useIndex: ArrayBase.useIndex, useRecord: ArrayBase.useRecord, }) export default ArrayItems ================================================ FILE: packages/element/src/array-items/style.scss ================================================ @import '../__builtins__/styles/common.scss'; $array-items-prefix-cls: '#{$formily-prefix}-array-items'; .#{$array-items-prefix-cls}-item-inner { visibility: visible; } .#{$array-items-prefix-cls} { .#{$formily-prefix}-array-base-addition { width: 100%; border: $--border-width-base dashed $--border-color-base; &:hover { background-color: $--color-white; border-color: $--border-color-hover; } &:active, &:focus { background-color: $--color-white; border-color: $--color-primary; } } } .#{$array-items-prefix-cls}-card { display: flex; border: 1px solid $--card-border-color; margin-bottom: 10px; padding: 3px 6px; background: $--color-white; justify-content: space-between; .#{$formily-prefix}-form-item:not(.#{$formily-prefix}-form-item-feedback-layout-popover) { margin-bottom: 0 !important; .#{$formily-prefix}-form-item-help { position: absolute; font-size: 12px; top: 100%; background: $--color-white; width: 100%; margin-top: 3px; padding: 3px; z-index: 1; border-radius: 3px; box-shadow: 0 0 10px $--border-color-base; } } } .#{$array-items-prefix-cls}-divide { display: flex; border-bottom: 1px solid $--card-border-color; padding: 10px 0; justify-content: space-between; .#{$formily-prefix}-form-item:not(.#{$formily-prefix}-form-item-feedback-layout-popover) { margin-bottom: 0 !important; .#{$formily-prefix}-form-item-help { position: absolute; font-size: 12px; top: 100%; background: $--color-white; width: 100%; margin-top: 3px; padding: 3px; z-index: 1; border-radius: 3px; box-shadow: 0 0 10px $--card-border-color; } } } ================================================ FILE: packages/element/src/array-items/style.ts ================================================ import './style.scss' // 依赖 import '../array-base/style' ================================================ FILE: packages/element/src/array-table/index.ts ================================================ import { ArrayField, FieldDisplayTypes, GeneralField, IVoidFieldFactoryProps, } from '@formily/core' import type { Schema } from '@formily/json-schema' import { observer } from '@formily/reactive-vue' import { isArr, isBool, isFn } from '@formily/shared' import { Fragment, h, RecursionField as _RecursionField, useField, useFieldSchema, } from '@formily/vue' import type { Pagination as PaginationProps, Table as TableProps, TableColumn as ElColumnProps, } from 'element-ui' import { Badge, Option, Pagination, Select, Table as ElTable, TableColumn as ElTableColumn, } from 'element-ui' import type { Component, VNode } from 'vue' import { computed, defineComponent, inject, provide, ref, Ref } from 'vue-demi' import { ArrayBase } from '../array-base' import { Space } from '../space' import { stylePrefix } from '../__builtins__/configs' import { composeExport } from '../__builtins__/shared' const RecursionField = _RecursionField as unknown as Component interface IArrayTableProps extends TableProps { pagination?: PaginationProps | boolean } interface IArrayTablePaginationProps extends PaginationProps { dataSource?: any[] } interface ObservableColumnSource { field: GeneralField fieldProps: IVoidFieldFactoryProps columnProps: ElColumnProps & { title: string; asterisk: boolean } schema: Schema display: FieldDisplayTypes required: boolean name: string } type ColumnProps = ElColumnProps & { key: string | number asterisk: boolean render?: ( startIndex?: Ref ) => (props: { row: Record column: ElColumnProps $index: number }) => VNode } interface PaginationAction { totalPage?: number pageSize?: number changePage?: (page: number) => void } const PaginationSymbol = Symbol('pagination') const isColumnComponent = (schema: Schema) => { return schema['x-component']?.indexOf('Column') > -1 } const isOperationsComponent = (schema: Schema) => { return schema['x-component']?.indexOf('Operations') > -1 } const isAdditionComponent = (schema: Schema) => { return schema['x-component']?.indexOf('Addition') > -1 } const getArrayTableSources = ( arrayFieldRef: Ref, schemaRef: Ref ) => { const arrayField = arrayFieldRef.value const parseSources = (schema: Schema): ObservableColumnSource[] => { if ( isColumnComponent(schema) || isOperationsComponent(schema) || isAdditionComponent(schema) ) { if (!schema['x-component-props']?.['prop'] && !schema['name']) return [] const name = schema['x-component-props']?.['prop'] || schema['name'] const field = arrayField.query(arrayField.address.concat(name)).take() const fieldProps = field?.props || schema.toFieldProps() const columnProps = (field?.component as any[])?.[1] || schema['x-component-props'] || {} const display = field?.display || schema['x-display'] const required = schema.reduceProperties((required, property) => { if (required) { return required } return !!property.required }, false) return [ { name, display, required, field, fieldProps, schema, columnProps, }, ] } else if (schema.properties) { return schema.reduceProperties((buf: any[], schema) => { return buf.concat(parseSources(schema)) }, []) } else { return [] } } const parseArrayTable = (schema: Schema['items']) => { if (!schema) return [] const sources: ObservableColumnSource[] = [] const items = isArr(schema) ? schema : ([schema] as Schema[]) return items.reduce((columns, schema) => { const item = parseSources(schema) if (item) { return columns.concat(item) } return columns }, sources) } if (!schemaRef.value) throw new Error('can not found schema object') return parseArrayTable(schemaRef.value.items) } const getArrayTableColumns = ( sources: ObservableColumnSource[] ): ColumnProps[] => { return sources.reduce( ( buf: ColumnProps[], { name, columnProps, schema, display, required }, key ) => { const { title, asterisk, ...props } = columnProps if (display !== 'visible') return buf if (!isColumnComponent(schema)) return buf const render = (startIndex?: Ref) => { return columnProps?.type && columnProps?.type !== 'default' ? undefined : (props: { row: Record column: ElColumnProps $index: number }): VNode => { let index = (startIndex?.value ?? 0) + props.$index // const index = reactiveDataSource.value.indexOf(props.row) const children = h( ArrayBase.Item, { props: { index, record: props.row }, key: `${key}${index}` }, { default: () => h( RecursionField, { props: { schema, name: index, onlyRenderProperties: true, }, }, {} ), } ) return children } } return buf.concat({ label: title, ...props, key, prop: name, asterisk: asterisk ?? required, render, }) }, [] ) } const renderAddition = () => { const schema = useFieldSchema() return schema.value.reduceProperties((addition, schema) => { if (isAdditionComponent(schema)) { return h( RecursionField, { props: { schema, name: 'addition', }, }, {} ) } return addition }, null) } const schedulerRequest = { request: null, } const StatusSelect = observer( defineComponent({ props: { value: Number, onChange: Function, options: Array, pageSize: Number, }, setup(props) { const fieldRef = useField() const prefixCls = `${stylePrefix}-array-table` return () => { const field = fieldRef.value const width = String(props.options?.length).length * 15 const errors = field.errors const parseIndex = (address: string) => { return Number( address .slice(address.indexOf(field.address.toString()) + 1) .match(/(\d+)/)?.[1] ) } return h( Select, { style: { width: `${width < 60 ? 60 : width}px`, }, class: [ `${prefixCls}-status-select`, { 'has-error': errors?.length, }, ], props: { value: props.value, popperClass: `${prefixCls}-status-select-dropdown`, }, on: { input: props.onChange, }, }, { default: () => { return props.options?.map(({ label, value }) => { const hasError = errors.some(({ address }) => { const currentIndex = parseIndex(address) const startIndex = (value - 1) * props.pageSize const endIndex = value * props.pageSize return currentIndex >= startIndex && currentIndex <= endIndex }) return h( Option, { key: value, props: { label, value, }, }, { default: () => { if (hasError) { return h( Badge, { props: { isDot: true, }, }, { default: () => label } ) } return label }, } ) }) }, } ) } }, }), { scheduler: (update) => { clearTimeout(schedulerRequest.request) schedulerRequest.request = setTimeout(() => { update() }, 100) }, } ) const usePagination = () => { return inject>(PaginationSymbol, ref({})) } const ArrayTablePagination = defineComponent({ inheritAttrs: false, props: ['pageSize', 'dataSource'], setup(props, { attrs, slots }) { const prefixCls = `${stylePrefix}-array-table` const current = ref(1) const pageSize = computed(() => props.pageSize || 10) const dataSource = computed(() => props.dataSource || []) const startIndex = computed(() => (current.value - 1) * pageSize.value) const endIndex = computed(() => startIndex.value + pageSize.value - 1) const total = computed(() => dataSource.value?.length || 0) const totalPage = computed(() => Math.ceil(total.value / pageSize.value)) const pages = computed(() => { return Array.from(new Array(totalPage.value)).map((_, index) => { const page = index + 1 return { label: page, value: page, } }) }) const renderPagination = function () { if (totalPage.value <= 1) return return h( 'div', { class: [`${prefixCls}-pagination`], }, { default: () => h( Space, {}, { default: () => [ h( StatusSelect, { props: { value: current.value, onChange: (val: number) => { current.value = val }, pageSize: pageSize.value, options: pages.value, }, }, {} ), h( Pagination, { props: { background: true, layout: 'prev, pager, next', ...attrs, pageSize: pageSize.value, pageCount: totalPage.value, currentPage: current.value, }, on: { 'current-change': (val: number) => { current.value = val }, }, }, {} ), ], } ), } ) } const paginationContext = computed(() => { return { totalPage: totalPage.value, pageSize: pageSize.value, changePage: (page: number) => (current.value = page), } }) provide(PaginationSymbol, paginationContext) return () => { return h( Fragment, {}, { default: () => slots?.default?.( dataSource.value?.slice(startIndex.value, endIndex.value + 1), renderPagination, startIndex ), } ) } }, }) const ArrayTableInner = observer( defineComponent({ name: 'FArrayTable', inheritAttrs: false, setup(props, { attrs, listeners, slots }) { const fieldRef = useField() const schemaRef = useFieldSchema() const prefixCls = `${stylePrefix}-array-table` const { getKey, keyMap } = ArrayBase.useKey(schemaRef.value) const defaultRowKey = (record: any) => { return getKey(record) } return () => { const props = attrs as unknown as IArrayTableProps const field = fieldRef.value const dataSource = Array.isArray(field.value) ? field.value.slice() : [] const pagination = props.pagination const sources = getArrayTableSources(fieldRef, schemaRef) const columns = getArrayTableColumns(sources) const renderColumns = (startIndex?: Ref) => { return columns.map(({ key, render, asterisk, ...props }) => { const children = {} as Record if (render) { children.default = render(startIndex) } if (asterisk) { children.header = ({ column }: { column: ElColumnProps }) => h( 'span', {}, { default: () => [ h( 'span', { class: `${prefixCls}-asterisk` }, { default: () => ['*'] } ), column.label, ], } ) } return h( ElTableColumn, { key, props, }, children ) }) } const renderStateManager = () => sources.map((column, key) => { //专门用来承接对Column的状态管理 if (!isColumnComponent(column.schema)) return return h( RecursionField, { props: { name: column.name, schema: column.schema, onlyRenderSelf: true, }, key, }, {} ) }) const renderTable = ( dataSource?: any[], pager?: () => VNode, startIndex?: Ref ) => { return h( 'div', { class: prefixCls }, { default: () => h( ArrayBase, { props: { keyMap, }, }, { default: () => [ h( ElTable, { props: { rowKey: defaultRowKey, ...attrs, data: dataSource, }, on: listeners, }, { ...slots, default: () => renderColumns(startIndex), } ), pager?.(), renderStateManager(), renderAddition(), ], } ), } ) } if (!pagination) { return renderTable(dataSource, null) } return h( ArrayTablePagination, { attrs: { ...(isBool(pagination) ? {} : pagination), dataSource, }, }, { default: renderTable } ) } }, }) ) const ArrayTableColumn: Component = { name: 'FArrayTableColumn', render(h) { return h() }, } const ArrayAddition = defineComponent({ name: 'ArrayAddition', setup(props, { attrs, listeners, slots }) { const array = ArrayBase.useArray() const paginationRef = usePagination() const onClick = listeners['click'] listeners['click'] = (e) => { const { totalPage = 0, pageSize = 10, changePage } = paginationRef.value // 如果添加数据后超过当前页,则自动切换到下一页 const total = array?.field?.value?.value.length || 0 if (total === (totalPage - 1) * pageSize + 1 && isFn(changePage)) { changePage(totalPage) } if (onClick) onClick(e) } return () => { return h( ArrayBase.Addition, { props, attrs, on: listeners, }, slots ) } }, }) export const ArrayTable = composeExport(ArrayTableInner, { Column: ArrayTableColumn, Index: ArrayBase.Index, SortHandle: ArrayBase.SortHandle, Addition: ArrayAddition, Remove: ArrayBase.Remove, MoveDown: ArrayBase.MoveDown, MoveUp: ArrayBase.MoveUp, useArray: ArrayBase.useArray, useIndex: ArrayBase.useIndex, useRecord: ArrayBase.useRecord, }) export default ArrayTable ================================================ FILE: packages/element/src/array-table/style.scss ================================================ @import '../__builtins__/styles/common.scss'; $array-table-prefix-cls: '#{$formily-prefix}-array-table'; .#{$array-table-prefix-cls} { .#{$formily-prefix}-form-item:not(.#{$formily-prefix}-form-item-feedback-layout-popover) { margin-bottom: 0 !important; } &-status-select-dropdown { .#{$namespace}-badge { line-height: 1; } } &-pagination { display: flex; justify-content: center; margin-top: 8px; .#{$array-table-prefix-cls}-status-select.has-error { .#{$namespace}-input__inner { border-color: $--color-danger !important; } } } .#{$namespace}-table { .cell { overflow: visible; } .cell.el-tooltip { overflow: hidden; } &__fixed { box-shadow: 10px 0 10px -10px rgb(0 0 0 / 12%); } &__fixed-right { box-shadow: -10px 0 10px -10px rgb(0 0 0 / 12%); } } .#{$formily-prefix}-form-item-help { position: absolute; font-size: 12px; top: 100%; background: #fff; width: 100%; margin-top: 3px; padding: 3px; z-index: 2; border-radius: 3px; box-shadow: 0 0 10px #eee; } .#{$formily-prefix}-array-base-addition { margin-top: 8px; width: 100%; border: $--border-width-base dashed $--border-color-base; &:hover { background-color: $--color-white; border-color: $--border-color-hover; } &:active, &:focus { background-color: $--color-white; border-color: $--color-primary; } } .#{$formily-prefix}-form-item-feedback-layout-popover { margin-bottom: 0; } &-inner-asterisk { color: $--color-danger; font-weight: $--font-weight-primary; } } ================================================ FILE: packages/element/src/array-table/style.ts ================================================ import './style.scss' import 'element-ui/packages/theme-chalk/src/table.scss' import 'element-ui/packages/theme-chalk/src/table-column.scss' import 'element-ui/packages/theme-chalk/src/button.scss' import 'element-ui/packages/theme-chalk/src/select.scss' import 'element-ui/packages/theme-chalk/src/badge.scss' // 依赖 import '../array-base/style' import '../space/style' ================================================ FILE: packages/element/src/array-tabs/index.ts ================================================ import { ArrayField } from '@formily/core' import { observer } from '@formily/reactive-vue' import { h, RecursionField, useField, useFieldSchema } from '@formily/vue' import { Badge, TabPane, Tabs } from 'element-ui' import { defineComponent, ref } from 'vue-demi' import { stylePrefix } from '../__builtins__/configs' import type { Tabs as TabsProps } from 'element-ui' export const ArrayTabs = observer( defineComponent({ name: 'ArrayTabs', props: [], setup(props, { attrs, listeners }) { const fieldRef = useField() const schemaRef = useFieldSchema() const prefixCls = `${stylePrefix}-array-tabs` const activeKey = ref('tab-0') return () => { const field = fieldRef.value const schema = schemaRef.value const value = Array.isArray(field.value) ? field.value : [] const dataSource = value?.length ? value : [{}] const onEdit = (targetKey: any, type: 'add' | 'remove') => { if (type == 'add') { const id = dataSource.length if (field?.value?.length) { field.push(null) } else { field.push(null, null) } activeKey.value = `tab-${id}` } else if (type == 'remove') { const index = targetKey.match(/-(\d+)/)?.[1] field.remove(Number(index)) if (activeKey.value === targetKey) { activeKey.value = `tab-${index - 1}` } } } const badgedTab = (index: number) => { const tab = `${field.title || 'Untitled'} ${index + 1}` const path = field.address.concat(index) const errors = field.form.queryFeedbacks({ type: 'error', address: `${path}.**`, }) if (errors.length) { return h( 'span', {}, { default: () => [ h( Badge, { class: [`${prefixCls}-errors-badge`], props: { value: errors.length, }, }, { default: () => [tab], } ), ], } ) } return h( 'span', {}, { default: () => [tab], } ) } const renderItems = () => dataSource?.map((item, index) => { const items = Array.isArray(schema.items) ? schema.items[index] : schema.items const key = `tab-${index}` return h( TabPane, { key, attrs: { closable: index !== 0, name: key, }, }, { default: () => h( RecursionField, { props: { schema: items, name: index, }, }, {} ), label: () => [badgedTab(index)], } ) }) return h( Tabs, { class: [prefixCls], attrs: { ...attrs, type: 'card', value: activeKey.value, addable: true, }, on: { ...listeners, input: (key) => { activeKey.value = key }, 'tab-remove': (target) => { onEdit(target, 'remove') listeners?.['tab-remove']?.(target) }, 'tab-add': () => { onEdit(null, 'add') listeners?.['tab-add']?.() }, }, }, { default: () => [renderItems()], } ) } }, }) ) export default ArrayTabs ================================================ FILE: packages/element/src/array-tabs/style.scss ================================================ @import '../__builtins__/styles/common.scss'; $array-table-prefix-cls: '#{$formily-prefix}-array-tabs'; .#{$array-table-prefix-cls} { .#{$formily-prefix}-array-tabs-addition { position: absolute; right: -56px; top: -1px; } .#{$array-table-prefix-cls}-errors-badge { line-height: 1; vertical-align: initial; } } ================================================ FILE: packages/element/src/array-tabs/style.ts ================================================ import './style.scss' import 'element-ui/packages/theme-chalk/src/tabs.scss' import 'element-ui/packages/theme-chalk/src/tab-pane.scss' import 'element-ui/packages/theme-chalk/src/badge.scss' import 'element-ui/packages/theme-chalk/src/button.scss' ================================================ FILE: packages/element/src/cascader/index.ts ================================================ import { connect, mapProps, mapReadPretty } from '@formily/vue' import { Cascader as ELCascader } from 'element-ui' import type { Cascader as ElCascaderProps } from 'element-ui' import { PreviewText } from '../preview-text' export type CascaderProps = ElCascaderProps export const Cascader = connect( ELCascader, mapProps({ dataSource: 'options' }), mapReadPretty(PreviewText.Cascader) ) export default Cascader ================================================ FILE: packages/element/src/cascader/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/cascader.scss' // 依赖 import '../preview-text/style' ================================================ FILE: packages/element/src/checkbox/index.ts ================================================ import { connect, h, mapProps, mapReadPretty } from '@formily/vue' import type { Checkbox as _ElCheckboxProps, CheckboxGroup as ElCheckboxGroupProps, } from 'element-ui' import { Checkbox as ElCheckbox, CheckboxButton as ElCheckboxButton, CheckboxGroup as ElCheckboxGroup, } from 'element-ui' import { defineComponent, PropType } from 'vue-demi' import { PreviewText } from '../preview-text' import { composeExport, resolveComponent, SlotTypes, transformComponent, } from '../__builtins__/shared' type ElCheckboxProps = Omit<_ElCheckboxProps, 'value'> & { value: ElCheckboxProps['label'] } export interface CheckboxProps extends ElCheckboxProps { option: Omit<_ElCheckboxProps, 'value'> & { value: ElCheckboxProps['label'] label: SlotTypes } } const CheckboxOption = defineComponent({ name: 'Checkbox', inheritAttrs: false, props: { option: { type: Object, default: null, }, }, setup(curtomProps, { attrs, slots, listeners }) { return () => { const props = attrs as unknown as CheckboxProps const option = curtomProps?.option if (option) { const children = { default: () => [ resolveComponent(slots.default ?? option.label, { option }), ], } const newProps = {} as Partial Object.assign(newProps, option) newProps.label = option.value delete newProps.value return h( attrs.optionType === 'button' ? ElCheckboxButton : ElCheckbox, { attrs: { ...newProps, }, }, children ) } return h( ElCheckbox, { attrs: { ...props, }, on: listeners, }, slots ) } }, }) export type CheckboxGroupProps = ElCheckboxGroupProps & { value: any[] options?: Array optionType: 'default' | 'button' } const TransformElCheckboxGroup = transformComponent(ElCheckboxGroup, { change: 'input', uselessChange: 'change' }) const CheckboxGroupOption = defineComponent({ name: 'CheckboxGroup', props: { options: { type: Array, default: () => [], }, optionType: { type: String as PropType, default: 'default', }, }, setup(customProps, { attrs, slots, listeners }) { return () => { const options = customProps.options || [] const children = options.length !== 0 ? { default: () => options.map((option) => { if (typeof option === 'string') { return h( Checkbox, { props: { option: { label: option, value: option, }, }, attrs: { optionType: customProps.optionType, }, }, slots?.option ? { default: () => slots.option({ option }) } : {} ) } else { return h( Checkbox, { props: { option, }, attrs: { optionType: customProps.optionType, }, }, slots?.option ? { default: () => slots.option({ option }) } : {} ) } }), } : slots return h( TransformElCheckboxGroup, { attrs: { ...attrs, }, on: listeners, }, children ) } }, }) const CheckboxGroup = connect( CheckboxGroupOption, mapProps({ dataSource: 'options' }), mapReadPretty(PreviewText.Select, { multiple: true, }) ) export const Checkbox = composeExport(connect(CheckboxOption), { Group: CheckboxGroup, }) export default Checkbox ================================================ FILE: packages/element/src/checkbox/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/checkbox.scss' import 'element-ui/packages/theme-chalk/src/checkbox-group.scss' import 'element-ui/packages/theme-chalk/src/checkbox-button.scss' // 依赖 import '../preview-text/style' ================================================ FILE: packages/element/src/date-picker/index.ts ================================================ import { transformComponent } from '../__builtins__/shared' import { connect, mapProps, mapReadPretty } from '@formily/vue' import type { DatePicker as ElDatePickerProps } from 'element-ui' import { DatePicker as ElDatePicker } from 'element-ui' import { PreviewText } from '../preview-text' export type DatePickerProps = ElDatePickerProps const TransformElDatePicker = transformComponent( ElDatePicker, { change: 'input', } ) const getDefaultFormat = (props, formatType = 'format') => { const type = props.type if (type === 'week' && formatType === 'format') { return 'yyyy-WW' } else if (type === 'month') { return 'yyyy-MM' } else if (type === 'year') { return 'yyyy' } else if (type === 'datetime' || type === 'datetimerange') { return 'yyyy-MM-dd HH:mm:ss' } return 'yyyy-MM-dd' } export const DatePicker = connect( TransformElDatePicker, mapProps({ readOnly: 'readonly' }, (props) => { return { ...props, format: props.format || getDefaultFormat(props), valueFormat: props.valueFormat || getDefaultFormat(props, 'valueFormat'), } }), mapReadPretty(PreviewText.DatePicker) ) export default DatePicker ================================================ FILE: packages/element/src/date-picker/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/date-picker.scss' // 依赖 import '../preview-text/style' ================================================ FILE: packages/element/src/editable/index.ts ================================================ import { Field, isVoidField } from '@formily/core' import { reaction } from '@formily/reactive' import { observer } from '@formily/reactive-vue' import { h, useField } from '@formily/vue' import { Popover } from 'element-ui' import { defineComponent, onBeforeUnmount, ref } from 'vue-demi' import { stylePrefix } from '../__builtins__/configs' import type { Popover as PopoverProps } from 'element-ui' import { FormBaseItem, FormItemProps } from '../form-item' import { composeExport, useCompatRef } from '../__builtins__/shared' export type EditableProps = FormItemProps export type EditablePopoverProps = PopoverProps const getParentPattern = (fieldRef) => { const field = fieldRef.value return field?.parent?.pattern || field?.form?.pattern } const getFormItemProps = (fieldRef): FormItemProps => { const field = fieldRef.value if (isVoidField(field)) return {} if (!field) return {} const takeMessage = () => { if (field.selfErrors.length) return field.selfErrors[0] if (field.selfWarnings.length) return field.selfWarnings[0] if (field.selfSuccesses.length) return field.selfSuccesses[0] } return { feedbackStatus: field.validateStatus === 'validating' ? 'pending' : field.validateStatus, feedbackText: takeMessage(), extra: field.description, } } const EditableInner = observer( defineComponent({ name: 'FEditable', setup(props, { attrs, slots, refs }) { const fieldRef = useField() const { elRef: innerRef, elRefBinder } = useCompatRef(refs) const prefixCls = `${stylePrefix}-editable` const setEditable = (payload: boolean) => { const pattern = getParentPattern(fieldRef) if (pattern !== 'editable') return fieldRef.value.setPattern(payload ? 'editable' : 'readPretty') } const dispose = reaction( () => { const pattern = getParentPattern(fieldRef) return pattern }, (pattern) => { if (pattern === 'editable') { fieldRef.value.setPattern('readPretty') } }, { fireImmediately: true, } ) onBeforeUnmount(dispose) return () => { const field = fieldRef.value const editable = field.pattern === 'editable' const pattern = getParentPattern(fieldRef) const itemProps = getFormItemProps(fieldRef) const recover = () => { if (editable && !fieldRef.value?.errors?.length) { setEditable(false) } } const onClick = (e: MouseEvent) => { const target = e.target as HTMLElement const close = innerRef.value.querySelector(`.${prefixCls}-close-btn`) if (target?.contains(close) || close?.contains(target)) { recover() } else if (!editable) { setTimeout(() => { setEditable(true) setTimeout(() => { innerRef.value.querySelector('input')?.focus() }) }) } } const renderEditHelper = () => { if (editable) return null return h( FormBaseItem, { attrs: { ...attrs, ...itemProps, }, }, { default: () => { return h( 'i', { class: [ `${prefixCls}-edit-btn`, pattern === 'editable' ? 'el-icon-edit' : 'el-icon-chat-dot-round', ], }, {} ) }, } ) } const renderCloseHelper = () => { if (!editable) return null return h( FormBaseItem, { attrs: { ...attrs, }, }, { default: () => { return h( 'i', { class: [`${prefixCls}-close-btn`, 'el-icon-close'], }, {} ) }, } ) } return h( 'div', { class: prefixCls, ref: elRefBinder, on: { click: onClick, }, }, { default: () => h( 'div', { class: `${prefixCls}-content`, }, { default: () => [ h( FormBaseItem, { attrs: { ...attrs, ...itemProps, }, }, slots ), renderEditHelper(), renderCloseHelper(), ], } ), } ) } }, }) ) const EditablePopover = observer( defineComponent({ name: 'FEditablePopover', setup(props, { attrs, slots }) { const fieldRef = useField() const prefixCls = `${stylePrefix}-editable` const visible = ref(false) return () => { const field = fieldRef.value const pattern = getParentPattern(fieldRef) return h( Popover, { class: [prefixCls], attrs: { ...attrs, title: attrs.title || field.title, value: visible.value, trigger: 'click', }, on: { input: (value) => { visible.value = value }, }, }, { default: () => [slots.default()], reference: () => h( FormBaseItem, { class: [`${prefixCls}-trigger`] }, { default: () => h( 'div', { class: [`${prefixCls}-content`], }, { default: () => [ h( 'span', { class: [`${prefixCls}-preview`], }, { default: () => [attrs.title || field.title], } ), h( 'i', { class: [ `${prefixCls}-edit-btn`, pattern === 'editable' ? 'el-icon-edit' : 'el-icon-chat-dot-round', ], }, {} ), ], } ), } ), } ) } }, }) ) export const Editable = composeExport(EditableInner, { Popover: EditablePopover, }) export default Editable ================================================ FILE: packages/element/src/editable/style.scss ================================================ @import '../__builtins__/styles/common.scss'; $editable-prefix-cls: '#{$formily-prefix}-editable'; .#{$editable-prefix-cls} { cursor: pointer; display: inline-block !important; .#{$formily-prefix}-form-text { .#{$formily-prefix}-tag { transition: none !important; } .#{$formily-prefix}-tag:last-child { margin-right: 0 !important; } } &-content { display: flex; align-items: center; > * { margin-right: 3px; &:last-child { margin-right: 0; } } } .#{$editable-prefix-cls}-edit-btn, .#{$editable-prefix-cls}-close-btn { transition: all 0.25s ease-in-out; &:hover { color: $--color-primary; } } .#{$formily-prefix}-form-text { display: flex; align-items: center; } .#{$editable-prefix-cls}-preview { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; word-break: break-all; max-width: 100px; display: block; } } ================================================ FILE: packages/element/src/editable/style.ts ================================================ import './style.scss' import 'element-ui/packages/theme-chalk/src/popover.scss' // 依赖 import '../form-item/style' ================================================ FILE: packages/element/src/el-form/index.ts ================================================ import { Form } from '@formily/core' import { FormProvider as _FormProvider, createForm } from '@formily/vue' import type { Form as _ElFormProps } from 'element-ui' import type { FunctionalComponentOptions, Component } from 'vue' import { Form as ElFormComponent } from 'element-ui' const FormProvider = _FormProvider as unknown as Component export type ElFormProps = _ElFormProps & { form?: Form component: Component onAutoSubmit?: (values: any) => any } export const ElForm: FunctionalComponentOptions = { functional: true, render(h, context) { const { form = createForm({}), component = ElFormComponent, onAutoSubmit = context.listeners?.autoSubmit, ...props } = context.props const submitHandler = ( Array.isArray(onAutoSubmit) ? onAutoSubmit[0] : onAutoSubmit ) as (values: any) => any return h(FormProvider, { props: { form } }, [ h( component, { ...context.data, props, nativeOn: { submit: (e: Event) => { e?.stopPropagation?.() e?.preventDefault?.() form.submit(submitHandler) }, }, }, context.children ), ]) }, } export default ElForm ================================================ FILE: packages/element/src/el-form/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/form.scss' ================================================ FILE: packages/element/src/el-form-item/index.ts ================================================ import { isVoidField } from '@formily/core' import { connect, mapProps } from '@formily/vue' import type { FormItem as _ElFormItemProps } from 'element-ui' import { FormItem as ElFormItemComponent } from 'element-ui' export type ElFormItemProps = _ElFormItemProps & { title: string } export const ElFormItem = connect( ElFormItemComponent, mapProps({ title: 'label', required: true }, (props, field) => ({ error: !isVoidField(field) ? field.errors.length ? field.errors.join(',') : undefined : undefined, })) ) export default ElFormItem ================================================ FILE: packages/element/src/el-form-item/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/form-item.scss' ================================================ FILE: packages/element/src/form/index.ts ================================================ import { Form as FormType, IFormFeedback } from '@formily/core' import { FormProvider as _FormProvider, h, useForm } from '@formily/vue' import { Component, VNode } from 'vue' import { defineComponent } from 'vue-demi' import { FormLayout, FormLayoutProps } from '../form-layout' import { PreviewText } from '../preview-text' const FormProvider = _FormProvider as unknown as Component export interface FormProps extends FormLayoutProps { form?: FormType component?: Component previewTextPlaceholder: string | (() => VNode) onAutoSubmit?: (values: any) => any onAutoSubmitFailed?: (feedbacks: IFormFeedback[]) => void } export const Form = defineComponent({ name: 'FForm', props: [ 'form', 'component', 'previewTextPlaceholder', 'onAutoSubmit', 'onAutoSubmitFailed', ], setup(props, { attrs, slots, listeners }) { const top = useForm() return () => { const { form, component = 'form', onAutoSubmit = listeners?.autoSubmit, onAutoSubmitFailed = listeners?.autoSubmitFailed, previewTextPlaceholder = slots?.previewTextPlaceholder, } = props const renderContent = (form: FormType) => { return h( PreviewText.Placeholder, { props: { value: previewTextPlaceholder, }, }, { default: () => [ h( FormLayout, { attrs: { ...attrs, }, }, { default: () => [ h( component, { on: { submit: (e: Event) => { e?.stopPropagation?.() e?.preventDefault?.() form .submit(onAutoSubmit as (e: any) => void) .catch(onAutoSubmitFailed as (e: any) => void) }, }, }, slots ), ], } ), ], } ) } if (form) { return h( FormProvider, { props: { form } }, { default: () => renderContent(form), } ) } if (!top.value) throw new Error('must pass form instance by createForm') return renderContent(top.value) } }, }) export default Form ================================================ FILE: packages/element/src/form/style.scss ================================================ ================================================ FILE: packages/element/src/form/style.ts ================================================ // 依赖 import '../preview-text/style' import '../form-layout/style' ================================================ FILE: packages/element/src/form-button-group/index.ts ================================================ import { h } from '@formily/vue' import { defineComponent } from 'vue-demi' import { FormBaseItem } from '../form-item' import { Space, SpaceProps } from '../space' import { stylePrefix } from '../__builtins__/configs' export type FormButtonGroupProps = Omit & { align?: 'left' | 'right' | 'center' gutter?: number className?: string alignFormItem: boolean } export const FormButtonGroup = defineComponent({ name: 'FFormButtonGroup', props: { align: { type: String, default: 'left', }, gutter: { type: Number, default: 8, }, alignFormItem: { type: Boolean, default: false, }, }, setup(props, { slots, attrs }) { const prefixCls = `${stylePrefix}-form-button-group` return () => { if (props.alignFormItem) { return h( FormBaseItem, { style: { margin: 0, padding: 0, width: '100%', }, attrs: { colon: false, label: ' ', ...attrs, }, }, { default: () => h(Space, { props: { size: props.gutter } }, slots), } ) } else { return h( Space, { class: [prefixCls], style: { justifyContent: props.align === 'left' ? 'flex-start' : props.align === 'right' ? 'flex-end' : 'center', display: 'flex', }, props: { ...attrs, size: props.gutter, }, attrs, }, slots ) } } }, }) export default FormButtonGroup ================================================ FILE: packages/element/src/form-button-group/style.scss ================================================ ================================================ FILE: packages/element/src/form-button-group/style.ts ================================================ import './style.scss' // 依赖 import '../form-item/style' import '../space/style' ================================================ FILE: packages/element/src/form-collapse/index.ts ================================================ import { Collapse, CollapseItem, Badge } from 'element-ui' import { model } from '@formily/reactive' import type { Collapse as CollapseProps, CollapseItem as CollapseItemProps, } from 'element-ui' import { useField, useFieldSchema, RecursionField, h, Fragment, } from '@formily/vue' import { observer } from '@formily/reactive-vue' import { Schema, SchemaKey } from '@formily/json-schema' import { composeExport, stylePrefix } from '../__builtins__' import { toArr } from '@formily/shared' import { computed, defineComponent, PropType } from 'vue-demi' import { GeneralField } from '@formily/core' type ActiveKeys = string | number | Array type ActiveKey = string | number type Panels = { name: SchemaKey; props: any; schema: Schema }[] export interface IFormCollapse { activeKeys: ActiveKeys hasActiveKey(key: ActiveKey): boolean setActiveKeys(key: ActiveKeys): void addActiveKey(key: ActiveKey): void removeActiveKey(key: ActiveKey): void toggleActiveKey(key: ActiveKey): void } export interface IFormCollapseProps extends CollapseProps { formCollapse?: IFormCollapse activeKey?: ActiveKey } const usePanels = (collapseField: GeneralField, schema: Schema) => { const panels: Panels = [] schema.mapProperties((schema, name) => { const field = collapseField.query(collapseField.address.concat(name)).take() if (field?.display === 'none' || field?.display === 'hidden') return if (schema['x-component']?.indexOf('FormCollapse.Item') > -1) { panels.push({ name, props: { ...schema?.['x-component-props'], key: schema?.['x-component-props']?.key || name, }, schema, }) } }) return panels } const createFormCollapse = (defaultActiveKeys?: ActiveKeys) => { const formCollapse = model({ activeKeys: defaultActiveKeys, setActiveKeys(keys: ActiveKeys) { formCollapse.activeKeys = keys }, hasActiveKey(key: ActiveKey) { if (Array.isArray(formCollapse.activeKeys)) { if (formCollapse.activeKeys.includes(key)) { return true } } else if (formCollapse.activeKeys == key) { return true } return false }, addActiveKey(key: ActiveKey) { if (formCollapse.hasActiveKey(key)) return formCollapse.activeKeys = toArr(formCollapse.activeKeys).concat(key) }, removeActiveKey(key: ActiveKey) { if (Array.isArray(formCollapse.activeKeys)) { formCollapse.activeKeys = formCollapse.activeKeys.filter( (item) => item != key ) } else { formCollapse.activeKeys = '' } }, toggleActiveKey(key: ActiveKey) { if (formCollapse.hasActiveKey(key)) { formCollapse.removeActiveKey(key) } else { formCollapse.addActiveKey(key) } }, }) return formCollapse } const FormCollapse = observer( defineComponent({ inheritAttrs: false, props: { formCollapse: { type: Object as PropType }, activeKey: { type: [String, Number], }, }, setup(props, { attrs, emit }) { const field = useField() const schema = useFieldSchema() const prefixCls = `${stylePrefix}-form-collapse` const _formCollapse = computed( () => props.formCollapse ?? createFormCollapse() ) const takeActiveKeys = (panels: Panels) => { if (props.activeKey) return props.activeKey if (_formCollapse.value?.activeKeys) return _formCollapse.value?.activeKeys if (attrs.accordion) return panels[0]?.name return panels.map((item) => item.name) } const badgedHeader = (key: SchemaKey, props: any) => { const errors = field.value.form.queryFeedbacks({ type: 'error', address: `${field.value.address.concat(key)}.*`, }) if (errors.length) { return h( Badge, { class: [`${prefixCls}-errors-badge`], props: { value: errors.length, }, }, { default: () => props.title } ) } return props.title } return () => { const panels = usePanels(field.value, schema.value) const activeKey = takeActiveKeys(panels) return h( Collapse, { class: prefixCls, props: { value: activeKey, }, on: { change: (key: string | string[]) => { emit('input', key) _formCollapse.value.setActiveKeys(key) }, }, }, { default: () => { return panels.map(({ props, schema, name }, index) => { return h( CollapseItem, { key: index, props: { ...props, name, }, }, { default: () => [ h(RecursionField, { props: { schema, name } }, {}), ], title: () => h( 'span', {}, { default: () => badgedHeader(name, props) } ), } ) }) }, } ) } }, }) ) export const FormCollapseItem = defineComponent({ name: 'FFormCollapseItem', setup(_props, { slots }) { return () => h(Fragment, {}, slots) }, }) const composeFormCollapse = composeExport(FormCollapse, { Item: FormCollapseItem, createFormCollapse, }) export { composeFormCollapse as FormCollapse } export default composeFormCollapse ================================================ FILE: packages/element/src/form-collapse/style.scss ================================================ @import '../__builtins__/styles/common.scss'; .#{$formily-prefix}-form-collapse-errors-badge { line-height: 1; vertical-align: initial; } ================================================ FILE: packages/element/src/form-collapse/style.ts ================================================ import './style.scss' import 'element-ui/packages/theme-chalk/src/collapse.scss' import 'element-ui/packages/theme-chalk/src/collapse-item.scss' import 'element-ui/packages/theme-chalk/src/badge.scss' ================================================ FILE: packages/element/src/form-dialog/index.ts ================================================ import { createForm, Form, IFormProps } from '@formily/core' import { toJS } from '@formily/reactive' import { observer } from '@formily/reactive-vue' import { applyMiddleware, IMiddleware, isBool, isFn, isNum, isStr, } from '@formily/shared' import { FormProvider, Fragment, h } from '@formily/vue' import type { Button as ButtonProps, Dialog as DialogProps } from 'element-ui' import { Button, Dialog } from 'element-ui' import { t } from 'element-ui/src/locale' import { Portal, PortalTarget } from 'portal-vue' import Vue, { Component, VNode } from 'vue' import { defineComponent } from 'vue-demi' import { stylePrefix } from '../__builtins__/configs' import { createPortalProvider, getProtalContext, isValidElement, loading, resolveComponent, } from '../__builtins__/shared' type FormDialogContentProps = { form: Form } type FormDialogContent = Component | ((props: FormDialogContentProps) => VNode) type DialogTitle = string | number | Component | VNode | (() => VNode) type IFormDialogProps = Omit & { title?: DialogTitle footer?: null | Component | VNode | (() => VNode) cancelText?: string | Component | VNode | (() => VNode) cancelButtonProps?: ButtonProps okText?: string | Component | VNode | (() => VNode) okButtonProps?: ButtonProps onOpen?: () => void onOpened?: () => void onClose?: () => void onClosed?: () => void onCancel?: () => void onOK?: () => void loadingText?: string } const PORTAL_TARGET_NAME = 'FormDialogFooter' const isDialogTitle = (props: any): props is DialogTitle => { return isNum(props) || isStr(props) || isBool(props) || isValidElement(props) } const getDialogProps = (props: any): IFormDialogProps => { if (isDialogTitle(props)) { return { title: props, } as IFormDialogProps } else { return props } } export interface IFormDialog { forOpen(middleware: IMiddleware): IFormDialog forConfirm(middleware: IMiddleware): IFormDialog forCancel(middleware: IMiddleware): IFormDialog open(props?: IFormProps): Promise close(): void } export interface IFormDialogComponentProps { content: FormDialogContent resolve: () => any reject: () => any } export function FormDialog( title: IFormDialogProps | DialogTitle, content: FormDialogContent ): IFormDialog export function FormDialog( title: IFormDialogProps | DialogTitle, id: string | symbol, content: FormDialogContent ): IFormDialog export function FormDialog( title: DialogTitle, id: string, content: FormDialogContent ): IFormDialog export function FormDialog( title: IFormDialogProps | DialogTitle, id: string | symbol | FormDialogContent, content?: FormDialogContent ): IFormDialog { if (isFn(id) || isValidElement(id)) { content = id as FormDialogContent id = 'form-dialog' } const prefixCls = `${stylePrefix}-form-dialog` const env = { root: document.createElement('div'), form: null, promise: null, instance: null, openMiddlewares: [], confirmMiddlewares: [], cancelMiddlewares: [], } document.body.appendChild(env.root) const props = getDialogProps(title) const dialogProps = { ...props, onClosed: () => { props.onClosed?.() env.instance.$destroy() env.instance = null env.root?.parentNode?.removeChild(env.root) env.root = undefined }, } const component = observer( defineComponent({ setup() { return () => h( Fragment, {}, { default: () => resolveComponent(content, { form: env.form, }), } ) }, }) ) const render = (visible = true, resolve?: () => any, reject?: () => any) => { if (!env.instance) { const ComponentConstructor = observer( Vue.extend({ props: ['dialogProps'], data() { return { visible: false, } }, render() { const { onClose, onClosed, onOpen, onOpened, onOK, onCancel, title, footer, okText, cancelText, okButtonProps, cancelButtonProps, ...dialogProps } = this.dialogProps return h( FormProvider, { props: { form: env.form, }, }, { default: () => h( Dialog, { class: [`${prefixCls}`], attrs: { visible: this.visible, ...dialogProps, }, on: { 'update:visible': (val) => { this.visible = val }, close: () => { onClose?.() }, closed: () => { onClosed?.() }, open: () => { onOpen?.() }, opened: () => { onOpened?.() }, }, }, { default: () => [h(component, {}, {})], title: () => h( 'div', {}, { default: () => resolveComponent(title) } ), footer: () => h( 'div', {}, { default: () => { const FooterProtalTarget = h( PortalTarget, { props: { name: PORTAL_TARGET_NAME, slim: true, }, }, {} ) if (footer === null) { return [null, FooterProtalTarget] } else if (footer) { return [ resolveComponent(footer), FooterProtalTarget, ] } return [ h( Button, { attrs: { ...cancelButtonProps }, on: { click: (e) => { onCancel?.(e) reject() }, }, }, { default: () => resolveComponent( cancelText || t('el.popconfirm.cancelButtonText') ), } ), h( Button, { attrs: { type: 'primary', ...okButtonProps, loading: env.form.submitting, }, on: { click: (e) => { onOK?.(e) resolve() }, }, }, { default: () => resolveComponent( okText || t('el.popconfirm.confirmButtonText') ), } ), FooterProtalTarget, ] }, } ), } ), } ) }, }) ) env.instance = new ComponentConstructor({ propsData: { dialogProps, }, parent: getProtalContext(id as string | symbol), }) env.instance.$mount(env.root) env.root = env.instance.$el } env.instance.visible = visible } const formDialog = { forOpen: (middleware: IMiddleware) => { if (isFn(middleware)) { env.openMiddlewares.push(middleware) } return formDialog }, forConfirm: (middleware: IMiddleware) => { if (isFn(middleware)) { env.confirmMiddlewares.push(middleware) } return formDialog }, forCancel: (middleware: IMiddleware) => { if (isFn(middleware)) { env.cancelMiddlewares.push(middleware) } return formDialog }, open: (props: IFormProps) => { if (env.promise) return env.promise env.promise = new Promise(async (resolve, reject) => { try { props = await loading(dialogProps.loadingText, () => applyMiddleware(props, env.openMiddlewares) ) env.form = env.form || createForm(props) } catch (e) { reject(e) } render( true, () => { env.form .submit(async () => { await applyMiddleware(env.form, env.confirmMiddlewares) resolve(toJS(env.form.values)) if (dialogProps.beforeClose) { setTimeout(() => { dialogProps.beforeClose(() => { formDialog.close() }) }) } else { formDialog.close() } }) .catch(() => {}) }, async () => { await loading(dialogProps.loadingText, () => applyMiddleware(env.form, env.cancelMiddlewares) ) if (dialogProps.beforeClose) { dialogProps.beforeClose(() => { formDialog.close() }) } else { formDialog.close() } } ) }) return env.promise }, close: () => { if (!env.root) return render(false) }, } return formDialog } const FormDialogFooter = defineComponent({ name: 'FFormDialogFooter', setup(props, { slots }) { return () => { return h( Portal, { props: { to: PORTAL_TARGET_NAME, }, }, slots ) } }, }) FormDialog.Footer = FormDialogFooter FormDialog.Portal = createPortalProvider('form-dialog') export default FormDialog ================================================ FILE: packages/element/src/form-dialog/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/dialog.scss' import 'element-ui/packages/theme-chalk/src/button.scss' import 'element-ui/packages/theme-chalk/src/loading.scss' ================================================ FILE: packages/element/src/form-drawer/index.ts ================================================ import { createForm, Form, IFormProps } from '@formily/core' import { toJS } from '@formily/reactive' import { observer } from '@formily/reactive-vue' import { applyMiddleware, IMiddleware, isBool, isFn, isNum, isStr, } from '@formily/shared' import { FormProvider, Fragment, h } from '@formily/vue' import type { Button as ButtonProps, Drawer as DrawerProps } from 'element-ui' import { Button, Drawer } from 'element-ui' import { t } from 'element-ui/src/locale' import { Portal, PortalTarget } from 'portal-vue' import Vue, { Component, VNode } from 'vue' import { defineComponent } from 'vue-demi' import { stylePrefix } from '../__builtins__/configs' import { createPortalProvider, getProtalContext, isValidElement, loading, resolveComponent, } from '../__builtins__/shared' type FormDrawerContentProps = { form: Form } type FormDrawerContent = Component | ((props: FormDrawerContentProps) => VNode) type DrawerTitle = string | number | Component | VNode | (() => VNode) type IFormDrawerProps = Omit & { title?: DrawerTitle footer?: null | Component | VNode | (() => VNode) cancelText?: string | Component | VNode | (() => VNode) cancelButtonProps?: ButtonProps okText?: string | Component | VNode | (() => VNode) okButtonProps?: ButtonProps onOpen?: () => void onOpened?: () => void onClose?: () => void onClosed?: () => void onCancel?: () => void onOK?: () => void loadingText?: string } const PORTAL_TARGET_NAME = 'FormDrawerFooter' const isDrawerTitle = (props: any): props is DrawerTitle => { return isNum(props) || isStr(props) || isBool(props) || isValidElement(props) } const getDrawerProps = (props: any): IFormDrawerProps => { if (isDrawerTitle(props)) { return { title: props, } as IFormDrawerProps } else { return props } } export interface IFormDrawer { forOpen(middleware: IMiddleware): IFormDrawer forConfirm(middleware: IMiddleware): IFormDrawer forCancel(middleware: IMiddleware): IFormDrawer open(props?: IFormProps): Promise close(): void } export interface IFormDrawerComponentProps { content: FormDrawerContent resolve: () => any reject: () => any } export function FormDrawer( title: IFormDrawerProps | DrawerTitle, content: FormDrawerContent ): IFormDrawer export function FormDrawer( title: IFormDrawerProps | DrawerTitle, id: string | symbol, content: FormDrawerContent ): IFormDrawer export function FormDrawer( title: DrawerTitle, id: string, content: FormDrawerContent ): IFormDrawer export function FormDrawer( title: IFormDrawerProps | DrawerTitle, id: string | symbol | FormDrawerContent, content?: FormDrawerContent ): IFormDrawer { if (isFn(id) || isValidElement(id)) { content = id as FormDrawerContent id = 'form-drawer' } const prefixCls = `${stylePrefix}-form-drawer` const env = { root: document.createElement('div'), form: null, promise: null, instance: null, openMiddlewares: [], confirmMiddlewares: [], cancelMiddlewares: [], } document.body.appendChild(env.root) const props = getDrawerProps(title) const drawerProps = { ...props, onClosed: () => { props.onClosed?.() env.instance.$destroy() env.instance = null env.root?.parentNode?.removeChild(env.root) env.root = undefined }, } const component = observer( defineComponent({ setup() { return () => h( Fragment, {}, { default: () => resolveComponent(content, { form: env.form, }), } ) }, }) ) const render = (visible = true, resolve?: () => any, reject?: () => any) => { if (!env.instance) { const ComponentConstructor = Vue.extend({ props: ['drawerProps'], data() { return { visible: false, } }, render() { const { onClose, onClosed, onOpen, onOpened, onOK, onCancel, title, footer, okText, cancelText, okButtonProps, cancelButtonProps, ...drawerProps } = this.drawerProps return h( FormProvider, { props: { form: env.form, }, }, { default: () => h( Drawer, { class: [`${prefixCls}`], attrs: { visible: this.visible, ...drawerProps, }, on: { 'update:visible': (val) => { this.visible = val }, close: () => { onClose?.() }, closed: () => { onClosed?.() }, open: () => { onOpen?.() }, opened: () => { onOpened?.() }, }, }, { default: () => [ h( 'div', { class: [`${prefixCls}-body`], }, { default: () => h(component, {}, {}), } ), h( 'div', { class: [`${prefixCls}-footer`], }, { default: () => { const FooterProtalTarget = h( PortalTarget, { props: { name: PORTAL_TARGET_NAME, slim: true, }, }, {} ) if (footer === null) { return [null, FooterProtalTarget] } else if (footer) { return [ resolveComponent(footer), FooterProtalTarget, ] } return [ h( Button, { attrs: cancelButtonProps, on: { click: (e) => { onCancel?.(e) reject() }, }, }, { default: () => resolveComponent( cancelText || t('el.popconfirm.cancelButtonText') ), } ), h( Button, { attrs: { type: 'primary', ...okButtonProps, }, on: { click: (e) => { onOK?.(e) resolve() }, }, }, { default: () => resolveComponent( okText || t('el.popconfirm.confirmButtonText') ), } ), FooterProtalTarget, ] }, } ), ], title: () => h('div', {}, { default: () => resolveComponent(title) }), } ), } ) }, }) env.instance = new ComponentConstructor({ propsData: { drawerProps, }, parent: getProtalContext(id as string | symbol), }) env.instance.$mount(env.root) } env.instance.visible = visible } const formDrawer = { forOpen: (middleware: IMiddleware) => { if (isFn(middleware)) { env.openMiddlewares.push(middleware) } return formDrawer }, forConfirm: (middleware: IMiddleware) => { if (isFn(middleware)) { env.confirmMiddlewares.push(middleware) } return formDrawer }, forCancel: (middleware: IMiddleware) => { if (isFn(middleware)) { env.cancelMiddlewares.push(middleware) } return formDrawer }, open: (props: IFormProps) => { if (env.promise) return env.promise env.promise = new Promise(async (resolve, reject) => { try { props = await loading(drawerProps.loadingText, () => applyMiddleware(props, env.openMiddlewares) ) env.form = env.form || createForm(props) } catch (e) { reject(e) } render( true, () => { env.form .submit(async () => { await applyMiddleware(env.form, env.confirmMiddlewares) resolve(toJS(env.form.values)) if (drawerProps.beforeClose) { setTimeout(() => { drawerProps.beforeClose(() => { formDrawer.close() }) }) } else { formDrawer.close() } }) .catch(() => {}) }, async () => { await loading(drawerProps.loadingText, () => applyMiddleware(env.form, env.cancelMiddlewares) ) if (drawerProps.beforeClose) { drawerProps.beforeClose(() => { formDrawer.close() }) } else { formDrawer.close() } } ) }) return env.promise }, close: () => { if (!env.root) return render(false) }, } return formDrawer } const FormDrawerFooter = defineComponent({ name: 'FFormDrawerFooter', setup(props, { slots }) { return () => { return h( Portal, { props: { to: PORTAL_TARGET_NAME, }, }, slots ) } }, }) FormDrawer.Footer = FormDrawerFooter FormDrawer.Protal = createPortalProvider('form-drawer') export default FormDrawer ================================================ FILE: packages/element/src/form-drawer/style.scss ================================================ @import '../__builtins__/styles/common.scss'; .#{$formily-prefix}-form-drawer { .el-drawer__body { display: flex; flex-direction: column; } &-body { flex: 1; overflow: auto; padding: $--dialog-padding-primary; } &-footer { padding: $--dialog-padding-primary; display: flex; justify-content: flex-end; align-items: center; } } ================================================ FILE: packages/element/src/form-drawer/style.ts ================================================ import './style.scss' import 'element-ui/packages/theme-chalk/src/drawer.scss' import 'element-ui/packages/theme-chalk/src/button.scss' import 'element-ui/packages/theme-chalk/src/loading.scss' ================================================ FILE: packages/element/src/form-grid/index.ts ================================================ import { Grid, IGridOptions } from '@formily/grid' import { markRaw } from '@formily/reactive' import { observer } from '@formily/reactive-vue' import { h } from '@formily/vue' import { computed, defineComponent, inject, InjectionKey, onMounted, PropType, provide, ref, Ref, watchEffect, } from 'vue-demi' import { useFormLayout } from '../form-layout' import { stylePrefix } from '../__builtins__/configs' import { composeExport } from '../__builtins__/shared' export interface IFormGridProps extends IGridOptions { grid?: Grid prefixCls?: string className?: string style?: React.CSSProperties } const FormGridSymbol: InjectionKey>> = Symbol('FormGridContext') interface GridColumnProps { gridSpan: number } export const createFormGrid = (props: IFormGridProps): Grid => { return markRaw(new Grid(props)) } export const useFormGrid = (): Ref> => inject(FormGridSymbol) /** * @deprecated */ const useGridSpan = (gridSpan: number) => { return gridSpan } /** * @deprecated */ export const useGridColumn = (gridSpan = 1) => { return gridSpan } const FormGridInner = observer( defineComponent({ name: 'FFormGrid', props: { columnGap: { type: Number, }, rowGap: { type: Number, }, minColumns: { type: [Number, Array], }, minWidth: { type: [Number, Array], }, maxColumns: { type: [Number, Array], }, maxWidth: { type: [Number, Array], }, breakpoints: { type: Array, }, colWrap: { type: Boolean, default: true, }, strictAutoFit: { type: Boolean, default: false, }, shouldVisible: { type: Function as PropType, default() { return () => true }, }, grid: { type: Object as PropType>, }, }, setup(props: IFormGridProps) { const layout = useFormLayout() const gridInstance = computed(() => { const newProps: IFormGridProps = {} Object.keys(props).forEach((key) => { if (typeof props[key] !== 'undefined') { newProps[key] = props[key] } }) const options = { columnGap: layout.value?.gridColumnGap ?? 8, rowGap: layout.value?.gridRowGap ?? 4, ...newProps, } return markRaw(options?.grid ? options.grid : new Grid(options)) }) const prefixCls = `${stylePrefix}-form-grid` const root = ref(null) provide(FormGridSymbol, gridInstance) onMounted(() => { watchEffect((onInvalidate) => { const dispose = gridInstance.value.connect(root.value) onInvalidate(() => { dispose() }) }) }) return { prefixCls, root, gridInstance, } }, render() { const { prefixCls, gridInstance } = this return h( 'div', { attrs: { class: `${prefixCls}`, }, style: { gridTemplateColumns: gridInstance.templateColumns, gap: gridInstance.gap, }, ref: 'root', }, { default: () => this.$slots.default, } ) }, }) ) as any const FormGridColumn = observer( defineComponent({ name: 'FFormGridColumn', props: { gridSpan: { type: Number, default: 1, }, }, setup(props: GridColumnProps, { slots }) { return () => { return h( 'div', { attrs: { 'data-grid-span': props.gridSpan, }, }, slots ) } }, }) ) export const FormGrid = composeExport(FormGridInner, { GridColumn: FormGridColumn, useGridSpan, useFormGrid, createFormGrid, }) export default FormGrid ================================================ FILE: packages/element/src/form-grid/style.scss ================================================ @import '../__builtins__/styles/common.scss'; .#{$formily-prefix}-form-grid { display: grid; } ================================================ FILE: packages/element/src/form-grid/style.ts ================================================ import './style.scss' ================================================ FILE: packages/element/src/form-item/animation.scss ================================================ @-webkit-keyframes antShowHelpIn { 0% { -webkit-transform: translateY(-5px); transform: translateY(-5px); opacity: 0; } to { -webkit-transform: translateY(0); transform: translateY(0); opacity: 1; } } .#{$form-item-prefix}-help-appear, .#{$form-item-prefix}-help-enter { -webkit-animation-duration: 0.3s; animation-duration: 0.3s; -webkit-animation-fill-mode: both; animation-fill-mode: both; -webkit-animation-play-state: paused; animation-play-state: paused; } .#{$form-item-prefix}-help-appear.#{$form-item-prefix}-help-appear-active, .#{$form-item-prefix}-help-enter.#{$form-item-prefix}-help-enter-active { -webkit-animation-name: antShowHelpIn; animation-name: antShowHelpIn; -webkit-animation-play-state: running; animation-play-state: running; } .#{$form-item-prefix}-help-appear, .#{$form-item-prefix}-help-enter { opacity: 0; } .#{$form-item-prefix}-help-appear, .#{$form-item-prefix}-help-enter { -webkit-animation-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1); animation-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1); } @keyframes antShowHelpIn { 0% { -webkit-transform: translateY(-5px); transform: translateY(-5px); opacity: 0; } to { -webkit-transform: translateY(0); transform: translateY(0); opacity: 1; } } @-webkit-keyframes antShowHelpOut { to { -webkit-transform: translateY(-5px); transform: translateY(-5px); opacity: 0; } } @keyframes antShowHelpOut { to { -webkit-transform: translateY(-5px); transform: translateY(-5px); opacity: 0; } } ================================================ FILE: packages/element/src/form-item/grid.scss ================================================ .#{$form-item-prefix}-item-col-24 { -webkit-box-flex: 0; -ms-flex: 0 0 100%; flex: 0 0 100%; max-width: 100%; } .#{$form-item-prefix}-item-col-23 { -webkit-box-flex: 0; -ms-flex: 0 0 95.83333333%; flex: 0 0 95.83333333%; max-width: 95.83333333%; } .#{$form-item-prefix}-item-col-22 { -webkit-box-flex: 0; -ms-flex: 0 0 91.66666667%; flex: 0 0 91.66666667%; max-width: 91.66666667%; } .#{$form-item-prefix}-item-col-21 { -webkit-box-flex: 0; -ms-flex: 0 0 87.5%; flex: 0 0 87.5%; max-width: 87.5%; } .#{$form-item-prefix}-item-col-20 { -webkit-box-flex: 0; -ms-flex: 0 0 83.33333333%; flex: 0 0 83.33333333%; max-width: 83.33333333%; } .#{$form-item-prefix}-item-col-19 { -webkit-box-flex: 0; -ms-flex: 0 0 79.16666667%; flex: 0 0 79.16666667%; max-width: 79.16666667%; } .#{$form-item-prefix}-item-col-18 { -webkit-box-flex: 0; -ms-flex: 0 0 75%; flex: 0 0 75%; max-width: 75%; } .#{$form-item-prefix}-item-col-17 { -webkit-box-flex: 0; -ms-flex: 0 0 70.83333333%; flex: 0 0 70.83333333%; max-width: 70.83333333%; } .#{$form-item-prefix}-item-col-16 { -webkit-box-flex: 0; -ms-flex: 0 0 66.66666667%; flex: 0 0 66.66666667%; max-width: 66.66666667%; } .#{$form-item-prefix}-item-col-15 { -webkit-box-flex: 0; -ms-flex: 0 0 62.5%; flex: 0 0 62.5%; max-width: 62.5%; } .#{$form-item-prefix}-item-col-14 { -webkit-box-flex: 0; -ms-flex: 0 0 58.33333333%; flex: 0 0 58.33333333%; max-width: 58.33333333%; } .#{$form-item-prefix}-item-col-13 { -webkit-box-flex: 0; -ms-flex: 0 0 54.16666667%; flex: 0 0 54.16666667%; max-width: 54.16666667%; } .#{$form-item-prefix}-item-col-12 { -webkit-box-flex: 0; -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } .#{$form-item-prefix}-item-col-11 { -webkit-box-flex: 0; -ms-flex: 0 0 45.83333333%; flex: 0 0 45.83333333%; max-width: 45.83333333%; } .#{$form-item-prefix}-item-col-10 { -webkit-box-flex: 0; -ms-flex: 0 0 41.66666667%; flex: 0 0 41.66666667%; max-width: 41.66666667%; } .#{$form-item-prefix}-item-col-9 { -webkit-box-flex: 0; -ms-flex: 0 0 37.5%; flex: 0 0 37.5%; max-width: 37.5%; } .#{$form-item-prefix}-item-col-8 { -webkit-box-flex: 0; -ms-flex: 0 0 33.33333333%; flex: 0 0 33.33333333%; max-width: 33.33333333%; } .#{$form-item-prefix}-item-col-7 { -webkit-box-flex: 0; -ms-flex: 0 0 29.16666667%; flex: 0 0 29.16666667%; max-width: 29.16666667%; } .#{$form-item-prefix}-item-col-6 { -webkit-box-flex: 0; -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } .#{$form-item-prefix}-item-col-5 { -webkit-box-flex: 0; -ms-flex: 0 0 20.83333333%; flex: 0 0 20.83333333%; max-width: 20.83333333%; } .#{$form-item-prefix}-item-col-4 { -webkit-box-flex: 0; -ms-flex: 0 0 16.66666667%; flex: 0 0 16.66666667%; max-width: 16.66666667%; } .#{$form-item-prefix}-item-col-3 { -webkit-box-flex: 0; -ms-flex: 0 0 12.5%; flex: 0 0 12.5%; max-width: 12.5%; } .#{$form-item-prefix}-item-col-2 { -webkit-box-flex: 0; -ms-flex: 0 0 8.33333333%; flex: 0 0 8.33333333%; max-width: 8.33333333%; } .#{$form-item-prefix}-item-col-1 { -webkit-box-flex: 0; -ms-flex: 0 0 4.16666667%; flex: 0 0 4.16666667%; max-width: 4.16666667%; } .#{$form-item-prefix}-item-col-0 { display: none; } ================================================ FILE: packages/element/src/form-item/index.ts ================================================ import { isVoidField } from '@formily/core' import { connect, h, mapProps } from '@formily/vue' import { Tooltip } from 'element-ui' import ResizeObserver from 'resize-observer-polyfill' import { Component } from 'vue' import { defineComponent, onBeforeUnmount, provide, ref, Ref, watch, } from 'vue-demi' import { FormLayoutShallowContext, useFormLayout } from '../form-layout' import { stylePrefix } from '../__builtins__/configs' import { composeExport, resolveComponent, useCompatRef, } from '../__builtins__/shared' export type FormItemProps = { className?: string required?: boolean label?: string | Component colon?: boolean tooltip?: string | Component layout?: 'vertical' | 'horizontal' | 'inline' labelStyle?: Record labelAlign?: 'left' | 'right' labelWrap?: boolean labelWidth?: number wrapperWidth?: number labelCol?: number wrapperCol?: number wrapperAlign?: 'left' | 'right' wrapperWrap?: boolean wrapperStyle?: Record fullness?: boolean addonBefore?: string | Component addonAfter?: string | Component size?: 'small' | 'default' | 'large' extra?: string feedbackText?: string | Component feedbackLayout?: 'loose' | 'terse' | 'popover' | 'none' | (string & {}) feedbackStatus?: 'error' | 'warning' | 'success' | 'pending' | (string & {}) tooltipLayout?: 'icon' | 'text' feedbackIcon?: string | Component asterisk?: boolean gridSpan?: number bordered?: boolean inset?: boolean } const useOverflow = (containerRef: Ref) => { const overflow = ref(false) let resizeObserver: ResizeObserver | undefined const cleanup = () => { if (resizeObserver) { resizeObserver.unobserve(containerRef.value) resizeObserver = null } } const observer = () => { const container = containerRef.value const content = container.querySelector('label') const containerWidth = container.getBoundingClientRect().width const contentWidth = content?.getBoundingClientRect().width if (containerWidth !== 0) { if (contentWidth > containerWidth) { overflow.value = true } else { overflow.value = false } } } const stopWatch = watch( () => containerRef.value, (el) => { cleanup() if (el) { resizeObserver = new ResizeObserver(observer) resizeObserver.observe(el) } }, { immediate: true, flush: 'post' } ) onBeforeUnmount(() => { cleanup() stopWatch() }) return overflow } const ICON_MAP = { error: () => h('i', { class: 'el-icon-circle-close' }, {}), success: () => h('i', { class: 'el-icon-circle-check' }, {}), warning: () => h('i', { class: 'el-icon-warning-outline' }, {}), } export const FormBaseItem = defineComponent({ name: 'FormItem', props: { className: {}, required: {}, label: {}, colon: {}, layout: {}, tooltip: {}, labelStyle: {}, labelAlign: {}, labelWrap: {}, labelWidth: {}, wrapperWidth: {}, labelCol: {}, wrapperCol: {}, wrapperAlign: {}, wrapperWrap: {}, wrapperStyle: {}, fullness: {}, addonBefore: {}, addonAfter: {}, size: {}, extra: {}, feedbackText: {}, feedbackLayout: {}, tooltipLayout: {}, feedbackStatus: {}, feedbackIcon: {}, asterisk: {}, gridSpan: {}, bordered: { default: true }, inset: { default: false }, }, setup(props, { slots, refs }) { const active = ref(false) const deepLayoutRef = useFormLayout() const prefixCls = `${stylePrefix}-form-item` const { elRef: containerRef, elRefBinder } = useCompatRef(refs) const overflow = useOverflow(containerRef) provide(FormLayoutShallowContext, ref(null)) return () => { const gridStyles: Record = {} const deepLayout = deepLayoutRef.value const { label, colon = deepLayout.colon ?? true, layout = deepLayout.layout ?? 'horizontal', tooltip, labelStyle = {}, labelWrap = deepLayout.labelWrap ?? false, labelWidth = deepLayout.labelWidth, wrapperWidth = deepLayout.wrapperWidth, labelCol = deepLayout.labelCol, wrapperCol = deepLayout.wrapperCol, wrapperAlign = deepLayout.wrapperAlign ?? 'left', wrapperWrap = deepLayout.wrapperWrap, wrapperStyle = {}, fullness = deepLayout.fullness, addonBefore, addonAfter, size = deepLayout.size, extra, feedbackText, feedbackLayout = deepLayout.feedbackLayout ?? 'loose', tooltipLayout = deepLayout.tooltipLayout ?? 'icon', feedbackStatus, feedbackIcon, asterisk, bordered = deepLayout.bordered, inset = deepLayout.inset, } = props const labelAlign = deepLayout.layout === 'vertical' ? props.labelAlign ?? deepLayout.labelAlign ?? 'left' : props.labelAlign ?? deepLayout.labelAlign ?? 'right' // 固定宽度 let enableCol = false if (labelWidth || wrapperWidth) { if (labelWidth) { labelStyle.width = `${labelWidth}px` labelStyle.maxWidth = `${labelWidth}px` } if (wrapperWidth) { wrapperStyle.width = `${wrapperWidth}px` wrapperStyle.maxWidth = `${wrapperWidth}px` } // 栅格模式 } else if (labelCol || wrapperCol) { enableCol = true } const formatChildren = feedbackLayout === 'popover' ? h( 'el-popover', { props: { disabled: !feedbackText, placement: 'top', }, }, { reference: () => h('div', {}, { default: () => slots.default?.() }), default: () => [ h( 'div', { class: { [`${prefixCls}-${feedbackStatus}-help`]: !!feedbackStatus, [`${prefixCls}-help`]: true, }, }, { default: () => [ feedbackStatus && ['error', 'success', 'warning'].includes(feedbackStatus) ? ICON_MAP[ feedbackStatus as 'error' | 'success' | 'warning' ]() : '', resolveComponent(feedbackText), ], } ), ], } ) : slots.default?.() const renderLabelText = () => { const labelChildren = h( 'div', { class: `${prefixCls}-label-content`, ref: elRefBinder, }, { default: () => [ asterisk && h( 'span', { class: `${prefixCls}-asterisk` }, { default: () => ['*'] } ), h('label', {}, { default: () => [resolveComponent(label)] }), ], } ) const isTextTooltip = tooltip && tooltipLayout === 'text' if (isTextTooltip || overflow.value) { return h( Tooltip, { props: { placement: 'top', }, }, { default: () => [labelChildren], content: () => h( 'div', {}, { default: () => [ overflow.value && resolveComponent(label), isTextTooltip && resolveComponent(tooltip), ], } ), } ) } else { return labelChildren } } const renderTooltipIcon = () => { if (tooltip && tooltipLayout === 'icon') { return h( 'span', { class: `${prefixCls}-label-tooltip`, }, { default: () => [ h( Tooltip, { props: { placement: 'top', }, }, { default: () => [h('i', { class: 'el-icon-info' }, {})], content: () => h( 'div', { class: `${prefixCls}-label-tooltip-content`, }, { default: () => [resolveComponent(tooltip)], } ), } ), ], } ) } } const renderLabel = label && h( 'div', { class: { [`${prefixCls}-label`]: true, [`${prefixCls}-label-tooltip`]: (tooltip && tooltipLayout === 'text') || overflow.value, [`${prefixCls}-item-col-${labelCol}`]: enableCol && !!labelCol, }, style: labelStyle, }, { default: () => [ // label content renderLabelText(), // label tooltip renderTooltipIcon(), // label colon label && h( 'span', { class: `${prefixCls}-colon`, }, { default: () => [colon ? ':' : ''] } ), ], } ) const renderFeedback = !!feedbackText && feedbackLayout !== 'popover' && feedbackLayout !== 'none' && h( 'div', { class: { [`${prefixCls}-${feedbackStatus}-help`]: !!feedbackStatus, [`${prefixCls}-help`]: true, [`${prefixCls}-help-enter`]: true, [`${prefixCls}-help-enter-active`]: true, }, }, { default: () => [resolveComponent(feedbackText)] } ) const renderExtra = extra && h( 'div', { class: `${prefixCls}-extra` }, { default: () => [resolveComponent(extra)] } ) const renderContent = h( 'div', { class: { [`${prefixCls}-control`]: true, [`${prefixCls}-item-col-${wrapperCol}`]: enableCol && !!wrapperCol, }, }, { default: () => [ h( 'div', { class: `${prefixCls}-control-content` }, { default: () => [ addonBefore && h( 'div', { class: `${prefixCls}-addon-before` }, { default: () => [resolveComponent(addonBefore)], } ), h( 'div', { class: { [`${prefixCls}-control-content-component`]: true, [`${prefixCls}-control-content-component-has-feedback-icon`]: !!feedbackIcon, }, style: wrapperStyle, }, { default: () => [ formatChildren, feedbackIcon && h( 'div', { class: `${prefixCls}-feedback-icon` }, { default: () => [ typeof feedbackIcon === 'string' ? h('i', { class: feedbackIcon }, {}) : resolveComponent(feedbackIcon), ], } ), ], } ), addonAfter && h( 'div', { class: `${prefixCls}-addon-after` }, { default: () => [resolveComponent(addonAfter)], } ), ], } ), renderFeedback, renderExtra, ], } ) return h( 'div', { style: { ...gridStyles, }, attrs: { 'data-grid-span': props.gridSpan, }, class: { [`${prefixCls}`]: true, [`${prefixCls}-layout-${layout}`]: true, [`${prefixCls}-${feedbackStatus}`]: !!feedbackStatus, [`${prefixCls}-feedback-has-text`]: !!feedbackText, [`${prefixCls}-size-${size}`]: !!size, [`${prefixCls}-feedback-layout-${feedbackLayout}`]: !!feedbackLayout, [`${prefixCls}-fullness`]: !!fullness || !!inset || !!feedbackIcon, [`${prefixCls}-inset`]: !!inset, [`${prefixCls}-active`]: active.value, [`${prefixCls}-inset-active`]: !!inset && active.value, [`${prefixCls}-label-align-${labelAlign}`]: true, [`${prefixCls}-control-align-${wrapperAlign}`]: true, [`${prefixCls}-label-wrap`]: !!labelWrap, [`${prefixCls}-control-wrap`]: !!wrapperWrap, [`${prefixCls}-bordered-none`]: bordered === false || !!inset || !!feedbackIcon, [`${props.className}`]: !!props.className, }, on: { focus: () => { if (feedbackIcon || inset) { active.value = true } }, blur: () => { if (feedbackIcon || inset) { active.value = false } }, }, }, { default: () => [renderLabel, renderContent], } ) } }, }) const Item = connect( FormBaseItem, mapProps( { validateStatus: true, title: 'label', required: true }, (props, field) => { if (isVoidField(field)) return props if (!field) return props const takeMessage = () => { if (field.validating) return if (props.feedbackText) return props.feedbackText if (field.selfErrors.length) return field.selfErrors if (field.selfWarnings.length) return field.selfWarnings if (field.selfSuccesses.length) return field.selfSuccesses } const errorMessages = takeMessage() return { feedbackText: Array.isArray(errorMessages) ? errorMessages.join(', ') : errorMessages, extra: props.extra || field.description, } }, (props, field) => { if (isVoidField(field)) return props if (!field) return props return { feedbackStatus: field.validateStatus === 'validating' ? 'pending' : (Array.isArray(field.decorator) && field.decorator[1]?.feedbackStatus) || field.validateStatus, } }, (props, field) => { if (isVoidField(field)) return props if (!field) return props let asterisk = false if (field.required && field.pattern !== 'readPretty') { asterisk = true } if ('asterisk' in props) { asterisk = props.asterisk } return { asterisk, } } ) ) export const FormItem = composeExport(Item, { BaseItem: FormBaseItem, }) export default FormItem ================================================ FILE: packages/element/src/form-item/style.scss ================================================ @use 'sass:math'; @import '../__builtins__/styles/common.scss'; @import './var.scss'; @import './grid.scss'; @import './animation.scss'; .#{$form-item-prefix} { display: flex; margin-bottom: $--form-item-margin-bottom; position: relative; line-height: $--form-item-medium-line-height; font-size: $--form-font-size; &-label * { line-height: $--form-item-medium-line-height; } &-label-content { min-height: $--form-item-medium-line-height; } &-content-component { line-height: $--form-item-medium-line-height; } .#{$namespace}-input, .#{$namespace}-input-number, .#{$namespace}-input-number.is-controls-right, .#{$namespace}-select, .#{$namespace}-cascader, .#{$namespace}-date-editor--daterange, .#{$namespace}-date-editor--timerange, .#{$namespace}-date-editor--datetimerange, .#{$namespace}-date-editor.#{$namespace}-input, .#{$namespace}-date-editor.#{$namespace}-input__inner, .#{$namespace}-tree-select { width: 100%; } .#{$namespace}-input-group { vertical-align: top; } } .#{$form-item-prefix}-label { position: relative; display: flex; &-content { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } &-tooltip { cursor: help; * { cursor: help; } label { border-bottom: 1px dashed currentColor; } } } .#{$form-item-prefix}-label label { color: $--color-text-regular; } .#{$form-item-prefix}-label-align-left { > .#{$form-item-prefix}-label { justify-content: flex-start; } } .#{$form-item-prefix}-label-align-right { > .#{$form-item-prefix}-label { justify-content: flex-end; } } .#{$form-item-prefix}-label-wrap { .#{$form-item-prefix}-label { label { white-space: pre-line; } } } .#{$form-item-prefix}-feedback-layout-terse { margin-bottom: 8px; &.#{$form-item-prefix}-feedback-has-text:not(.#{$form-item-prefix}-inset) { margin-bottom: 0; } } .#{$form-item-prefix}-feedback-layout-loose { margin-bottom: $--form-error-line-height; &.#{$form-item-prefix}-feedback-has-text:not(.#{$form-item-prefix}-inset) { margin-bottom: 0; } } .#{$form-item-prefix}-feedback-layout-none { margin-bottom: 0; &.#{$form-item-prefix}-feedback-has-text:not(.#{$form-item-prefix}-inset) { margin-bottom: 0; } } .#{$form-item-prefix}-control { width: 100%; flex: 1; .#{$form-item-prefix}-control-content { display: flex; .#{$form-item-prefix}-control-content-component { width: 100%; min-height: $--form-item-medium-line-height; line-height: $--form-item-medium-line-height; &-has-feedback-icon { flex: 1; position: relative; display: flex; align-items: center; } } .#{$form-item-prefix}-addon-before { margin-right: 8px; display: inline-flex; align-items: center; min-height: $--form-item-medium-line-height; flex-shrink: 0; } .#{$form-item-prefix}-addon-after { margin-left: 8px; display: inline-flex; align-items: center; min-height: $--form-item-medium-line-height; flex-shrink: 0; } } } .#{$form-item-prefix}-size-small { font-size: $--font-size-extra-small; .#{$form-item-prefix}-label * { line-height: $--form-item-small-line-height; } .#{$form-item-prefix}-label-content { min-height: $--form-item-small-line-height; } .#{$form-item-prefix}-control-content { .#{$form-item-prefix}-control-content-component { line-height: $--form-item-small-line-height; min-height: $--form-item-small-line-height; } } .#{$form-item-prefix}-help, .#{$form-item-prefix}-extra { min-height: $--form-error-line-height; } .#{$form-item-prefix}-control-content { min-height: $--form-item-small-line-height; } .#{$form-item-prefix}-label > label { height: $--form-item-small-line-height; } .#{$namespace}-input { input { height: $--form-item-small-line-height; line-height: $--form-item-small-line-height; } } .#{$namespace}-input-number { line-height: $--form-item-small-line-height; &.is-controls-right { .#{$namespace}-input-number__increase, .#{$namespace}-input-number__decrease { line-height: math.div($--form-item-small-line-height, 2); height: math.div($--form-item-small-line-height, 2); font-size: $--font-size-extra-small; box-sizing: border-box; } } } } .#{$form-item-prefix}-size-large { font-size: $--font-size-medium; .#{$form-item-prefix}-label * { line-height: $--form-item-large-line-height; } .#{$form-item-prefix}-label-content { min-height: $--form-item-large-line-height; } .#{$form-item-prefix}-control-content { .#{$form-item-prefix}-control-content-component { line-height: $--form-item-large-line-height; min-height: $--form-item-large-line-height; } } .#{$form-item-prefix}-help, .#{$form-item-prefix}-extra { min-height: $--form-error-line-height; } .#{$form-item-prefix}-control-content { min-height: $--form-item-large-line-height; } .#{$namespace}-input { input { height: $--form-item-large-line-height; line-height: $--form-item-large-line-height; } } .#{$namespace}-select { input { height: $--form-item-large-line-height !important; line-height: $--form-item-large-line-height; } } .#{$namespace}-select__tags .el-tag { height: $--form-item-large-line-height - 12px; line-height: $--form-item-large-line-height - 12px; } .#{$namespace}-input-number { line-height: $--form-item-large-line-height; &.is-controls-right { .#{$namespace}-input-number__increase, .#{$namespace}-input-number__decrease { line-height: math.div($--form-item-large-line-height, 2) - 1; font-size: $--font-size-medium; } } } } .#{$form-item-prefix} { &-layout-vertical { display: block; .#{$form-item-prefix}-label * { line-height: $--form-item-label-top-line-height; } .#{$form-item-prefix}-label-content { min-height: $--form-item-label-top-line-height; } } } .#{$form-item-prefix}-feedback-layout-popover { margin-bottom: 8px; } .#{$form-item-prefix}-label-tooltip { margin-left: 4px; color: $--color-text-secondary; display: flex; align-items: center; height: $--form-item-medium-line-height; cursor: pointer; i { line-height: 1; } } .#{$form-item-prefix}-control-align-left { .#{$form-item-prefix}-control-content { justify-content: flex-start; } } .#{$form-item-prefix}-control-align-right { .#{$form-item-prefix}-control-content { justify-content: flex-end; } } .#{$form-item-prefix}-control-wrap { .#{$form-item-prefix}-control { white-space: pre-line; } } .#{$form-item-prefix}-asterisk { color: $--color-danger; margin-right: 4px; display: inline-block; font-family: SimSun, sans-serif; } .#{$form-item-prefix}-colon { margin-left: 2px; margin-right: 8px; } .#{$form-item-prefix}-help, .#{$form-item-prefix}-extra { clear: both; min-height: $--form-error-line-height; line-height: $--form-error-line-height; color: $--color-text-secondary; transition: $--color-transition-base; padding-top: 0; } .#{$form-item-prefix}-fullness { > .#{$form-item-prefix}-control { > .#{$form-item-prefix}-control-content { > .#{$form-item-prefix}-control-content-component { > *:first-child { width: 100%; } } } } } .#{$form-item-prefix}-control-content-component-has-feedback-icon { border-radius: $--border-radius-base; border: $--border-base; padding-right: 8px; transition: $--all-transition; touch-action: manipulation; outline: none; .#{$namespace}-input-number, .#{$namespace}-date-editor .#{$namespace}-input__inner, .#{$namespace}-select .#{$namespace}-input__inner, .#{$namespace}-input .#{$namespace}-input__inner { border: none !important; box-shadow: none !important; } .#{$namespace}-input-number.is-controls-right .#{$namespace}-input__inner { padding-right: 40px; } .#{$namespace}-input-number.is-controls-right .#{$namespace}-input-number__increase { top: 0; right: 8px; border-right: $--border-base; } .#{$namespace}-input-number.is-controls-right .#{$namespace}-input-number__decrease { bottom: 0; right: 8px; border-right: $--border-base; } } .#{$form-item-prefix} { &:hover { .#{$form-item-prefix}-control-content-component-has-feedback-icon { @include hover; } } } .#{$form-item-prefix}-active { .#{$form-item-prefix}-control-content-component-has-feedback-icon { @include active; } } .#{$form-item-prefix}-error { & .#{$namespace}-input__inner, & .#{$namespace}-textarea__inner { &, &.hover { border-color: $--color-danger; } } & .#{$namespace}-input__inner, & .#{$namespace}-textarea__inner { &:focus { border-color: $--color-danger; } } & .#{$namespace}-input-group__append, & .#{$namespace}-input-group__prepend { & .#{$namespace}-input__inner { border-color: transparent; } } .#{$namespace}-input__validateIcon { color: $--color-danger !important; } } .#{$form-item-prefix}-error-help, .#{$form-item-prefix}-warning-help, .#{$form-item-prefix}-success-help { i { margin-right: 8px; } } .#{$form-item-prefix}-error-help { color: $--color-danger; } .#{$form-item-prefix}-warning-help { color: $--color-warning; } .#{$form-item-prefix}-success-help { color: $--color-success; } .#{$form-item-prefix}-warning { & .#{$namespace}-input__inner, & .#{$namespace}-textarea__inner { &, &.hover { border-color: $--color-warning; } } & .#{$namespace}-input__inner, & .#{$namespace}-textarea__inner { &:focus { border-color: $--color-warning; } } & .#{$namespace}-input-group__append, & .#{$namespace}-input-group__prepend { & .#{$namespace}-input__inner { border-color: transparent; } } .#{$namespace}-input__validateIcon { color: $--color-warning !important; } } .#{$form-item-prefix}-success { & .#{$namespace}-input__inner, & .#{$namespace}-textarea__inner { &, &.hover { border-color: $--color-success; } } & .#{$namespace}-input__inner, & .#{$namespace}-textarea__inner { &:focus { border-color: $--color-success; } } & .#{$namespace}-input-group__append, & .#{$namespace}-input-group__prepend { & .#{$namespace}-input__inner { border-color: transparent; } } .#{$namespace}-input__validateIcon { color: $--color-success !important; } } .#{$form-item-prefix}-bordered-none { .#{$namespace}-input__inner { border: none !important; } .#{$namespace}-input-number__decrease, .#{$namespace}-input-number__increase { border: none !important; background: transparent !important; } } .#{$form-item-prefix}-inset { border-radius: $--border-radius-base; border: $--border-base; padding-left: 12px; transition: 0.3s all; &:hover { @include hover; } } .#{$form-item-prefix}-inset-active { @include active; } ================================================ FILE: packages/element/src/form-item/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/tooltip.scss' import './style.scss' ================================================ FILE: packages/element/src/form-item/var.scss ================================================ $form-item-prefix: '#{$formily-prefix}-form-item'; $--form-font-size: $--font-size-base !default; $--form-label-font-size: $--form-font-size !default; $--form-item-large-line-height: 40px; $--form-item-medium-line-height: 32px; $--form-item-small-line-height: 24px; $--form-item-label-top-line-height: 40px; $--form-error-line-height: 22px !default; $--form-item-margin-bottom: $--form-error-line-height !default; ================================================ FILE: packages/element/src/form-layout/index.ts ================================================ import { h } from '@formily/vue' import { defineComponent, inject, InjectionKey, provide, Ref, ref, watch, } from 'vue-demi' import { stylePrefix } from '../__builtins__/configs' import { useCompatRef } from '../__builtins__/shared' import { useResponsiveFormLayout } from './useResponsiveFormLayout' export type FormLayoutProps = { className?: string colon?: boolean labelAlign?: 'right' | 'left' | ('right' | 'left')[] wrapperAlign?: 'right' | 'left' | ('right' | 'left')[] labelWrap?: boolean labelWidth?: number wrapperWidth?: number wrapperWrap?: boolean labelCol?: number | number[] wrapperCol?: number | number[] fullness?: boolean size?: 'small' | 'default' | 'large' layout?: | 'vertical' | 'horizontal' | 'inline' | ('vertical' | 'horizontal' | 'inline')[] direction?: 'rtl' | 'ltr' shallow?: boolean feedbackLayout?: 'loose' | 'terse' | 'popover' tooltipLayout?: 'icon' | 'text' bordered?: boolean breakpoints?: number[] inset?: boolean spaceGap?: number gridColumnGap?: number gridRowGap?: number } export const FormLayoutDeepContext: InjectionKey> = Symbol( 'FormLayoutDeepContext' ) export const FormLayoutShallowContext: InjectionKey> = Symbol('FormLayoutShallowContext') export const useFormDeepLayout = (): Ref => inject(FormLayoutDeepContext, ref({})) export const useFormShallowLayout = (): Ref => inject(FormLayoutShallowContext, ref({})) export const useFormLayout = (): Ref => { const shallowLayout = useFormShallowLayout() const deepLayout = useFormDeepLayout() const formLayout = ref({ ...deepLayout.value, ...shallowLayout.value, }) watch( [shallowLayout, deepLayout], () => { formLayout.value = { ...deepLayout.value, ...shallowLayout.value, } }, { deep: true, } ) return formLayout } export const FormLayout = defineComponent({ name: 'FFormLayout', props: { className: {}, colon: { default: true }, labelAlign: {}, wrapperAlign: {}, labelWrap: { default: false }, labelWidth: {}, wrapperWidth: {}, wrapperWrap: { default: false }, labelCol: {}, wrapperCol: {}, fullness: { default: false }, size: { default: 'default' }, layout: { default: 'horizontal' }, direction: { default: 'ltr' }, shallow: { default: true }, feedbackLayout: {}, tooltipLayout: {}, bordered: { default: true }, inset: { default: false }, breakpoints: {}, spaceGap: {}, gridColumnGap: {}, gridRowGap: {}, }, setup(customProps, { slots, refs }) { const { elRef: root, elRefBinder } = useCompatRef(refs) const { props } = useResponsiveFormLayout(customProps, root) const deepLayout = useFormDeepLayout() const newDeepLayout = ref({ ...deepLayout, }) const shallowProps = ref({}) watch( [props, deepLayout], () => { shallowProps.value = props.value.shallow ? props.value : undefined if (!props.value.shallow) { Object.assign(newDeepLayout.value, props.value) } else { if (props.value.size) { newDeepLayout.value.size = props.value.size } if (props.value.colon) { newDeepLayout.value.colon = props.value.colon } } }, { deep: true, immediate: true } ) provide(FormLayoutDeepContext, newDeepLayout) provide(FormLayoutShallowContext, shallowProps) const formPrefixCls = `${stylePrefix}-form` return () => { const classNames = { [`${formPrefixCls}-${props.value.layout}`]: true, [`${formPrefixCls}-rtl`]: props.value.direction === 'rtl', [`${formPrefixCls}-${props.value.size}`]: props.value.size !== undefined, [`${props.value.className}`]: props.value.className !== undefined, } return h( 'div', { ref: elRefBinder, class: classNames, }, slots ) } }, }) export default FormLayout ================================================ FILE: packages/element/src/form-layout/style.scss ================================================ @import '../__builtins__/styles/common.scss'; .#{$formily-prefix}-form-inline { display: flex; flex-wrap: wrap; } ================================================ FILE: packages/element/src/form-layout/style.ts ================================================ import './style.scss' ================================================ FILE: packages/element/src/form-layout/useResponsiveFormLayout.ts ================================================ import { isArr, isValid } from '@formily/shared' import { onMounted, Ref, ref } from 'vue-demi' interface IProps { breakpoints?: number[] layout?: | 'vertical' | 'horizontal' | 'inline' | ('vertical' | 'horizontal' | 'inline')[] labelCol?: number | number[] wrapperCol?: number | number[] labelAlign?: 'right' | 'left' | ('right' | 'left')[] wrapperAlign?: 'right' | 'left' | ('right' | 'left')[] [props: string]: any } interface ICalcBreakpointIndex { (originalBreakpoints: number[], width: number): number } interface ICalculateProps { (target: Element, props: IProps): IProps } interface IUseResponsiveFormLayout { (props: IProps, root: Ref): { props: Ref } } const calcBreakpointIndex: ICalcBreakpointIndex = (breakpoints, width) => { for (let i = 0; i < breakpoints.length; i++) { if (width <= breakpoints[i]) { return i } } } const calcFactor = (value: T | T[], breakpointIndex: number): T => { if (Array.isArray(value)) { if (breakpointIndex === -1) return value[0] return value[breakpointIndex] ?? value[value.length - 1] } else { return value } } const factor = (value: T | T[], breakpointIndex: number): T => isValid(value) ? calcFactor(value as any, breakpointIndex) : value const calculateProps: ICalculateProps = (target, props) => { const { clientWidth } = target const { breakpoints, layout, labelAlign, wrapperAlign, labelCol, wrapperCol, ...otherProps } = props const breakpointIndex = calcBreakpointIndex(breakpoints, clientWidth) return { layout: factor(layout, breakpointIndex), labelAlign: factor(labelAlign, breakpointIndex), wrapperAlign: factor(wrapperAlign, breakpointIndex), labelCol: factor(labelCol, breakpointIndex), wrapperCol: factor(wrapperCol, breakpointIndex), ...otherProps, } } export const useResponsiveFormLayout: IUseResponsiveFormLayout = ( props, root ) => { const { breakpoints } = props if (!isArr(breakpoints)) { return { props: ref(props) } } const layoutProps = ref(props) const updateUI = () => { if (root.value) { layoutProps.value = calculateProps(root.value, props) } } onMounted(() => { const observer = () => { updateUI() } const resizeObserver = new ResizeObserver(observer) if (root.value) { resizeObserver.observe(root.value) } updateUI() return () => { resizeObserver.disconnect() } }) return { props: layoutProps, } } ================================================ FILE: packages/element/src/form-step/index.ts ================================================ import { Form, VoidField } from '@formily/core' import { Schema, SchemaKey } from '@formily/json-schema' import { action, model, observable } from '@formily/reactive' import { observer } from '@formily/reactive-vue' import { Fragment, h, RecursionField, useField, useFieldSchema, } from '@formily/vue' import { Step, Steps } from 'element-ui' import { defineComponent, PropType } from 'vue-demi' import { stylePrefix } from '../__builtins__/configs' import type { Step as StepProps, Steps as StepsProps } from 'element-ui' import { composeExport } from '../__builtins__/shared' export interface IFormStep { connect: (steps: SchemaStep[], field: VoidField) => void current: number allowNext: boolean allowBack: boolean setCurrent(key: number): void submit: Form['submit'] next(): void back(): void } export interface IFormStepProps extends StepsProps { formStep?: IFormStep } type SchemaStep = { name: SchemaKey props: any schema: Schema } type FormStepEnv = { form: Form field: VoidField steps: SchemaStep[] } const parseSteps = (schema: Schema) => { const steps: SchemaStep[] = [] schema.mapProperties((schema, name) => { if (schema['x-component']?.indexOf('StepPane') > -1) { steps.push({ name, props: schema['x-component-props'], schema, }) } }) return steps } const createFormStep = (defaultCurrent = 0): IFormStep => { const env: FormStepEnv = observable({ form: null, field: null, steps: [], }) const setDisplay = action.bound((target: number) => { const currentStep = env.steps[target] env.steps.forEach(({ name }) => { env.form.query(`${env.field.address}.${name}`).take((field) => { if (name === currentStep.name) { field.setDisplay('visible') } else { field.setDisplay('hidden') } }) }) }) const next = action.bound(() => { if (formStep.allowNext) { setDisplay(formStep.current + 1) formStep.setCurrent(formStep.current + 1) } }) const back = action.bound(() => { if (formStep.allowBack) { setDisplay(formStep.current - 1) formStep.setCurrent(formStep.current - 1) } }) const formStep: IFormStep = model({ connect(steps, field) { env.steps = steps env.form = field?.form env.field = field }, current: defaultCurrent, setCurrent(key: number) { formStep.current = key }, get allowNext() { return formStep.current < env.steps.length - 1 }, get allowBack() { return formStep.current > 0 }, async next() { try { await env.form.validate() next() } catch {} }, async back() { back() }, async submit(onSubmit) { return env.form?.submit?.(onSubmit) }, }) return formStep } const FormStepInner = observer( defineComponent({ name: 'FFormStep', props: { formStep: { type: Object as PropType, default() { return { current: 0, } }, }, }, setup(props, { attrs }) { const field = useField().value const prefixCls = `${stylePrefix}-form-step` const fieldSchemaRef = useFieldSchema() const steps = parseSteps(fieldSchemaRef.value) props.formStep.connect?.(steps, field) return () => { const current = props.active || props.formStep?.current || 0 const renderSteps = (steps: SchemaStep[], callback) => { return steps.map(callback) } return h( 'div', { class: [prefixCls], }, { default: () => [ h( Steps, { props: { active: current, }, style: [{ marginBottom: '10px' }, attrs.style], attrs, }, { default: () => renderSteps(steps, ({ props }, key) => { return h(Step, { props, key }, {}) }), } ), renderSteps(steps, ({ name, schema }, key) => { if (key !== current) return return h(RecursionField, { props: { name, schema }, key }, {}) }), ], } ) } }, }) ) const StepPane = defineComponent({ name: 'FFormStepPane', setup(_props, { slots }) { return () => h(Fragment, {}, slots) }, }) export const FormStep = composeExport(FormStepInner, { StepPane, createFormStep, }) export default FormStep ================================================ FILE: packages/element/src/form-step/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/steps.scss' import 'element-ui/packages/theme-chalk/src/step.scss' ================================================ FILE: packages/element/src/form-tab/index.ts ================================================ import { Schema, SchemaKey } from '@formily/json-schema' import { model } from '@formily/reactive' import { observer } from '@formily/reactive-vue' import { Fragment, h, RecursionField, useField, useFieldSchema, } from '@formily/vue' import { Badge, TabPane, Tabs } from 'element-ui' import { computed, defineComponent, reactive } from 'vue-demi' import { stylePrefix } from '../__builtins__/configs' import type { TabPane as TabPaneProps, Tabs as TabsProps } from 'element-ui' import { composeExport } from '../__builtins__/shared' export interface IFormTab { activeKey: string setActiveKey(key: string): void } export interface IFormTabProps extends TabsProps { formTab?: IFormTab } export interface IFormTabPaneProps extends TabPaneProps { key: string | number } const useTabs = () => { const tabsField = useField().value const schema = useFieldSchema().value const tabs: { name: SchemaKey; props: any; schema: Schema }[] = reactive([]) schema.mapProperties((schema, name) => { const field = tabsField.query(tabsField.address.concat(name)).take() if (field?.display === 'none' || field?.display === 'hidden') return if (schema['x-component']?.indexOf('TabPane') > -1) { tabs.push({ name, props: { name: schema?.['x-component-props']?.name || name, ...schema?.['x-component-props'], }, schema, }) } }) return tabs } const createFormTab = (defaultActiveKey?: string) => { const formTab = model({ activeKey: defaultActiveKey, setActiveKey(key: string) { formTab.activeKey = key }, }) return formTab } const FormTabInner = observer( defineComponent({ name: 'FFormTab', props: ['formTab'], setup(props, { attrs, listeners }) { const field = useField().value const formTabRef = computed(() => props.formTab ?? createFormTab()) const prefixCls = `${stylePrefix}-form-tab` return () => { const formTab = formTabRef.value const tabs = useTabs() const activeKey = props.value || formTab?.activeKey || tabs?.[0]?.name const badgedTab = (key: SchemaKey, props: any) => { const errors = field.form.queryFeedbacks({ type: 'error', address: `${field.address.concat(key)}.*`, }) if (errors.length) { return () => h( Badge, { class: [`${prefixCls}-errors-badge`], props: { value: errors.length, }, }, { default: () => props.label } ) } return () => props.label } const getTabs = (tabs) => { return tabs.map(({ props, schema, name }, key) => { return h( TabPane, { key, props, }, { default: () => [ h( RecursionField, { props: { schema, name, }, }, {} ), ], label: () => [ h('div', {}, { default: badgedTab(name, props) }), ], } ) }) } return h( Tabs, { class: [prefixCls], style: attrs.style, props: { ...attrs, value: activeKey, }, on: { ...listeners, input: (key) => { listeners.input?.(key) formTab.setActiveKey?.(key) }, }, }, { default: () => getTabs(tabs), } ) } }, }) ) const FormTabPane = defineComponent({ name: 'FFormTabPane', setup(_props, { slots }) { return () => h(Fragment, {}, slots) }, }) export const FormTab = composeExport(FormTabInner, { TabPane: FormTabPane, createFormTab, }) export default FormTab ================================================ FILE: packages/element/src/form-tab/style.scss ================================================ @import '../__builtins__/styles/common.scss'; .#{$formily-prefix}-form-tab-errors-badge { line-height: 1; vertical-align: initial; } ================================================ FILE: packages/element/src/form-tab/style.ts ================================================ import './style.scss' import 'element-ui/packages/theme-chalk/src/tabs.scss' import 'element-ui/packages/theme-chalk/src/tab-pane.scss' import 'element-ui/packages/theme-chalk/src/badge.scss' ================================================ FILE: packages/element/src/index.ts ================================================ import './style' export * from './array-base' export * from './array-table' export * from './cascader' export * from './checkbox' export * from './date-picker' export * from './el-form' export * from './el-form-item' export * from './form' export * from './form-button-group' export * from './form-grid' export * from './form-item' export * from './form-layout' export * from './input' export * from './input-number' export * from './password' export * from './radio' export * from './reset' export * from './select' export * from './space' export * from './submit' export * from './switch' export * from './time-picker' export * from './transfer' export * from './upload' export * from './preview-text' export * from './form-collapse' export * from './form-tab' export * from './form-step' export * from './array-cards' export * from './array-collapse' export * from './array-items' export * from './array-tabs' export * from './editable' export * from './form-dialog' export * from './form-drawer' ================================================ FILE: packages/element/src/input/index.ts ================================================ import { composeExport, transformComponent } from '../__builtins__/shared' import { connect, mapProps, mapReadPretty } from '@formily/vue' import { PreviewText } from '../preview-text' import type { Input as ElInputProps } from 'element-ui' import { Input as ElInput } from 'element-ui' export type InputProps = ElInputProps const TransformElInput = transformComponent(ElInput, { change: 'input', }) const InnerInput = connect( TransformElInput, mapProps({ readOnly: 'readonly' }), mapReadPretty(PreviewText.Input) ) const TextArea = connect( InnerInput, mapProps((props) => { return { ...props, type: 'textarea', } }), mapReadPretty(PreviewText.Input) ) export const Input = composeExport(InnerInput, { TextArea, }) export default Input ================================================ FILE: packages/element/src/input/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/input.scss' // 依赖 import '../preview-text/style' ================================================ FILE: packages/element/src/input-number/index.ts ================================================ import { transformComponent } from '../__builtins__/shared' import { connect, mapProps, mapReadPretty } from '@formily/vue' import type { InputNumber as _ElInputNumberProps } from 'element-ui' import { InputNumber as ElInputNumber } from 'element-ui' import { PreviewText } from '../preview-text' export type InputNumberProps = _ElInputNumberProps const TransformElInputNumber = transformComponent( ElInputNumber, { change: 'input', } ) export const InputNumber = connect( TransformElInputNumber, mapProps({ readOnly: 'readonly' }, (props) => { let controlsPosition = 'right' if (props.controlsPosition) { controlsPosition = props.controlsPosition } return { controlsPosition, } }), mapReadPretty(PreviewText.Input) ) export default InputNumber ================================================ FILE: packages/element/src/input-number/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/input-number.scss' // 依赖 import '../preview-text/style' ================================================ FILE: packages/element/src/password/index.ts ================================================ import { Input } from '../input' import { connect, mapProps, mapReadPretty } from '@formily/vue' import { PreviewText } from '../preview-text' import type { Input as ElInputProps } from 'element-ui' export type PasswordProps = ElInputProps export const Password = connect( Input, mapProps((props) => { return { ...props, showPassword: true, } }), mapReadPretty(PreviewText.Input) ) export default Password ================================================ FILE: packages/element/src/password/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/input.scss' // 依赖 import '../preview-text/style' ================================================ FILE: packages/element/src/preview-text/index.ts ================================================ import { Field } from '@formily/core' import { observer } from '@formily/reactive-vue' import { isArr, isValid } from '@formily/shared' import { h, useField } from '@formily/vue' import { Tag } from 'element-ui' import { formatDate } from 'element-ui/src/utils/date-util' import { computed, defineComponent, Ref, toRef } from 'vue-demi' import type { CascaderProps } from '../cascader' import type { DatePickerProps } from '../date-picker' import { InputProps } from '../input' import type { SelectProps } from '../select' import { Space } from '../space' import type { TimePickerProps } from '../time-picker' import { stylePrefix } from '../__builtins__/configs' import { composeExport, createContext, resolveComponent, useContext, } from '../__builtins__/shared' const prefixCls = `${stylePrefix}-preview-text` const PlaceholderContext = createContext('N/A') export const usePlaceholder = (value?: Ref) => { const placeholderCtx = useContext(PlaceholderContext) const placeholder = computed(() => { return isValid(value?.value) && value.value !== '' ? value.value : resolveComponent(placeholderCtx.value) || 'N/A' }) return placeholder } const Input = defineComponent({ name: 'FPreviewTextInput', props: ['value'], setup(props, { attrs, slots }) { const value = toRef(props, 'value') const placeholder = usePlaceholder(value) return () => { return h( Space, { class: [prefixCls], style: attrs.style, }, { default: () => [ slots?.prepend?.(), slots?.prefix?.(), placeholder.value, slots?.suffix?.(), slots?.append?.(), ], } ) } }, }) const Select = observer( defineComponent({ name: 'FPreviewTextSelect', props: [], setup(_props, { attrs }) { const fieldRef = useField() const field = fieldRef.value const props = attrs as unknown as SelectProps const dataSource: any[] = field?.dataSource?.length ? field.dataSource : props?.options?.length ? props.options : [] const placeholder = usePlaceholder() const getSelected = () => { const value = props.value if (props.multiple) { return isArr(value) ? value.map((val) => ({ label: val, value: val })) : [] } else { return isValid(value) ? [{ label: value, value }] : [] } } const getLabels = () => { const selected = getSelected() if (!selected.length) { return h( Tag, {}, { default: () => placeholder.value, } ) } return selected.map(({ value, label }, key) => { const text = dataSource?.find((item) => item.value == value)?.label || label return h( Tag, { key, props: { type: 'info', effect: 'light', }, }, { default: () => text || placeholder.value, } ) }) } return () => { return h( Space, { class: [prefixCls], style: attrs.style, }, { default: () => getLabels(), } ) } }, }) ) const Cascader = observer( defineComponent({ name: 'FPreviewTextCascader', props: [], setup(_props, { attrs }) { const fieldRef = useField() const field = fieldRef.value const props = attrs as unknown as CascaderProps const dataSource: any[] = field?.dataSource?.length ? field.dataSource : props?.options?.length ? props.options : [] const placeholder = usePlaceholder() const valueKey = props.props?.value || 'value' const labelKey = props.props?.label || 'label' const getSelected = () => { return isArr(props.value) ? props.value : [] } const findLabel = (value: any, dataSource: any[]) => { for (let i = 0; i < dataSource?.length; i++) { const item = dataSource[i] if (item?.[valueKey] === value) { return item?.[labelKey] } else { const childLabel = findLabel(value, item?.children) if (childLabel) return childLabel } } } const getLabels = () => { const selected = getSelected() if (!selected?.length) { return h( Tag, {}, { default: () => placeholder.value, } ) } return selected.map((value, key) => { const text = findLabel(value, dataSource) return h( Tag, { key, props: { type: 'info', effect: 'light', }, }, { default: () => text || placeholder.value, } ) }) } return () => { return h( Space, { class: [prefixCls], style: attrs.style, }, { default: () => getLabels(), } ) } }, }) ) const DatePicker = defineComponent({ name: 'FPreviewTextDatePicker', props: [], setup(_props, { attrs }) { const props = attrs as unknown as DatePickerProps const placeholder = usePlaceholder() const getLabels = () => { if (isArr(props.value)) { const labels = (props.value as any[]).map( (value: String | Date) => formatDate(value, props.format) || placeholder.value ) return labels.join('~') } else { return formatDate(props.value, props.format) || placeholder.value } } return () => { return h( 'div', { class: [prefixCls], style: attrs.style, }, { default: () => getLabels(), } ) } }, }) const TimePicker = defineComponent({ name: 'FPreviewTextTimePicker', props: [], setup(_props, { attrs }) { const props = attrs as unknown as TimePickerProps const format = props.pickerOptions?.format || 'HH:mm:ss' const placeholder = usePlaceholder() const getLabels = () => { if (isArr(props.value)) { const labels = props.value.map( (value) => formatDate(value, format) || placeholder.value ) return labels.join('~') } else { return formatDate(props.value, format) || placeholder.value } } return () => { return h( 'div', { class: [prefixCls], style: attrs.style, }, { default: () => getLabels(), } ) } }, }) const Text = defineComponent({ name: 'FPreviewText', setup(_props, { attrs }) { const placeholder = usePlaceholder() return () => { return h( 'div', { class: [prefixCls], style: attrs.style, }, { default: () => placeholder.value, } ) } }, }) export const PreviewText = composeExport(Text, { Input, Select, Cascader, DatePicker, TimePicker, Placeholder: PlaceholderContext.Provider, usePlaceholder, }) export default PreviewText ================================================ FILE: packages/element/src/preview-text/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/tag.scss' // 依赖 import '../input/style' import '../select/style' import '../cascader/style' import '../time-picker/style' import '../date-picker/style' import '../space/style' ================================================ FILE: packages/element/src/radio/index.ts ================================================ import { connect, h, mapProps, mapReadPretty } from '@formily/vue' import type { Radio as ElRadioProps, RadioGroup as ElRadioGroupProps, } from 'element-ui' import { Radio as ElRadio, RadioButton, RadioGroup as ElRadioGroup, } from 'element-ui' import { defineComponent, PropType } from 'vue-demi' import { PreviewText } from '../preview-text' import { composeExport, resolveComponent, SlotTypes, transformComponent, } from '../__builtins__/shared' export type RadioGroupProps = ElRadioGroupProps & { value: any options?: ( | (Omit & { value: ElRadioProps['label'] label: SlotTypes }) | string )[] optionType: 'default' | 'button' } export type RadioProps = ElRadioProps const TransformElRadioGroup = transformComponent(ElRadioGroup, { change: 'input', uselessChange:'change' }) const RadioGroupOption = defineComponent({ name: 'FRadioGroup', props: { options: { type: Array as PropType, default: () => [], }, optionType: { type: String as PropType, default: 'default', }, }, setup(customProps, { attrs, slots, listeners }) { return () => { const options = customProps.options || [] const OptionType = customProps.optionType === 'button' ? RadioButton : ElRadio const children = options.length !== 0 ? { default: () => options.map((option) => { if (typeof option === 'string') { return h( OptionType, { props: { label: option } }, { default: () => [ resolveComponent(slots?.option ?? option, { option }), ], } ) } else { return h( OptionType, { props: { ...option, value: undefined, label: option.value, }, }, { default: () => [ resolveComponent(slots?.option ?? option.label, { option, }), ], } ) } }), } : slots return h( TransformElRadioGroup, { attrs: { ...attrs, }, on: listeners, }, children ) } }, }) const RadioGroup = connect( RadioGroupOption, mapProps({ dataSource: 'options' }), mapReadPretty(PreviewText.Select) ) export const Radio = composeExport(ElRadio, { Group: RadioGroup, }) export default Radio ================================================ FILE: packages/element/src/radio/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/radio.scss' import 'element-ui/packages/theme-chalk/src/radio-group.scss' import 'element-ui/packages/theme-chalk/src/radio-button.scss' ================================================ FILE: packages/element/src/reset/index.ts ================================================ import { IFieldResetOptions } from '@formily/core' import { observer } from '@formily/reactive-vue' import { h, useParentForm } from '@formily/vue' import { defineComponent } from 'vue-demi' import type { Button as IElButton } from 'element-ui' import { Button as ElButton } from 'element-ui' export type ResetProps = IFieldResetOptions & IElButton export const Reset = observer( defineComponent({ name: 'FReset', props: { forceClear: { type: Boolean, default: false, }, validate: { type: Boolean, default: false, }, }, setup(props, context) { const formRef = useParentForm() const { listeners, slots } = context return () => { const form = formRef?.value return h( ElButton, { attrs: context.attrs, on: { ...listeners, click: (e: any) => { if (listeners?.click) { if (listeners.click(e) === false) return } form ?.reset('*', { forceClear: props.forceClear, validate: props.validate, }) .then(listeners.resetValidateSuccess as (e: any) => void) .catch(listeners.resetValidateFailed as (e: any) => void) }, }, }, slots ) } }, }) ) export default Reset ================================================ FILE: packages/element/src/reset/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/button.scss' ================================================ FILE: packages/element/src/select/index.ts ================================================ import { connect, h, mapProps, mapReadPretty } from '@formily/vue' import { defineComponent } from 'vue-demi' import { PreviewText } from '../preview-text' import type { Option as ElOptionProps, Select as ElSelectProps, } from 'element-ui' import { Option as ElOption, Select as ElSelect } from 'element-ui' import { resolveComponent } from '../__builtins__' export type SelectProps = ElSelectProps & { options?: Array } const SelectOption = defineComponent({ name: 'FSelect', props: ['options'], setup(customProps, { attrs, slots, listeners }) { return () => { const options = customProps.options || [] const children = options.length !== 0 ? { default: () => options.map((option) => { if (typeof option === 'string') { return h( ElOption, { props: { value: option, label: option } }, { default: () => [ resolveComponent(slots?.option, { option }), ], } ) } else { return h( ElOption, { props: { ...option, }, }, { default: () => [ resolveComponent(slots?.option, { option, }), ], } ) } }), } : slots return h( ElSelect, { attrs: { ...attrs, }, on: listeners, }, children ) } }, }) export const Select = connect( SelectOption, mapProps({ dataSource: 'options', loading: true }), mapReadPretty(PreviewText.Select) ) export default Select ================================================ FILE: packages/element/src/select/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/select.scss' // 依赖 import '../preview-text/style' ================================================ FILE: packages/element/src/space/index.ts ================================================ // https://github.com/vueComponent/ant-design-vue/blob/next/components/space/index.tsx import { h } from '@formily/vue' import { defineComponent } from 'vue-demi' import { stylePrefix } from '../__builtins__/configs' import type { VNode } from 'vue' import { useFormLayout } from '../form-layout' export type SpaceProps = { size: 'small' | 'middle' | 'large' | number direction: 'horizontal' | 'vertical' align: 'start' | 'end' | 'center' | 'baseline' } const spaceSize = { small: 8, middle: 16, large: 24, } export const Space = defineComponent({ name: 'FSpace', props: ['size', 'direction', 'align'], setup(props, { attrs, slots }) { const layout = useFormLayout() return () => { const { align, size = layout.value?.spaceGap ?? 'small', direction = 'horizontal', } = props const prefixCls = `${stylePrefix}-space` const children = slots.default?.() let items: VNode[] = [] if (Array.isArray(children)) { if (children.length === 1) { if ((children[0]['tag'] as string)?.endsWith('Fragment')) { // Fragment hack items = (children[0]['componentOptions'] as { children: VNode[] }) ?.children } else { items = children } } else { items = children } } const len = items.length if (len === 0) { return null } const mergedAlign = align === undefined && direction === 'horizontal' ? 'center' : align const someSpaceClass = { [prefixCls]: true, [`${prefixCls}-${direction}`]: true, [`${prefixCls}-align-${mergedAlign}`]: mergedAlign, } const itemClassName = `${prefixCls}-item` const marginDirection = 'marginRight' // directionConfig === 'rtl' ? 'marginLeft' : 'marginRight'; const renderItems = items.map((child, i) => h( 'div', { class: itemClassName, key: `${itemClassName}-${i}`, }, { default: () => [child] } ) ) return h( 'div', { ...attrs, class: { ...(attrs as any).class, ...someSpaceClass }, style: { ...(attrs as any).style, gap: typeof size === 'string' ? `${spaceSize[size]}px` : `${size}px`, }, }, { default: () => renderItems } ) } }, }) export default Space ================================================ FILE: packages/element/src/space/style.scss ================================================ @import '../__builtins__/styles/common.scss'; .#{$formily-prefix}-space { display: inline-flex; &-vertical { flex-direction: column; } &-align { &-center { align-items: center; } &-start { align-items: flex-start; } &-end { align-items: flex-end; } &-baseline { align-items: baseline; } } &-item { display: contents; } } ================================================ FILE: packages/element/src/space/style.ts ================================================ import './style.scss' ================================================ FILE: packages/element/src/style.ts ================================================ // auto generated code import './array-base/style.scss' import './array-cards/style.scss' import './array-collapse/style.scss' import './array-items/style.scss' import './array-table/style.scss' import './array-tabs/style.scss' import './editable/style.scss' import './form-button-group/style.scss' import './form-collapse/style.scss' import './form-drawer/style.scss' import './form-grid/style.scss' import './form-item/style.scss' import './form-layout/style.scss' import './form-tab/style.scss' import './form/style.scss' import './space/style.scss' ================================================ FILE: packages/element/src/submit/index.ts ================================================ import { IFormFeedback } from '@formily/core' import { observer } from '@formily/reactive-vue' import { h, useParentForm } from '@formily/vue' import { defineComponent } from 'vue-demi' import type { Button as ElButtonProps } from 'element-ui' import { Button as ElButton } from 'element-ui' export interface ISubmitProps extends ElButtonProps { onClick?: (e: MouseEvent) => any onSubmit?: (values: any) => any onSubmitSuccess?: (payload: any) => void onSubmitFailed?: (feedbacks: IFormFeedback[]) => void } export const Submit = observer( defineComponent({ name: 'FSubmit', props: ['onClick', 'onSubmit', 'onSubmitSuccess', 'onSubmitFailed'], setup(props, { attrs, slots, listeners }) { const formRef = useParentForm() return () => { const { onClick = listeners?.click, onSubmit = listeners?.submit, onSubmitSuccess = listeners?.submitSuccess, onSubmitFailed = listeners?.submitFailed, } = props const form = formRef?.value return h( ElButton, { attrs: { nativeType: listeners?.submit ? 'button' : 'submit', type: 'primary', ...attrs, loading: attrs.loading !== undefined ? attrs.loading : form?.submitting, }, on: { ...listeners, click: (e: any) => { if (onClick) { if (onClick(e) === false) return } if (onSubmit) { form ?.submit(onSubmit as (e: any) => void) .then(onSubmitSuccess as (e: any) => void) .catch(onSubmitFailed as (e: any) => void) } }, }, }, slots ) } }, }) ) export default Submit ================================================ FILE: packages/element/src/submit/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/button.scss' ================================================ FILE: packages/element/src/switch/index.ts ================================================ import { connect, mapProps } from '@formily/vue' import type { Switch as ElSwitchProps } from 'element-ui' import { Switch as ElSwitch } from 'element-ui' export type SwitchProps = ElSwitchProps export const Switch = connect(ElSwitch, mapProps({ readOnly: 'readonly' })) export default Switch ================================================ FILE: packages/element/src/switch/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/switch.scss' ================================================ FILE: packages/element/src/time-picker/index.ts ================================================ import { transformComponent } from '../__builtins__/shared' import { connect, mapProps, mapReadPretty } from '@formily/vue' import { PreviewText } from '../preview-text' import type { TimePicker as ElTimePickerProps } from 'element-ui' import { TimePicker as ElTimePicker } from 'element-ui' export type TimePickerProps = ElTimePickerProps const TransformElTimePicker = transformComponent( ElTimePicker, { change: 'input', } ) export const TimePicker = connect( TransformElTimePicker, mapProps({ readOnly: 'readonly' }), mapReadPretty(PreviewText.TimePicker) ) export default TimePicker ================================================ FILE: packages/element/src/time-picker/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/time-picker.scss' // 依赖 import '../preview-text/style' ================================================ FILE: packages/element/src/transfer/index.ts ================================================ import { connect, mapProps } from '@formily/vue' import type { Transfer as ElTransferProps } from 'element-ui' import { Transfer as ElTransfer } from 'element-ui' export type TransferProps = ElTransferProps export const Transfer = connect(ElTransfer, mapProps({ dataSource: 'data' })) export default Transfer ================================================ FILE: packages/element/src/transfer/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/transfer.scss' ================================================ FILE: packages/element/src/upload/index.ts ================================================ import { Field } from '@formily/core' import { connect, Fragment, h, mapProps, useField } from '@formily/vue' import { defineComponent } from 'vue-demi' import type { ElUpload as ElUploadProps, ElUploadInternalFileDetail, } from 'element-ui/types/upload' import { Button as ElButton, Upload as ElUpload } from 'element-ui' export type UploadProps = ElUploadProps & { textContent?: String errorAdaptor?: (error?: ErrorEvent) => String } const UploadWrapper = defineComponent({ name: 'FUpload', props: { textContent: { type: String, default: '', }, errorAdaptor: { type: Function, default(error?: ErrorEvent) { return error?.message || '' }, }, }, setup(curProps: UploadProps, { slots, attrs, listeners, emit }) { return () => { const fieldRef = useField() const setFeedBack = (error?: ErrorEvent) => { const message = curProps.errorAdaptor(error) fieldRef.value.setFeedback({ type: 'error', code: 'UploadError', messages: message ? [message] : [], }) } const props = { ...attrs, onChange( file: ElUploadInternalFileDetail, fileList: ElUploadInternalFileDetail[] ) { ;(attrs.onChange as Function)?.(file, fileList) setFeedBack() emit('change', fileList) }, onRemove( file: ElUploadInternalFileDetail, fileList: ElUploadInternalFileDetail[] ) { ;(attrs.onRemove as Function)?.(file, fileList) setFeedBack() emit('change', fileList) }, onError( error: ErrorEvent, file: ElUploadInternalFileDetail, fileList: ElUploadInternalFileDetail[] ) { ;(attrs.onError as Function)?.(error, file, fileList) setTimeout(() => { setFeedBack(error) }, 0) }, } const children = { ...slots, } if (!slots.default) { children.default = () => { const listType = attrs.listType const drag = attrs.drag if (drag) { return h( Fragment, {}, { default: () => [ h('i', { staticClass: 'el-icon-upload' }, {}), h( 'div', { staticClass: 'el-upload__text' }, { default: () => [curProps.textContent] } ), ], } ) } if (listType === 'picture-card') { return h( 'i', { staticClass: 'el-icon-plus', }, {} ) } return h( ElButton, { props: { icon: 'el-icon-upload2' } }, { default: () => [curProps.textContent] } ) } } return h(ElUpload, { attrs: props, on: listeners }, children) } }, }) export const Upload = connect( UploadWrapper, mapProps({ readOnly: 'readonly', value: 'fileList' }) ) export default Upload ================================================ FILE: packages/element/src/upload/style.ts ================================================ import 'element-ui/packages/theme-chalk/src/upload.scss' import 'element-ui/packages/theme-chalk/src/button.scss' ================================================ FILE: packages/element/transformer.ts ================================================ import createTransformer from 'ts-import-plugin' const transformer = createTransformer({ libraryName: 'element-ui', libraryDirectory: 'lib', camel2DashComponentName: true, style: false, }) export default function () { return transformer } ================================================ FILE: packages/element/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./lib", "paths": { "@formily/*": ["packages/*", "devtools/*"] }, "declaration": true } } ================================================ FILE: packages/element/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./lib", "skipLibCheck": true, "paths": { "@formily/*": ["../../packages/*", "../../devtools/*"] }, "plugins": [{ "transform": "./transformer.ts", "after": true }], "lib": ["ESNext", "DOM"] }, "include": ["./src/**/*.ts", "./src/**/*.tsx"], "exclude": ["./esm/*", "./lib/*"] } ================================================ FILE: packages/grid/.npmignore ================================================ node_modules *.log build docs doc-site __tests__ .eslintrc jest.config.js tsconfig.json .umi src ================================================ FILE: packages/grid/LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2015-present, Alibaba Group Holding Limited. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/grid/README.md ================================================ # @formily/grid ================================================ FILE: packages/grid/package.json ================================================ { "name": "@formily/grid", "version": "2.3.7", "license": "MIT", "main": "lib", "module": "esm", "umd:main": "dist/formily.grid.umd.production.js", "unpkg": "dist/formily.grid.umd.production.js", "jsdelivr": "dist/formily.grid.umd.production.js", "jsnext:main": "esm", "repository": { "type": "git", "url": "git+https://github.com/alibaba/formily.git" }, "types": "esm/index.d.ts", "bugs": { "url": "https://github.com/alibaba/formily/issues" }, "homepage": "https://github.com/alibaba/formily#readme", "engines": { "npm": ">=3.0.0" }, "scripts": { "build": "rimraf -rf lib esm dist && npm run build:cjs && npm run build:esm && npm run build:umd", "build:cjs": "tsc --project tsconfig.build.json", "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm", "build:umd": "rollup --config" }, "peerDependencies": { "typescript": "4.x || 5.x" }, "dependencies": { "@formily/reactive": "2.3.7", "@juggle/resize-observer": "^3.3.1" }, "publishConfig": { "access": "public" }, "gitHead": "ac79c196ae9324889aca5e0501146f9b37b04283" } ================================================ FILE: packages/grid/rollup.config.js ================================================ import baseConfig from '../../scripts/rollup.base.js' export default baseConfig('formily.grid', 'Formily.Grid') ================================================ FILE: packages/grid/src/index.ts ================================================ import { define, observable, batch, reaction } from '@formily/reactive' import { ChildListMutationObserver } from './observer' import { ResizeObserver } from '@juggle/resize-observer' export interface IGridOptions { maxRows?: number maxColumns?: number | number[] minColumns?: number | number[] maxWidth?: number | number[] minWidth?: number | number[] breakpoints?: number[] columnGap?: number rowGap?: number colWrap?: boolean strictAutoFit?: boolean shouldVisible?: (node: GridNode, grid: Grid) => boolean onDigest?: (grid: Grid) => void onInitialized?: (grid: Grid) => void } const SpanRegExp = /span\s*(\d+)/ const isValid = (value: any) => value !== undefined && value !== null const calcBreakpointIndex = (breakpoints: number[], width: number) => { if (Array.isArray(breakpoints)) { for (let i = 0; i < breakpoints.length; i++) { if (width <= breakpoints[i]) { return i } } } return -1 } const calcFactor = (value: T | T[], breakpointIndex: number): T => { if (Array.isArray(value)) { if (breakpointIndex === -1) return value[0] return value[breakpointIndex] ?? value[value.length - 1] } else { return value } } const parseGridNode = (elements: HTMLCollection): GridNode[] => { return Array.from(elements).reduce((buf, element: HTMLElement, index) => { const style = getComputedStyle(element) const visible = !(style.display === 'none') const origin = element.getAttribute('data-grid-span') const span = parseSpan(style.gridColumnStart) ?? 1 const originSpan = Number(origin ?? span) const node: GridNode = { index, span, visible, originSpan, element, } if (!origin) { element.setAttribute('data-grid-span', String(span)) } return buf.concat(node) }, []) } const calcChildTotalColumns = (nodes: GridNode[], shadow = false) => nodes.reduce((buf, node) => { if (!shadow) { if (!node.visible) return buf } if (node.originSpan === -1) return buf + (node.span ?? 1) return buf + node.span }, 0) const calcChildOriginTotalColumns = (nodes: GridNode[], shadow = false) => nodes.reduce((buf, node) => { if (!shadow) { if (!node.visible) return buf } if (node.originSpan === -1) return buf + (node.span ?? 1) return buf + node.originSpan }, 0) const calcSatisfyColumns = ( width: number, maxColumns: number, minColumns: number, maxWidth: number, minWidth: number, gap: number ) => { const results = [] for (let columns = minColumns; columns <= maxColumns; columns++) { const innerWidth = width - (columns - 1) * gap const columnWidth = innerWidth / columns if (columnWidth >= minWidth && columnWidth <= maxWidth) { results.push(columns) } else if (columnWidth < minWidth) { results.push(Math.min(Math.floor(innerWidth / minWidth), maxColumns)) } else if (columnWidth > maxWidth) { results.push(Math.min(Math.floor(innerWidth / maxWidth), maxColumns)) } } return Math.max(...results) } const parseSpan = (gridColumnStart: string) => { return Number(String(gridColumnStart).match(SpanRegExp)?.[1] ?? 1) } const factor = (value: T | T[], grid: Grid): T => isValid(value) ? calcFactor(value as any, grid.breakpoint) : value const resolveChildren = (grid: Grid) => { let walked = 0, shadowWalked = 0, rowIndex = 0, shadowRowIndex = 0 if (!grid.ready) return grid.children = grid.children.map((node) => { const columnIndex = walked % grid.columns const shadowColumnIndex = shadowWalked % grid.columns const remainColumns = grid.columns - columnIndex const originSpan = node.originSpan const targetSpan = originSpan > grid.columns ? grid.columns : originSpan const span = grid.options.strictAutoFit ? targetSpan : targetSpan > remainColumns ? remainColumns : targetSpan const gridColumn = originSpan === -1 ? `span ${remainColumns} / -1` : `span ${span} / auto` if (node.element.style.gridColumn !== gridColumn) { node.element.style.gridColumn = gridColumn } if (node.visible) { walked += span } shadowWalked += span if (columnIndex === 0) { rowIndex++ } if (shadowColumnIndex == 0) { shadowRowIndex++ } node.shadowRow = shadowRowIndex node.shadowColumn = shadowColumnIndex + 1 if (node.visible) { node.row = rowIndex node.column = columnIndex + 1 } if (grid.options?.shouldVisible) { if (!grid.options.shouldVisible(node, grid)) { if (node.visible) { node.element.style.display = 'none' } node.visible = false } else { if (!node.visible) { node.element.style.display = '' } node.visible = true } } return node }) } const nextTick = (callback?: () => void) => Promise.resolve(0).then(callback) export type GridNode = { index?: number visible?: boolean column?: number shadowColumn?: number row?: number shadowRow?: number span?: number originSpan?: number element?: HTMLElement } export class Grid { options: IGridOptions width = 0 height = 0 container: Container children: GridNode[] = [] childTotalColumns = 0 shadowChildTotalColumns = 0 childOriginTotalColumns = 0 shadowChildOriginTotalColumns = 0 ready = false constructor(options?: IGridOptions) { this.options = { breakpoints: [720, 1280, 1920], columnGap: 8, rowGap: 4, minWidth: 100, colWrap: true, strictAutoFit: false, ...options, } define(this, { options: observable.shallow, width: observable.ref, height: observable.ref, ready: observable.ref, children: observable.ref, childOriginTotalColumns: observable.ref, shadowChildOriginTotalColumns: observable.ref, shadowChildTotalColumns: observable.ref, childTotalColumns: observable.ref, columns: observable.computed, templateColumns: observable.computed, gap: observable.computed, maxColumns: observable.computed, minColumns: observable.computed, maxWidth: observable.computed, minWidth: observable.computed, breakpoints: observable.computed, breakpoint: observable.computed, rowGap: observable.computed, columnGap: observable.computed, colWrap: observable.computed, }) } set breakpoints(breakpoints) { this.options.breakpoints = breakpoints } get breakpoints() { return this.options.breakpoints } get breakpoint() { return calcBreakpointIndex(this.options.breakpoints, this.width) } set maxWidth(maxWidth) { this.options.maxWidth = maxWidth } get maxWidth() { return factor(this.options.maxWidth, this) ?? Infinity } set minWidth(minWidth) { this.options.minWidth = minWidth } get minWidth() { return factor(this.options.minWidth, this) ?? 100 } set maxColumns(maxColumns) { this.options.maxColumns = maxColumns } get maxColumns() { return factor(this.options.maxColumns, this) ?? Infinity } set maxRows(maxRows) { this.options.maxRows = maxRows } get maxRows() { return this.options.maxRows ?? Infinity } set minColumns(minColumns) { this.options.minColumns = minColumns } get minColumns() { return factor(this.options.minColumns, this) ?? 1 } set rowGap(rowGap) { this.options.rowGap = rowGap } get rowGap() { return factor(this.options.rowGap, this) ?? 5 } set columnGap(columnGap) { this.options.columnGap = columnGap } get columnGap() { return factor(this.options.columnGap, this) ?? 10 } set colWrap(colWrap) { this.options.colWrap = colWrap } get colWrap() { return factor(this.options.colWrap, this) ?? true } get columns() { if (!this.ready) return 0 const originTotalColumns = this.childOriginTotalColumns if (this.colWrap === false) { return originTotalColumns } const baseColumns = this.childSize const strictMaxWidthColumns = Math.round( this.width / (this.maxWidth + this.columnGap) ) const looseMaxWidthColumns = Math.min( originTotalColumns, strictMaxWidthColumns ) const maxWidthColumns = this.options.strictAutoFit ? strictMaxWidthColumns : looseMaxWidthColumns const strictMinWidthColumns = Math.round( this.width / (this.minWidth + this.columnGap) ) const looseMinWidthColumns = Math.min( originTotalColumns, strictMinWidthColumns ) const minWidthColumns = this.options.strictAutoFit ? strictMinWidthColumns : looseMinWidthColumns const minCalculatedColumns = Math.min( baseColumns, originTotalColumns, maxWidthColumns, minWidthColumns ) const maxCalculatedColumns = Math.max( baseColumns, originTotalColumns, maxWidthColumns, minWidthColumns ) const finalColumns = calcSatisfyColumns( this.width, maxCalculatedColumns, minCalculatedColumns, this.maxWidth, this.minWidth, this.columnGap ) if (finalColumns >= this.maxColumns) { return this.maxColumns } if (finalColumns <= this.minColumns) { return this.minColumns } return finalColumns } get rows() { return Math.ceil(this.childTotalColumns / this.columns) } get shadowRows() { return Math.ceil(this.shadowChildTotalColumns / this.columns) } get templateColumns() { if (!this.width) return '' if (this.maxWidth === Infinity) { return `repeat(${this.columns},minmax(0,1fr))` } if (this.options.strictAutoFit !== true) { const columnWidth = (this.width - (this.columns - 1) * this.columnGap) / this.columns if (columnWidth < this.minWidth || columnWidth > this.maxWidth) { return `repeat(${this.columns},minmax(0,1fr))` } } return `repeat(${this.columns},minmax(${this.minWidth}px,${this.maxWidth}px))` } get gap() { return `${this.rowGap}px ${this.columnGap}px` } get childSize() { return this.children.length } get fullnessLastColumn() { return this.columns === this.children[this.childSize - 1]?.span } connect = (container: Container) => { if (container) { this.container = container const initialize = batch.bound(() => { digest() this.ready = true }) const digest = batch.bound(() => { this.children = parseGridNode(this.container.children) this.childTotalColumns = calcChildTotalColumns(this.children) this.shadowChildTotalColumns = calcChildTotalColumns( this.children, true ) this.childOriginTotalColumns = calcChildOriginTotalColumns( this.children ) this.shadowChildOriginTotalColumns = calcChildOriginTotalColumns( this.children, true ) const rect = this.container.getBoundingClientRect() if (rect.width && rect.height) { this.width = rect.width this.height = rect.height } resolveChildren(this) nextTick(() => { this.options?.onDigest?.(this) }) if (!this.ready) { nextTick(() => { this.options?.onInitialized?.(this) }) } }) const mutationObserver = new ChildListMutationObserver(digest) // add requestAnimationFrame to smooth digest const smoothDigest = () => { requestAnimationFrame(() => { digest() }) } const resizeObserver = new ResizeObserver(smoothDigest) const dispose = reaction(() => ({ ...this.options }), digest) resizeObserver.observe(this.container) mutationObserver.observe(this.container, { attributeFilter: ['data-grid-span'], attributes: true, }) initialize() return () => { resizeObserver.unobserve(this.container) resizeObserver.disconnect() mutationObserver.disconnect() dispose() this.children = [] } } return () => {} } static id = (options: IGridOptions = {}) => JSON.stringify( [ 'maxRows', 'maxColumns', 'minColumns', 'maxWidth', 'minWidth', 'breakpoints', 'columnGap', 'rowGap', 'colWrap', 'strictAutoFit', ].map((key) => options[key]) ) } ================================================ FILE: packages/grid/src/observer.ts ================================================ const isHTMLElement = (node: Node): node is HTMLElement => node.nodeType === 1 type ChildNode = { element?: HTMLElement observer?: MutationObserver dispose?: () => void } export class ChildListMutationObserver { observer: MutationObserver callback: MutationCallback childList: ChildNode[] = [] init: MutationObserverInit constructor(callback: MutationCallback) { this.callback = callback this.observer = new MutationObserver(this.handler) } observeChildList(element: HTMLElement) { Array.from(element.children).forEach((node: HTMLElement) => { this.addObserver(node) }) } addObserver(element: HTMLElement) { const child = this.childList.find((t) => t.element === element) if (!child) { const childIndex = this.childList.length const child = { element, observer: new MutationObserver(this.callback), dispose: () => { if (child.observer) { child.observer.disconnect() delete child.observer this.childList.splice(childIndex, 1) } }, } child.observer.observe(child.element, { ...this.init, subtree: false, childList: false, characterData: false, characterDataOldValue: false, attributeOldValue: false, }) this.childList.push(child) } } removeObserver(element: HTMLElement) { const child = this.childList.find((t) => t.element === element) if (child) { child.dispose?.() } } handler = (mutations: MutationRecord[]) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { mutation.addedNodes.forEach((node) => { if (isHTMLElement(node)) { this.addObserver(node) } }) mutation.removedNodes.forEach((node) => { if (isHTMLElement(node)) { this.removeObserver(node) } }) } }) this.callback(mutations, this.observer) } observe = (element: HTMLElement, init?: MutationObserverInit) => { this.init = init this.observeChildList(element) this.observer.observe(element, { ...this.init, subtree: false, childList: true, characterData: false, characterDataOldValue: false, attributeOldValue: false, }) } disconnect = () => { this.observer.disconnect() } } ================================================ FILE: packages/grid/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./lib", "paths": { "@formily/*": ["packages/*", "devtools/*"] }, "declaration": true } } ================================================ FILE: packages/grid/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["./src/**/*.ts", "./src/**/*.tsx"], "exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"], "compilerOptions": { "lib": ["ESNext", "DOM"] } } ================================================ FILE: packages/json-schema/.npmignore ================================================ node_modules *.log build docs doc-site __tests__ .eslintrc jest.config.js tsconfig.json .umi src ================================================ FILE: packages/json-schema/LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2015-present, Alibaba Group Holding Limited. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/json-schema/README.md ================================================ # @formily/json-schema ================================================ FILE: packages/json-schema/package.json ================================================ { "name": "@formily/json-schema", "version": "2.3.7", "license": "MIT", "main": "lib", "module": "esm", "umd:main": "dist/formily.json-schema.umd.production.js", "unpkg": "dist/formily.json-schema.umd.production.js", "jsdelivr": "dist/formily.json-schema.umd.production.js", "jsnext:main": "esm", "repository": { "type": "git", "url": "git+https://github.com/alibaba/formily.git" }, "types": "esm/index.d.ts", "bugs": { "url": "https://github.com/alibaba/formily/issues" }, "homepage": "https://github.com/alibaba/formily#readme", "engines": { "npm": ">=3.0.0" }, "scripts": { "build": "rimraf -rf lib esm dist && npm run build:cjs && npm run build:esm && npm run build:umd", "build:cjs": "tsc --project tsconfig.build.json", "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm", "build:umd": "rollup --config" }, "peerDependencies": { "typescript": ">4.1.5" }, "dependencies": { "@formily/core": "2.3.7", "@formily/reactive": "2.3.7", "@formily/shared": "2.3.7" }, "publishConfig": { "access": "public" }, "gitHead": "ac79c196ae9324889aca5e0501146f9b37b04283" } ================================================ FILE: packages/json-schema/rollup.config.js ================================================ import baseConfig from '../../scripts/rollup.base.js' export default baseConfig('formily.json-schema', 'Formily.JSONSchema') ================================================ FILE: packages/json-schema/src/__tests__/__snapshots__/schema.spec.ts.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`all methods 1`] = ` Object { "_isJSONSchemaObject": true, "additionalProperties": Object { "_isJSONSchemaObject": true, "type": "string", "version": "2.0", }, "patternProperties": Object { "^[a-zA-Z0-9]*$": Object { "_isJSONSchemaObject": true, "name": "^[a-zA-Z0-9]*$", "properties": Object { "made": Object { "_isJSONSchemaObject": true, "name": "made", "type": "string", "version": "2.0", "x-index": 1, }, "model": Object { "_isJSONSchemaObject": true, "name": "model", "type": "string", "version": "2.0", "x-index": 2, }, "year": Object { "_isJSONSchemaObject": true, "name": "year", "type": "string", "version": "2.0", "x-index": 0, }, }, "version": "2.0", }, }, "properties": Object { "array": Object { "_isJSONSchemaObject": true, "additionalItems": Object { "_isJSONSchemaObject": true, "type": "string", "version": "2.0", }, "items": Array [ Object { "_isJSONSchemaObject": true, "type": "integer", "version": "2.0", }, Object { "_isJSONSchemaObject": true, "type": "integer", "version": "2.0", }, ], "name": "array", "title": "string", "type": "string", "version": "2.0", }, "string": Object { "_isJSONSchemaObject": true, "description": null, "name": "string", "title": "string", "type": "string", "version": "2.0", "x-reactions": Array [ Object { "fulfill": Object { "schema": Object {}, }, "target": "xxx", "when": true, }, ], }, }, "type": "object", "version": "2.0", "x-reactions": null, } `; exports[`all methods 2`] = ` Object { "_isJSONSchemaObject": true, "additionalProperties": Object { "_isJSONSchemaObject": true, "type": "string", "version": "2.0", }, "patternProperties": Object { "^[a-zA-Z0-9]*$": Object { "_isJSONSchemaObject": true, "name": "^[a-zA-Z0-9]*$", "properties": Object { "made": Object { "_isJSONSchemaObject": true, "name": "made", "type": "string", "version": "2.0", "x-index": 1, }, "model": Object { "_isJSONSchemaObject": true, "name": "model", "type": "string", "version": "2.0", "x-index": 2, }, "year": Object { "_isJSONSchemaObject": true, "name": "year", "type": "string", "version": "2.0", "x-index": 0, }, }, "version": "2.0", }, }, "properties": Object { "array": Object { "_isJSONSchemaObject": true, "additionalItems": Object { "_isJSONSchemaObject": true, "type": "string", "version": "2.0", }, "items": Array [ Object { "_isJSONSchemaObject": true, "type": "integer", "version": "2.0", }, Object { "_isJSONSchemaObject": true, "type": "integer", "version": "2.0", }, ], "name": "array", "title": "string", "type": "string", "version": "2.0", }, "string": Object { "_isJSONSchemaObject": true, "description": null, "name": "string", "title": "string", "type": "string", "version": "2.0", "x-reactions": Array [ Object { "fulfill": Object { "schema": Object {}, }, "target": "xxx", "when": true, }, ], }, }, "type": "object", "version": "2.0", "x-reactions": null, } `; exports[`all methods 3`] = ` Object { "_isJSONSchemaObject": true, "additionalProperties": Object { "_isJSONSchemaObject": true, "type": "string", "version": "2.0", }, "patternProperties": Object {}, "properties": Object { "array": Object { "_isJSONSchemaObject": true, "additionalItems": Object { "_isJSONSchemaObject": true, "type": "string", "version": "2.0", }, "items": Array [ Object { "_isJSONSchemaObject": true, "type": "integer", "version": "2.0", }, Object { "_isJSONSchemaObject": true, "type": "integer", "version": "2.0", }, ], "name": "array", "title": "string", "type": "string", "version": "2.0", }, }, "type": "object", "version": "2.0", "x-reactions": null, } `; exports[`all methods 4`] = ` Object { "_isJSONSchemaObject": true, "description": null, "name": "string", "title": "string", "type": "string", "version": "2.0", "x-reactions": Array [ Object { "fulfill": Object { "schema": Object {}, }, "target": "xxx", "when": true, }, ], } `; exports[`all methods 5`] = ` Object { "_isJSONSchemaObject": true, "additionalItems": undefined, "additionalProperties": undefined, "items": Array [ undefined, ], "properties": Object { "xxx": undefined, }, "type": "object", "version": "2.0", } `; exports[`all methods 6`] = ` Object { "_isJSONSchemaObject": true, "additionalItems": undefined, "additionalProperties": undefined, "items": Array [ undefined, ], "properties": Object { "xxx": undefined, }, "type": "object", "version": "2.0", } `; exports[`all props 1`] = ` Object { "_isJSONSchemaObject": true, "additionalProperties": Object { "_isJSONSchemaObject": true, "type": "string", "version": "2.0", }, "description": "description", "patternProperties": Object { "^[a-zA-Z0-9]*$": Object { "_isJSONSchemaObject": true, "name": "^[a-zA-Z0-9]*$", "properties": Object { "made": Object { "_isJSONSchemaObject": true, "name": "made", "type": "string", "version": "2.0", }, "model": Object { "_isJSONSchemaObject": true, "name": "model", "type": "string", "version": "2.0", }, "year": Object { "_isJSONSchemaObject": true, "name": "year", "type": "string", "version": "2.0", }, }, "version": "2.0", }, }, "properties": Object { "array": Object { "_isJSONSchemaObject": true, "additionalItems": Object { "_isJSONSchemaObject": true, "type": "number", "version": "2.0", }, "items": Object { "_isJSONSchemaObject": true, "type": "string", "version": "2.0", }, "name": "array", "type": "array", "version": "2.0", }, "array2": Object { "_isJSONSchemaObject": true, "items": Array [ Object { "_isJSONSchemaObject": true, "type": "string", "version": "2.0", }, Object { "_isJSONSchemaObject": true, "type": "object", "version": "2.0", }, ], "name": "array2", "type": "array", "version": "2.0", }, "boolean": Object { "_isJSONSchemaObject": true, "default": false, "name": "boolean", "type": "boolean", "version": "2.0", }, "date": Object { "_isJSONSchemaObject": true, "default": "2020-12-23", "name": "date", "type": "date", "version": "2.0", }, "datetime": Object { "_isJSONSchemaObject": true, "default": "2020-12-23 23:00:00", "name": "datetime", "type": "datetime", "version": "2.0", }, "number": Object { "_isJSONSchemaObject": true, "default": 100, "name": "number", "type": "number", "version": "2.0", }, "string": Object { "_isJSONSchemaObject": true, "default": "default", "name": "string", "required": true, "type": "string", "version": "2.0", "x-component": "Input", "x-component-props": Object { "placeholder": "placeholder", }, "x-decorator": "FormItem", "x-decorator-props": Object { "labelCol": 3, }, "x-disabled": true, "x-display": "visible", "x-editable": false, "x-hidden": false, "x-pattern": "readPretty", "x-reactions": Array [ Object { "target": "xxx", "when": "{{aa > bb}}", }, ], "x-read-only": true, "x-validator": Array [ "phone", ], }, "void": Object { "_isJSONSchemaObject": true, "name": "void", "type": "void", "version": "2.0", }, }, "title": "title", "type": "object", "version": "2.0", } `; ================================================ FILE: packages/json-schema/src/__tests__/compiler.spec.ts ================================================ import { compile, silent, registerCompiler, shallowCompile, patchCompile, patchSchemaCompile, } from '../compiler' import { Schema } from '../schema' test('compile', () => { expect(compile('{{123}}xx')).toEqual('{{123}}xx') expect(compile('{{123}} ')).toEqual(123) expect(compile('{{123}}')).toEqual(123) expect( compile({ hello: '{{123}}', }) ).toEqual({ hello: 123, }) expect( compile({ array: ['{{123}}'], }) ).toEqual({ array: [123], }) const date = new Date() date['expression'] = '{{123}}' const compiledDate = compile(date) expect(compiledDate).toEqual(date) expect(compiledDate['expression']).toEqual('{{123}}') const moment = { _isAMomentObject: true, expression: '{{123}}' } const compiledMoment = compile(moment) expect(compiledMoment).toEqual(moment) expect(compiledMoment['expression']).toEqual('{{123}}') const react = { _owner: true, $$typeof: true, expression: '{{123}}' } const compiledReact = compile(react) expect(compiledReact).toEqual(react) expect(compiledReact['expression']).toEqual('{{123}}') const actions = { [Symbol.for('__REVA_ACTIONS')]: true, expression: '{{123}}', } const compiledActions = compile(actions) expect(compiledActions).toEqual(actions) expect(compiledActions['expression']).toEqual('{{123}}') const schema = new Schema({ type: 'object', properties: { aa: { type: 'string', 'x-component': 'Input', 'x-component-props': '{{123}}', }, }, }) const compiledSchema = schema.compile() expect(compiledSchema.toJSON()).toEqual(schema.toJSON()) expect(compiledSchema.properties?.['aa']['x-component-props']).toEqual( '{{123}}' ) const toJSable = { toJS() { return { aa: 123, } }, expression: '{{123}}', } const compiledToJSable = compile(toJSable) expect(compiledToJSable).toEqual(toJSable) expect(compiledToJSable['expression']).toEqual('{{123}}') const toJSONable = { toJSON() { return { aa: 123, } }, expression: '{{123}}', } const compiledToJSONable = compile(toJSONable) expect(compiledToJSONable).toEqual(toJSONable) expect(compiledToJSONable['expression']).toEqual('{{123}}') const circularRef = { expression: '{{123}}', } circularRef['self'] = circularRef const compiledCircularRef = compile(circularRef) expect(compiledCircularRef['expression']).toEqual(123) }) test('shallowCompile', () => { expect(shallowCompile('{{123}}xx')).toEqual('{{123}}xx') expect(shallowCompile('{{123}} ')).toEqual(123) expect(shallowCompile('{{123}}')).toEqual(123) expect( shallowCompile({ hello: '{{123}}', }) ).toEqual({ hello: '{{123}}', }) expect( shallowCompile({ array: ['{{123}}'], }) ).toEqual({ array: ['{{123}}'], }) expect(shallowCompile(['{{123}}'])).toEqual(['{{123}}']) expect(shallowCompile([{ kk: '{{123}}' }])).toEqual([{ kk: '{{123}}' }]) }) test('unsilent', () => { silent(false) expect(() => compile('{{ ( }}')).toThrowError() }) test('silent', () => { silent(true) expect(() => compile('{{ ( }}')).not.toThrowError() silent(false) }) test('patchCompile', () => { const targetState = { title: '', description: '', dataSource: [22], } patchCompile( targetState as any, { title: '132', description: '{{"Hello world"}}', dataSource: [1, 2, 3, '{{333}}'], extend: '333', }, {} ) expect(targetState).toEqual({ title: '132', description: 'Hello world', dataSource: [1, 2, 3, 333], }) }) test('patchSchemaCompile', () => { const targetState = { title: '', description: '', dataSource: [22], } patchSchemaCompile( targetState as any, { title: '132', description: '{{"Hello world"}}', enum: [1, 2, 3, '{{333}}'], 'x-reactions': { fulfill: { schema: { title: 'hello', }, }, }, version: '1.2.3', }, {} ) expect(targetState).toEqual({ title: '132', description: 'Hello world', dataSource: [ { label: 1, value: 1 }, { label: 2, value: 2 }, { label: 3, value: 3 }, { label: 333, value: 333 }, ], }) }) test('patchSchemaCompile demand un initialized', () => { const setValidatorRule = (name: string, value: any) => { targetState[name] = value } const targetState = { title: '', description: '', dataSource: [22], setValidatorRule, } patchSchemaCompile( targetState as any, { title: '132', 'x-display': undefined, 'x-hidden': null, description: '{{"Hello world"}}', enum: [1, 2, 3, '{{333}}'], format: 'phone', 'x-reactions': { fulfill: { schema: { title: 'hello', }, }, }, version: '1.2.3', }, {}, true ) expect(targetState).toEqual({ title: '132', description: 'Hello world', hidden: null, format: 'phone', setValidatorRule, dataSource: [ { label: 1, value: 1 }, { label: 2, value: 2 }, { label: 3, value: 3 }, { label: 333, value: 333 }, ], }) }) test('patchSchemaCompile demand initialized', () => { const targetState = { initialized: true, title: '', description: '', dataSource: [22], } patchSchemaCompile( targetState as any, { title: '132', description: '{{"Hello world"}}', enum: [1, 2, 3, '{{333}}'], 'x-reactions': { fulfill: { schema: { title: 'hello', }, }, }, version: '1.2.3', }, {}, true ) expect(targetState).toEqual({ initialized: true, title: '', description: '', dataSource: [22], }) }) test('patchSchemaCompile x-compile-omitted', () => { const targetState = { title: '', validator: [], } patchSchemaCompile( targetState as any, { title: '132', 'x-validator': [ { remoteCheckUniq: '{{field.value}}', }, ], version: '1.2.3', }, { field: { value: 888, }, } ) expect(targetState).toEqual({ title: '132', validator: [{ remoteCheckUniq: 888 }], }) const targetOmitState = { title: '', validator: [], } patchSchemaCompile( targetOmitState as any, { title: '132', 'x-compile-omitted': ['x-validator'], 'x-validator': [ { remoteCheckUniq: '{{field.value}}', }, ], version: '1.2.3', }, { field: { value: 888, }, } ) expect(targetOmitState).toEqual({ title: '132', validator: [{ remoteCheckUniq: '{{field.value}}' }], }) }) test('registerCompiler', () => { registerCompiler(() => { return 'compiled' }) expect(compile('{{123}}xx')).toEqual('{{123}}xx') expect(compile('{{123}} ')).toEqual('compiled') expect(compile('{{123}}')).toEqual('compiled') expect( compile({ hello: '{{123}}', }) ).toEqual({ hello: 'compiled', }) expect( compile({ array: ['{{123}}'], }) ).toEqual({ array: ['compiled'], }) registerCompiler(null) }) ================================================ FILE: packages/json-schema/src/__tests__/patches.spec.ts ================================================ import { Schema } from '../schema' import { registerTypeDefaultComponents, registerVoidComponents, } from '../polyfills' registerVoidComponents(['MyCard']) registerTypeDefaultComponents({ string: 'Input', }) Schema.enablePolyfills(['1.0']) test('v1 polyfill', () => { const schema = new Schema({ type: 'string', editable: true, } as any) expect(schema['x-editable']).toEqual(true) const schema1 = new Schema({ type: 'string', visible: true, } as any) expect(schema1['x-visible']).toEqual(true) const schema2 = new Schema({ type: 'string', display: false, } as any) expect(schema2['x-display']).toEqual('hidden') expect(schema2['x-display']).toEqual('hidden') const schema3 = new Schema({ type: 'string', 'x-linkages': [ { type: 'value:visible', condition: '{{$value == 123}}', }, ], } as any) expect(schema3['x-reactions']).toEqual([ { when: '{{$self.value == 123}}', fulfill: { state: { visible: true, }, }, otherwise: { state: { visible: false, }, }, }, ]) const schema4 = new Schema({ type: 'string', 'x-linkages': [ { type: 'value:schema', target: 'xxx', condition: '{{$value == 123}}', schema: { title: 'xxx', }, otherwise: { title: '123', }, }, ], } as any) expect(schema4['x-reactions']).toEqual([ { when: '{{$self.value == 123}}', target: 'xxx', fulfill: { schema: { version: '1.0', title: 'xxx', 'x-decorator': 'FormItem', }, }, otherwise: { schema: { version: '1.0', title: '123', 'x-decorator': 'FormItem', }, }, }, ]) const schema5 = new Schema({ type: 'string', 'x-linkages': [ { type: 'value:state', target: 'xxx', condition: '{{$value == 123}}', state: { title: 'xxx', }, otherwise: { title: '123', }, }, ], } as any) expect(schema5['x-reactions']).toEqual([ { when: '{{$self.value == 123}}', target: 'xxx', fulfill: { state: { title: 'xxx', }, }, otherwise: { state: { title: '123', }, }, }, ]) const schema6 = new Schema({ type: 'string', 'x-props': { labelCol: 3, wrapperCol: 4, }, 'x-linkages': [ { type: 'value:visible', condition: null, }, ], } as any) expect(schema6['x-component']).toEqual('Input') expect(schema6['x-decorator']).toEqual('FormItem') expect(schema6['x-decorator-props']).toEqual({ labelCol: 3, wrapperCol: 4, }) const schema7 = new Schema({ type: 'object', 'x-component': 'MyCard', 'x-linkages': {}, } as any) expect(schema7.type === 'void').toBeTruthy() new Schema({ type: 'object', 'x-component': 'MyCard', 'x-linkages': [null], } as any) new Schema({ type: 'object', 'x-component': 'MyCard', 'x-linkages': [{}], } as any) const schema8 = new Schema({ type: 'string', 'x-rules': ['phone'], } as any) expect(schema8['x-validator']).toEqual(['phone']) }) ================================================ FILE: packages/json-schema/src/__tests__/schema.spec.ts ================================================ import { Schema } from '../' import { isFn } from '@formily/shared' test('has methods', () => { const schema = new Schema({ type: 'object', properties: { aa: { type: 'string', }, }, }) expect(isFn(schema.setAdditionalItems)).toBeTruthy() expect(isFn(schema.setAdditionalProperties)).toBeTruthy() expect(isFn(schema.setItems)).toBeTruthy() expect(isFn(schema.setPatternProperties)).toBeTruthy() expect(isFn(schema.setProperties)).toBeTruthy() expect(isFn(schema.addPatternProperty)).toBeTruthy() expect(isFn(schema.addProperty)).toBeTruthy() expect(isFn(schema.fromJSON)).toBeTruthy() expect(isFn(schema.toJSON)).toBeTruthy() expect(isFn(schema.reducePatternProperties)).toBeTruthy() expect(isFn(schema.reduceProperties)).toBeTruthy() expect(isFn(schema.removeProperty)).toBeTruthy() expect(isFn(schema.removePatternProperty)).toBeTruthy() expect(isFn(schema.mapPatternProperties)).toBeTruthy() expect(isFn(schema.mapProperties)).toBeTruthy() expect(isFn(Schema.isSchemaInstance)).toBeTruthy() expect(isFn(Schema.registerCompiler)).toBeTruthy() expect(isFn(Schema.registerPatches)).toBeTruthy() expect(isFn(Schema.shallowCompile)).toBeTruthy() expect(isFn(Schema.compile)).toBeTruthy() expect(isFn(Schema.getOrderProperties)).toBeTruthy() }) test('all props', () => { const schema = new Schema({ type: 'object', title: 'title', description: 'description', patternProperties: { '^[a-zA-Z0-9]*$': { properties: { model: { type: 'string' }, made: { type: 'string' }, year: { type: 'string' }, }, }, }, additionalProperties: { type: 'string', }, properties: { string: { type: 'string', default: 'default', required: true, 'x-component': 'Input', 'x-component-props': { placeholder: 'placeholder', }, 'x-decorator': 'FormItem', 'x-decorator-props': { labelCol: 3, }, 'x-disabled': true, 'x-display': 'visible', 'x-editable': false, 'x-hidden': false, 'x-pattern': 'readPretty', 'x-read-only': true, 'x-validator': ['phone'], 'x-reactions': [ { target: 'xxx', when: '{{aa > bb}}', }, ], }, boolean: { type: 'boolean', default: false, }, number: { type: 'number', default: 100, }, date: { type: 'date', default: '2020-12-23', }, datetime: { type: 'datetime', default: '2020-12-23 23:00:00', }, array: { type: 'array', items: { type: 'string', }, additionalItems: { type: 'number', }, }, array2: { type: 'array', items: [ { type: 'string', }, { type: 'object', }, ], }, void: { type: 'void', }, }, }) expect(schema).toMatchSnapshot() }) test('all methods', () => { const schema = new Schema({ type: 'object', 'x-reactions': null, }) const schema2 = new Schema({ type: 'object', fn: () => {}, } as any) const schema3 = new Schema({ type: 'object', additionalItems: null, additionalProperties: null, properties: null, }) schema3.additionalItems = null schema3.additionalProperties = null schema3.properties = { xxx: null, } schema3.items = [null] const schema4 = new Schema({ type: 'object', additionalItems: {}, additionalProperties: {}, properties: {}, }) schema4.additionalItems = {} as any schema4.additionalProperties = {} as any schema4.properties = { xxx: {} as any, } schema4.items = [{}] as any const schema5 = new Schema({ type: 'array', }) schema5.items = null const schema6 = new Schema({ type: 'array', }) schema6.items = {} as any const schema7 = new Schema({ type: 'array', items: { type: 'string', }, }) const string = schema.addProperty('string', { type: 'string', title: 'string', description: null, 'x-reactions': [ { target: 'xxx', when: true, fulfill: { schema: {}, }, }, ], }) const array = schema.addProperty('array', { type: 'string', title: 'string', items: [{ type: 'integer' }, { type: 'integer' }], }) const pattern = schema.addPatternProperty('^[a-zA-Z0-9]*$', { properties: { model: { type: 'string', 'x-index': 2 }, made: { type: 'string', 'x-index': 1 }, year: { type: 'string', 'x-index': 0 }, }, }) schema.addPatternProperty('xxx', null) schema.setAdditionalProperties({ type: 'string', }) schema.setAdditionalProperties(null) array.setItems(null) array.setAdditionalItems({ type: 'string', }) array.setAdditionalItems(null) schema.setPatternProperties(null) schema.fromJSON(null) expect(schema2['fn']).toBeUndefined() expect(schema.properties.string).not.toBeUndefined() expect(schema.patternProperties['^[a-zA-Z0-9]*$']).not.toBeUndefined() expect(schema).toMatchSnapshot() expect(schema.toJSON()).toMatchSnapshot() expect(pattern.mapProperties((schema, key) => key)).toEqual([ 'year', 'made', 'model', ]) expect( pattern.reduceProperties((buf, schema, key) => buf.concat('_' + key), []) ).toEqual(['_year', '_made', '_model']) expect(schema.mapPatternProperties((schema, key) => key)).toEqual([ '^[a-zA-Z0-9]*$', ]) expect( schema.reducePatternProperties( (buf, schema, key) => buf.concat('_' + key), [] ) ).toEqual(['_^[a-zA-Z0-9]*$']) schema5.toJSON() schema6.toJSON() schema7.toJSON() schema.removeProperty('string') expect(schema.properties.string).toBeUndefined() schema.removePatternProperty('^[a-zA-Z0-9]*$') expect(schema.patternProperties['^[a-zA-Z0-9]*$']).toBeUndefined() expect(schema.compile()).toMatchSnapshot() expect(string.compile()).toMatchSnapshot() expect(schema3.toJSON()).toMatchSnapshot() expect(schema4.toJSON()).toMatchSnapshot() }) describe('all static methods', () => { expect(Schema.compile({ aa: '{{123}}' })).toEqual({ aa: 123 }) expect(Schema.shallowCompile('{{123}}')).toEqual(123) expect(Schema.getOrderProperties()).toEqual([]) Schema.registerPatches(null) }) test('single function x-reactions', () => { const reactions = () => console.info('x-reactions') const schema = new Schema({ type: 'string', 'x-reactions': reactions, }) expect(schema.compile()['x-reactions']).toEqual(reactions) }) test('definitions and $ref', () => { const schema = new Schema({ definitions: { address: { type: 'object', properties: { street_address: { type: 'string', }, city: { type: 'string', }, state: { type: 'string', }, }, required: ['street_address', 'city', 'state'], }, }, type: 'object', properties: { billing_address: { title: 'Billing address', $ref: '#/definitions/address', }, shipping_address: { title: 'Shipping address', $ref: '#/definitions/address', }, }, }) expect(schema.properties.billing_address.required).toEqual([ 'street_address', 'city', 'state', ]) }) ================================================ FILE: packages/json-schema/src/__tests__/server-validate.spec.ts ================================================ import { createForm, Form } from '@formily/core' import { ISchema, Schema, SchemaKey } from '../' // 这是schema const schemaJson = { type: 'object', title: 'xxx配置', properties: { string: { type: 'string', title: 'string', maxLength: 5, required: true, }, number: { type: 'number', title: 'number', required: true, }, url: { type: 'string', title: 'url', format: 'url', }, arr: { type: 'array', title: 'array', maxItems: 2, required: true, items: { type: 'object', properties: { string: { type: 'string', title: 'string', required: true, }, }, }, }, }, } // 这是需要校验的数据 const schemaData = { string: '123456', // 超过5个字 // number 字段不存在 url: 'xxxxx', // 不合法的url arr: [ { string: '1', }, { string: '2', }, { // 数组超出2项 string: '', // 没有填 }, ], } function recursiveField( form: Form, schema: ISchema, basePath?: string, name?: SchemaKey ) { const fieldSchema = new Schema(schema) const fieldProps = fieldSchema.toFieldProps() function recursiveProperties(propBasePath?: string) { fieldSchema.mapProperties((propSchema, propName) => { recursiveField(form, propSchema, propBasePath, propName) }) } if (name === undefined || name === null) { recursiveProperties(basePath) return } if (schema.type === 'object') { const field = form.createObjectField({ ...fieldProps, name, basePath, }) recursiveProperties(field.address.toString()) } else if (schema.type === 'array') { const field = form.createArrayField({ ...fieldProps, name, basePath, }) const fieldAddress = field.address.toString() const fieldValues = form.getValuesIn(fieldAddress) fieldValues.forEach((value: any, index: number) => { if (schema.items) { const itemsSchema = Array.isArray(schema.items) ? schema.items[index] || schema.items[0] : schema.items recursiveField(form, itemsSchema as ISchema, fieldAddress, index) } }) } else if (schema.type === 'void') { const field = form.createVoidField({ ...fieldProps, name, basePath, }) recursiveProperties(field.address.toString()) } else { form.createField({ ...fieldProps, name, basePath, }) } } test('server validate', async () => { const form = createForm({ values: schemaData, }) recursiveField(form, schemaJson) let errors: any[] try { await form.validate() } catch (e) { errors = e } expect(errors).not.toBeUndefined() }) ================================================ FILE: packages/json-schema/src/__tests__/shared.spec.ts ================================================ import { isNoNeedCompileObject, createDataSource } from '../shared' import { observable } from '@formily/reactive' import { Schema } from '../schema' test('isNoNeedCompileObject', () => { expect(isNoNeedCompileObject({})).toBeFalsy() expect(isNoNeedCompileObject({ $$typeof: null, _owner: null })).toBeTruthy() expect(isNoNeedCompileObject({ _isAMomentObject: true })).toBeTruthy() expect( isNoNeedCompileObject({ [Symbol.for('__REVA_ACTIONS')]: true }) ).toBeTruthy() expect(isNoNeedCompileObject({ toJSON: () => {} })).toBeTruthy() expect(isNoNeedCompileObject({ toJS: () => {} })).toBeTruthy() expect(isNoNeedCompileObject(observable({}))).toBeTruthy() expect(isNoNeedCompileObject(new Schema({}))).toBeTruthy() }) test('createDataSource', () => { expect(createDataSource(['111'])).toEqual([{ label: '111', value: '111' }]) expect(createDataSource([{ label: '111', value: '111' }])).toEqual([ { label: '111', value: '111' }, ]) }) ================================================ FILE: packages/json-schema/src/__tests__/transformer.spec.ts ================================================ import { Schema } from '../schema' import { createForm } from '@formily/core' import { isObservable } from '@formily/reactive' import { ISchema, ISchemaTransformerOptions } from '../types' const attach = void }>(target: T): T => { target.onMount() return target } const getFormAndFields = ( field1SchemaProps: Omit = {}, field2SchemaProps: Omit = {}, options: ISchemaTransformerOptions = {} ) => { const filed1Schema = new Schema({ name: 'field1', ...field1SchemaProps, }).toFieldProps(options) const filed2Schema = new Schema({ name: 'field2', ...field2SchemaProps, }).toFieldProps(options) const form = createForm() const field1 = form.createField(filed1Schema) const field2 = form.createField(filed2Schema) return { form, field1, field2, } } test('baseReaction', () => { const { field1, field2 } = getFormAndFields( { title: 'field1Title', }, { title: 'field2Title', } ) expect(field1.title).toBe('field1Title') expect(field2.title).toBe('field2Title') }) test('baseReaction with scopes', () => { const scopeTitle = 'fieldTitle' const scopeDescription = 'fieldDescription' const { field1, field2 } = getFormAndFields( { title: '{{scopeTitle}}', }, { description: '{{scopeDescription}}', }, { scope: { scopeTitle, scopeDescription, }, } ) expect(field1.title).toBe(scopeTitle) expect(field2.description).toBe(scopeDescription) }) test('userReactions with target(state)', () => { const field2Title = 'field2Title' const { field2 } = getFormAndFields({ 'x-reactions': { target: 'field2', fulfill: { state: { title: field2Title, }, }, }, }) expect(field2.title).toBe(field2Title) }) test('userReactions with target(schema)', () => { const field2Data = 'fieldData' const { field2 } = getFormAndFields({ 'x-reactions': { target: 'field2', fulfill: { schema: { 'x-data': field2Data, }, }, }, }) expect(field2.data).toBe(field2Data) }) test('userReactions with target(runner)', () => { const mockFn = jest.fn() const field2Title = 'field2Title' const { field2 } = getFormAndFields( { 'x-reactions': { target: 'field2', fulfill: { run: `$target.title='${field2Title}';fn()`, }, }, }, {}, { scope: { fn: mockFn, }, } ) expect(mockFn).toBeCalledTimes(1) expect(field2.title).toBe(field2Title) }) test('userReactions without target(state)', () => { const field1Title = 'field1Title' const { field1 } = getFormAndFields({ 'x-reactions': { fulfill: { state: { title: field1Title, }, }, }, }) expect(field1.title).toBe(field1Title) }) test('userReactions without target(schema)', () => { const field1Data = 'fieldData' const { field1 } = getFormAndFields({ 'x-reactions': { fulfill: { schema: { 'x-data': field1Data, }, }, }, }) expect(field1.data).toBe(field1Data) }) test('userReactions without target(runner)', () => { const mockFn = jest.fn() const { field1 } = getFormAndFields( { 'x-reactions': { fulfill: { run: `$self.__target__=$target;fn()`, }, }, }, {}, { scope: { fn: mockFn, }, } ) expect(mockFn).toBeCalledTimes(1) expect((field1 as any).__target__).toBe(null) }) test('userReactions with condition', () => { const mockFn = jest.fn() const { field1 } = getFormAndFields( { 'x-value': true, 'x-reactions': { when: '$self.value===true', fulfill: { run: 'mockFn($self.value)', }, otherwise: { run: 'mockFn($self.value)', }, }, }, {}, { scope: { mockFn, }, } ) expect(mockFn).nthCalledWith(1, true) field1.value = false expect(mockFn).nthCalledWith(2, false) }) test('userReactions with condition(wrong type)', () => { const field1Value = 'field1Value' const mockFn = jest.fn() getFormAndFields( { 'x-value': field1Value, 'x-reactions': { dependencies: 'value', fulfill: { run: 'mockFn($deps, $dependencies)', }, }, }, {}, { scope: { mockFn, }, } ) expect(mockFn).nthCalledWith(1, [], []) }) test('userReactions with condition(array)', () => { const field1Value = 'field1Value' const field2Value = 'field2Value' const field1Title = 'field1Title' const field1Description = 'field1Description' const mockFn = jest.fn() getFormAndFields( { title: field1Title, description: field1Description, 'x-value': field1Value, }, { 'x-value': field2Value, 'x-reactions': { dependencies: [ 'field2', { name: 1, source: 'field1', }, { name: 2, source: 'field1#title', }, { name: 3, source: 'field1', property: 'description', }, ], fulfill: { run: `mockFn($deps)`, }, }, }, { scope: { mockFn, }, } ) expect(mockFn).nthCalledWith(1, [ field2Value, field1Value, field1Title, field1Description, ]) }) test('userReactions with condition(object)', () => { const field2Value = 'field2Value' const field1Title = 'field1Title' const mockFn = jest.fn() getFormAndFields( { title: field1Title, }, { 'x-value': field2Value, 'x-reactions': { dependencies: { key1: 'field1#title', key2: 'field2', }, fulfill: { run: `mockFn($deps)`, }, }, }, { scope: { mockFn, }, } ) expect(mockFn).nthCalledWith(1, { key1: field1Title, key2: field2Value, }) }) test('userReactions with user-defined effects', () => { const field2Value = 'field2Value' const field1Title = 'field1Title' const mockFn = jest.fn() const { field2 } = getFormAndFields( { title: field1Title, 'x-reactions': { target: 'field2', fulfill: { run: `mockFn($target.value)`, }, effects: ['onFieldInit'], }, }, { 'x-value': field2Value, }, { scope: { mockFn, }, } ) expect(mockFn).toBeCalledTimes(1) expect(mockFn).nthCalledWith(1, field2Value) field2.value = field1Title expect(mockFn).toBeCalledTimes(1) }) test('userReactions with function type', () => { const componentProps = { prop: 1, } let observable: any = {} const { field1 } = getFormAndFields({ 'x-reactions': (field, baseScope) => { baseScope.$props(componentProps) observable = baseScope.$observable({}) }, }) expect(field1.componentProps).toMatchObject(componentProps) expect(isObservable(observable)).toBe(true) }) test('userReactions with $lookup $record $records $index', () => { const initialValues = { array: [ { a: 1, b: 2 }, { a: 3, b: 4 }, ], } const form = attach( createForm({ initialValues, }) ) form.createArrayField({ name: 'array', }) form.createObjectField({ name: '0', basePath: 'array', }) form.createObjectField({ name: '1', basePath: 'array', }) const field0aSchema = new Schema({ name: 'array.0.a', 'x-reactions': `{{$self.title = $record.b}}`, }).toFieldProps({}) const field0bSchema = new Schema({ name: 'array.0.b', 'x-reactions': '{{$self.title = $lookup.array[0].a}}', }).toFieldProps({}) const field1aSchema = new Schema({ name: 'array.1.a', 'x-reactions': '{{$self.title = $records[$index].b}}', }).toFieldProps({}) const field1bSchema = new Schema({ name: 'array.1.b', 'x-reactions': `{{$self.title = $record.$lookup.array[$record.$index].a}}`, }).toFieldProps({}) const field0a = attach(form.createField(field0aSchema)) const field0b = attach(form.createField(field0bSchema)) const field1a = attach(form.createField(field1aSchema)) const field1b = attach(form.createField(field1bSchema)) expect(field0a.title).toEqual(2) expect(field0b.title).toEqual(1) expect(field1a.title).toEqual(4) expect(field1b.title).toEqual(3) }) test('userReactions with primary type record', () => { const initialValues = { array: [1, 2, 3], } const form = attach( createForm({ initialValues, }) ) const field0Schema = new Schema({ name: 'array.0', 'x-reactions': `{{$self.title = $record}}`, }).toFieldProps({}) const field1Schema = new Schema({ name: 'array.1', 'x-reactions': '{{$self.title = $record}}', }).toFieldProps({}) form.createArrayField({ name: 'array', }) const field0 = attach(form.createField(field0Schema)) const field1 = attach(form.createField(field1Schema)) expect(field0.title).toEqual(1) expect(field1.title).toEqual(2) }) ================================================ FILE: packages/json-schema/src/__tests__/traverse.spec.ts ================================================ import { traverse, traverseSchema } from '../shared' import { FormPath } from '@formily/shared' test('traverseSchema', () => { const visited = [] const omitted = [] traverseSchema( { type: 'string', title: '{{aa}}', required: true, 'x-validator': 'phone', 'x-compile-omitted': ['title'], default: { input: 123, }, }, (value, path, omitCompile) => { if (omitCompile) { omitted.push(value) } else { visited.push(path) } } ) expect(visited).toEqual([ ['x-validator'], ['type'], ['required'], ['default'], ]) expect(omitted).toEqual(['{{aa}}']) }) test('traverse circular reference', () => { // eslint-disable-next-line var a = { dd: { mm: null, }, bb: { cc: { dd: 123, }, }, kk: { toJS() {}, }, } a.dd.mm = a traverse(a, () => {}) traverseSchema(a as any, () => {}) }) test('traverse none circular reference', () => { // eslint-disable-next-line var dd = { mm: null, } let a = { dd, bb: { dd, }, } const paths = [] traverse(a, (value, path) => { paths.push(path) }) traverseSchema(a, () => {}) expect( paths.some((path) => FormPath.parse(path).includes('dd.mm')) ).toBeTruthy() expect( paths.some((path) => FormPath.parse(path).includes('bb.dd.mm')) ).toBeTruthy() }) ================================================ FILE: packages/json-schema/src/compiler.ts ================================================ import { isArr, isFn, isPlainObj, isStr, reduce, FormPath, } from '@formily/shared' import { IGeneralFieldState } from '@formily/core' import { untracked, hasCollected } from '@formily/reactive' import { traverse, traverseSchema, isNoNeedCompileObject, hasOwnProperty, patchStateFormSchema, } from './shared' import { ISchema } from './types' const ExpRE = /^\s*\{\{([\s\S]*)\}\}\s*$/ const Registry = { silent: false, compile(expression: string, scope = {}) { if (Registry.silent) { try { return new Function('$root', `with($root) { return (${expression}); }`)( scope ) } catch {} } else { return new Function('$root', `with($root) { return (${expression}); }`)( scope ) } }, } export const silent = (value = true) => { Registry.silent = !!value } export const registerCompiler = ( compiler: (expression: string, scope: any) => any ) => { if (isFn(compiler)) { Registry.compile = compiler } } export const shallowCompile = ( source: Source, scope?: Scope ) => { if (isStr(source)) { const matched = source.match(ExpRE) if (!matched) return source return Registry.compile(matched[1], scope) } return source } export const compile = ( source: Source, scope?: Scope ): any => { const seenObjects = [] const compile = (source: any) => { if (isStr(source)) { return shallowCompile(source, scope) } else if (isArr(source)) { return source.map((value: any) => compile(value)) } else if (isPlainObj(source)) { if (isNoNeedCompileObject(source)) return source const seenIndex = seenObjects.indexOf(source) if (seenIndex > -1) { return source } const addIndex = seenObjects.length seenObjects.push(source) const results = reduce( source, (buf, value, key) => { buf[key] = compile(value) return buf }, {} ) seenObjects.splice(addIndex, 1) return results } return source } return compile(source) } export const patchCompile = ( targetState: IGeneralFieldState, sourceState: any, scope: any ) => { traverse(sourceState, (value, pattern) => { const compiled = compile(value, scope) if (compiled === undefined) return const path = FormPath.parse(pattern) const key = path.segments[0] if (hasOwnProperty.call(targetState, key)) { untracked(() => FormPath.setIn(targetState, path, compiled)) } }) } export const patchSchemaCompile = ( targetState: IGeneralFieldState, sourceSchema: ISchema, scope: any, demand = false ) => { traverseSchema(sourceSchema, (value, path, omitCompile) => { let compiled = value let collected = hasCollected(() => { if (!omitCompile) { compiled = compile(value, scope) } }) if (compiled === undefined) return if (demand) { if (collected || !targetState.initialized) { patchStateFormSchema(targetState, path, compiled) } } else { patchStateFormSchema(targetState, path, compiled) } }) } ================================================ FILE: packages/json-schema/src/global.d.ts ================================================ /// import * as Types from './types' declare global { namespace Formily.Schema { export { Types } } } ================================================ FILE: packages/json-schema/src/index.ts ================================================ export * from './schema' export * from './types' ================================================ FILE: packages/json-schema/src/patches.ts ================================================ import { isFn, isArr } from '@formily/shared' import { SchemaPatch } from './types' const patches: SchemaPatch[] = [] const polyfills: Record = {} export const reducePatches = (schema: any) => { return patches.reduce( (buf, patch) => { return patch(buf) }, { ...schema } ) } export const registerPatches = (...args: SchemaPatch[]) => { args.forEach((patch) => { if (isFn(patch)) { patches.push(patch) } }) } export const registerPolyfills = (version: string, patch: SchemaPatch) => { if (version && isFn(patch)) { polyfills[version] = polyfills[version] || [] polyfills[version].push(patch) } } export const enablePolyfills = (versions?: string[]) => { if (isArr(versions)) { versions.forEach((version) => { if (isArr(polyfills[version])) { polyfills[version].forEach((patch) => { registerPatches(patch) }) } }) } } ================================================ FILE: packages/json-schema/src/polyfills/SPECIFICATION_1_0.ts ================================================ import { registerPolyfills } from '../patches' import { toArr, isArr, isStr, lowerCase, isValid } from '@formily/shared' import { ISchema } from '../types' const VOID_COMPONENTS = [ 'card', 'block', 'grid-col', 'grid-row', 'grid', 'layout', 'step', 'tab', 'text-box', ] const TYPE_DEFAULT_COMPONENTS = {} const transformCondition = (condition: string) => { if (isStr(condition)) { return condition.replace(/\$value/, '$self.value') } } const transformXLinkage = (linkages: any[]) => { if (isArr(linkages)) { return linkages.reduce((buf, item) => { if (!item) return buf if (item.type === 'value:visible') { return buf.concat({ target: item.target, when: transformCondition(item.condition), fulfill: { state: { visible: true, }, }, otherwise: { state: { visible: false, }, }, }) } else if (item.type === 'value:schema') { return buf.concat({ target: item.target, when: transformCondition(item.condition), fulfill: { schema: SpecificationV1Polyfill({ version: '1.0', ...item.schema }), }, otherwise: { schema: SpecificationV1Polyfill({ version: '1.0', ...item.otherwise, }), }, }) } else if (item.type === 'value:state') { return buf.concat({ target: item.target, when: transformCondition(item.condition), fulfill: { state: item.state, }, otherwise: { state: item.otherwise, }, }) } }, []) } return [] } const SpecificationV1Polyfill = (schema: ISchema) => { if (isValid(schema['editable'])) { schema['x-editable'] = schema['x-editable'] || schema['editable'] delete schema['editable'] } if (isValid(schema['visible'])) { schema['x-visible'] = schema['x-visible'] || schema['visible'] delete schema['visible'] } if (isValid(schema['display'])) { schema['x-display'] = schema['x-display'] || (schema['display'] ? 'visible' : 'hidden') delete schema['display'] } if (isValid(schema['x-props'])) { schema['x-decorator-props'] = schema['x-decorator-props'] || schema['x-props'] delete schema['display'] } if (schema['x-linkages']) { schema['x-reactions'] = toArr(schema['x-reactions']).concat( transformXLinkage(schema['x-linkages']) ) delete schema['x-linkages'] } if (schema['x-component']) { if ( VOID_COMPONENTS.some( (component) => lowerCase(component) === lowerCase(schema['x-component']) ) ) { schema['type'] = 'void' } } else { if (TYPE_DEFAULT_COMPONENTS[schema['type']]) { schema['x-component'] = TYPE_DEFAULT_COMPONENTS[schema['type']] } } if ( !schema['x-decorator'] && schema['type'] !== 'void' && schema['type'] !== 'object' ) { schema['x-decorator'] = schema['x-decorator'] || 'FormItem' } if (schema['x-rules']) { schema['x-validator'] = [] .concat(schema['x-validator'] || []) .concat(schema['x-rules']) } return schema } registerPolyfills('1.0', SpecificationV1Polyfill) export const registerVoidComponents = (components: string[]) => { VOID_COMPONENTS.push(...components) } export const registerTypeDefaultComponents = (maps: Record) => { Object.assign(TYPE_DEFAULT_COMPONENTS, maps) } ================================================ FILE: packages/json-schema/src/polyfills/index.ts ================================================ export * from './SPECIFICATION_1_0' ================================================ FILE: packages/json-schema/src/schema.ts ================================================ import { ISchema, SchemaEnum, SchemaProperties, SchemaReaction, SchemaTypes, SchemaKey, ISchemaTransformerOptions, Slot, } from './types' import { IFieldFactoryProps } from '@formily/core' import { map, each, isFn, instOf, FormPath, isStr } from '@formily/shared' import { compile, silent, shallowCompile, registerCompiler } from './compiler' import { transformFieldProps } from './transformer' import { reducePatches, registerPatches, registerPolyfills, enablePolyfills, } from './patches' import { registerVoidComponents, registerTypeDefaultComponents, } from './polyfills' import { SchemaNestedMap } from './shared' export class Schema< Decorator = any, Component = any, DecoratorProps = any, ComponentProps = any, Pattern = any, Display = any, Validator = any, Message = any, ReactionField = any > implements ISchema { parent?: Schema root?: Schema name?: SchemaKey title?: Message description?: Message default?: any readOnly?: boolean writeOnly?: boolean type?: SchemaTypes enum?: SchemaEnum const?: any multipleOf?: number maximum?: number exclusiveMaximum?: number minimum?: number exclusiveMinimum?: number maxLength?: number minLength?: number pattern?: string | RegExp maxItems?: number minItems?: number uniqueItems?: boolean maxProperties?: number minProperties?: number required?: string[] | boolean | string format?: string /** nested json schema spec **/ definitions?: Record< string, Schema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > > properties?: Record< string, Schema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > > items?: | Schema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > | Schema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message >[] additionalItems?: Schema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > patternProperties?: Record< string, Schema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > > additionalProperties?: Schema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message >; //顺序描述 ['x-index']?: number; //交互模式 ['x-pattern']?: Pattern; //展示状态 ['x-display']?: Display; //校验器 ['x-validator']?: Validator; //装饰器 ['x-decorator']?: Decorator; //装饰器属性 ['x-decorator-props']?: DecoratorProps; //组件 ['x-component']?: Component; //组件属性 ['x-component-props']?: ComponentProps; ['x-reactions']?: SchemaReaction[]; ['x-content']?: any; ['x-data']?: any; ['x-visible']?: boolean; ['x-hidden']?: boolean; ['x-disabled']?: boolean; ['x-editable']?: boolean; ['x-read-only']?: boolean; ['x-read-pretty']?: boolean; ['x-compile-omitted']?: string[]; ['x-slot-node']?: Slot; [key: `x-${string | number}` | symbol]: any _isJSONSchemaObject = true version = '2.0' constructor( json: ISchema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message >, parent?: Schema ) { if (parent) { this.parent = parent this.root = parent.root } else { this.root = this } return this.fromJSON(json) } addProperty = ( key: SchemaKey, schema: ISchema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > ) => { this.properties = this.properties || {} this.properties[key] = new Schema(schema, this) this.properties[key].name = key return this.properties[key] } removeProperty = (key: SchemaKey) => { const schema = this.properties[key] delete this.properties[key] return schema } setProperties = ( properties: SchemaProperties< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > ) => { for (const key in properties) { this.addProperty(key, properties[key]) } return this } addPatternProperty = ( key: SchemaKey, schema: ISchema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > ) => { if (!schema) return this.patternProperties = this.patternProperties || {} this.patternProperties[key] = new Schema(schema, this) this.patternProperties[key].name = key return this.patternProperties[key] } removePatternProperty = (key: SchemaKey) => { const schema = this.patternProperties[key] delete this.patternProperties[key] return schema } setPatternProperties = ( properties: SchemaProperties< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > ) => { if (!properties) return this for (const key in properties) { this.addPatternProperty(key, properties[key]) } return this } setAdditionalProperties = ( properties: ISchema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > ) => { if (!properties) return this.additionalProperties = new Schema(properties) return this.additionalProperties } setItems = ( schema: | ISchema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > | ISchema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message >[] ) => { if (!schema) return if (Array.isArray(schema)) { this.items = schema.map((item) => new Schema(item, this)) } else { this.items = new Schema(schema, this) } return this.items } setAdditionalItems = ( items: ISchema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > ) => { if (!items) return this.additionalItems = new Schema(items, this) return this.additionalItems } findDefinitions = (ref: string) => { if (!ref || !this.root || !isStr(ref)) return if (ref.indexOf('#/') !== 0) return return FormPath.getIn(this.root, ref.substring(2).split('/')) } mapProperties = ( callback?: ( schema: Schema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message >, key: SchemaKey, index: number ) => T ): T[] => { return Schema.getOrderProperties(this).map(({ schema, key }, index) => { return callback(schema, key, index) }) } mapPatternProperties = ( callback?: ( schema: Schema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message >, key: SchemaKey, index: number ) => T ): T[] => { return Schema.getOrderProperties(this, 'patternProperties').map( ({ schema, key }, index) => { return callback(schema, key, index) } ) } reduceProperties = ( callback?: ( buffer: P, schema: Schema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message >, key: SchemaKey, index: number ) => R, predicate?: P ): R => { let results: any = predicate Schema.getOrderProperties(this, 'properties').forEach( ({ schema, key }, index) => { results = callback(results, schema, key, index) } ) return results } reducePatternProperties = ( callback?: ( buffer: P, schema: Schema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message >, key: SchemaKey, index: number ) => R, predicate?: P ): R => { let results: any = predicate Schema.getOrderProperties(this, 'patternProperties').forEach( ({ schema, key }, index) => { results = callback(results, schema, key, index) } ) return results } compile = (scope?: any) => { const schema = new Schema({}, this.parent) each(this, (value, key) => { if (isFn(value) && !key.includes('x-')) return if (key === 'parent' || key === 'root') return if (!SchemaNestedMap[key]) { schema[key] = value ? compile(value, scope) : value } else { schema[key] = value ? shallowCompile(value, scope) : value } }) return schema } fromJSON = ( json: ISchema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > ) => { if (!json) return this if (Schema.isSchemaInstance(json)) return json each(reducePatches(json), (value, key) => { if (isFn(value) && !key.includes('x-')) return if (key === 'properties') { this.setProperties(value) } else if (key === 'patternProperties') { this.setPatternProperties(value) } else if (key === 'additionalProperties') { this.setAdditionalProperties(value) } else if (key === 'items') { this.setItems(value) } else if (key === 'additionalItems') { this.setAdditionalItems(value) } else if (key === '$ref') { this.fromJSON(this.findDefinitions(value)) } else { this[key] = value } }) return this } toJSON = ( recursion = true ): ISchema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > => { const results = {} each(this, (value: any, key) => { if ( (isFn(value) && !key.includes('x-')) || key === 'parent' || key === 'root' ) return if (key === 'properties' || key === 'patternProperties') { if (!recursion) return results[key] = map(value, (item) => item?.toJSON?.()) } else if (key === 'additionalProperties' || key === 'additionalItems') { if (!recursion) return results[key] = value?.toJSON?.() } else if (key === 'items') { if (!recursion) return if (Array.isArray(value)) { results[key] = value.map((item) => item?.toJSON?.()) } else { results[key] = value?.toJSON?.() } } else { results[key] = value } }) return results } toFieldProps = ( options?: ISchemaTransformerOptions ): IFieldFactoryProps => { return transformFieldProps(this, options) } static getOrderProperties = ( schema: ISchema = {}, propertiesName: keyof ISchema = 'properties' ) => { const orderProperties = [] const unorderProperties = [] for (const key in schema[propertiesName]) { const item = schema[propertiesName][key] const index = item['x-index'] if (!isNaN(index)) { orderProperties[index] = { schema: item, key } } else { unorderProperties.push({ schema: item, key }) } } return orderProperties.concat(unorderProperties).filter((item) => !!item) } static compile = (expression: any, scope?: any) => { return compile(expression, scope) } static shallowCompile = (expression: any, scope?: any) => { return shallowCompile(expression, scope) } static isSchemaInstance = (value: any): value is Schema => { return instOf(value, Schema) } static registerCompiler = registerCompiler static registerPatches = registerPatches static registerVoidComponents = registerVoidComponents static registerTypeDefaultComponents = registerTypeDefaultComponents static registerPolyfills = registerPolyfills static enablePolyfills = enablePolyfills static silent = silent } ================================================ FILE: packages/json-schema/src/shared.ts ================================================ import { isFn, each, isPlainObj, isArr, toArr, FormPath } from '@formily/shared' import { isObservable, untracked } from '@formily/reactive' import { Schema } from './schema' import { ISchema } from './types' const REVA_ACTIONS_KEY = Symbol.for('__REVA_ACTIONS') export const SchemaNestedMap = { parent: true, root: true, properties: true, patternProperties: true, additionalProperties: true, items: true, additionalItems: true, 'x-linkages': true, 'x-reactions': true, } export const SchemaStateMap = { title: 'title', description: 'description', default: 'initialValue', enum: 'dataSource', readOnly: 'readOnly', writeOnly: 'editable', 'x-content': 'content', 'x-data': 'data', 'x-value': 'value', 'x-editable': 'editable', 'x-disabled': 'disabled', 'x-read-pretty': 'readPretty', 'x-read-only': 'readOnly', 'x-visible': 'visible', 'x-hidden': 'hidden', 'x-display': 'display', 'x-pattern': 'pattern', 'x-validator': 'validator', 'x-decorator': 'decoratorType', 'x-component': 'componentType', 'x-decorator-props': 'decoratorProps', 'x-component-props': 'componentProps', } export const SchemaValidatorMap = { required: true, format: true, maxItems: true, minItems: true, maxLength: true, minLength: true, maximum: true, minimum: true, exclusiveMaximum: true, exclusiveMinimum: true, pattern: true, const: true, multipleOf: true, maxProperties: true, minProperties: true, uniqueItems: true, } export const SchemaNormalKeys = Object.keys(SchemaStateMap) export const SchemaValidatorKeys = Object.keys(SchemaValidatorMap) export const hasOwnProperty = Object.prototype.hasOwnProperty export const traverse = ( target: any, visitor: (value: any, path: Array) => void ) => { const seenObjects = [] const root = target const traverse = (target: any, path = []) => { if (isPlainObj(target)) { const seenIndex = seenObjects.indexOf(target) if (seenIndex > -1) { return } const addIndex = seenObjects.length seenObjects.push(target) if (isNoNeedCompileObject(target) && root !== target) { visitor(target, path) return } each(target, (value, key) => { traverse(value, path.concat(key)) }) seenObjects.splice(addIndex, 1) } else { visitor(target, path) } } traverse(target) } export const traverseSchema = ( schema: ISchema, visitor: (value: any, path: any[], omitCompile?: boolean) => void ) => { if (schema['x-validator'] !== undefined) { visitor( schema['x-validator'], ['x-validator'], schema['x-compile-omitted']?.includes('x-validator') ) } const seenObjects = [] const root = schema const traverse = (target: any, path = []) => { if ( path[0] === 'x-compile-omitted' || path[0] === 'x-validator' || path[0] === 'version' || path[0] === '_isJSONSchemaObject' ) return if (String(path[0]).indexOf('x-') == -1 && isFn(target)) return if (SchemaNestedMap[path[0]]) return if (schema['x-compile-omitted']?.indexOf(path[0]) > -1) { visitor(target, path, true) return } if (isPlainObj(target)) { if (path[0] === 'default' || path[0] === 'x-value') { visitor(target, path) return } const seenIndex = seenObjects.indexOf(target) if (seenIndex > -1) { return } const addIndex = seenObjects.length seenObjects.push(target) if (isNoNeedCompileObject(target) && root !== target) { visitor(target, path) return } each(target, (value, key) => { traverse(value, path.concat(key)) }) seenObjects.splice(addIndex, 1) } else { visitor(target, path) } } traverse(schema) } export const isNoNeedCompileObject = (source: any) => { if ('$$typeof' in source && '_owner' in source) { return true } if (source['_isAMomentObject']) { return true } if (Schema.isSchemaInstance(source)) { return true } if (source[REVA_ACTIONS_KEY]) { return true } if (isFn(source['toJS'])) { return true } if (isFn(source['toJSON'])) { return true } if (isObservable(source)) { return true } return false } export const createDataSource = (source: any[]) => { return toArr(source).map((item) => { if (typeof item === 'object') { return item } else { return { label: item, value: item, } } }) } export const patchStateFormSchema = ( targetState: any, pattern: any[], compiled: any ) => { untracked(() => { const path = FormPath.parse(pattern) const segments = path.segments const key = segments[0] const isEnum = key === 'enum' && isArr(compiled) const schemaMapKey = SchemaStateMap[key] if (schemaMapKey) { FormPath.setIn( targetState, [schemaMapKey].concat(segments.slice(1)), isEnum ? createDataSource(compiled) : compiled ) } else { const isValidatorKey = SchemaValidatorMap[key] if (isValidatorKey) { targetState['setValidatorRule']?.(key, compiled) } } }) } ================================================ FILE: packages/json-schema/src/transformer.ts ================================================ import { untracked, autorun, observable } from '@formily/reactive' import { isArr, isStr, toArr, each, isFn, isPlainObj, reduce, lazyMerge, } from '@formily/shared' import { Schema } from './schema' import { ISchema, ISchemaTransformerOptions, IFieldStateSetterOptions, SchemaReaction, } from './types' import { onFieldInit, onFieldMount, onFieldUnmount, onFieldValueChange, onFieldInputValueChange, onFieldInitialValueChange, onFieldValidateStart, onFieldValidateEnd, onFieldValidateFailed, onFieldValidateSuccess, IFieldFactoryProps, Field, } from '@formily/core' import { patchCompile, patchSchemaCompile, shallowCompile } from './compiler' const FieldEffects = { onFieldInit, onFieldMount, onFieldUnmount, onFieldValueChange, onFieldInputValueChange, onFieldInitialValueChange, onFieldValidateStart, onFieldValidateEnd, onFieldValidateFailed, onFieldValidateSuccess, } const DefaultFieldEffects = ['onFieldInit', 'onFieldValueChange'] const getDependencyValue = ( field: Field, pattern: string, property?: string ) => { const [target, path] = String(pattern).split(/\s*#\s*/) return field.query(target).getIn(path || property || 'value') } const getDependencies = ( field: Field, dependencies: | Array | object ) => { if (isArr(dependencies)) { const results = [] dependencies.forEach((pattern) => { if (isStr(pattern)) { results.push(getDependencyValue(field, pattern)) } else if (isPlainObj(pattern)) { if (pattern.name && pattern.source) { results[pattern.name] = getDependencyValue( field, pattern.source, pattern.property ) } } }) return results } else if (isPlainObj(dependencies)) { return reduce( dependencies, (buf, pattern, key) => { buf[key] = getDependencyValue(field, pattern) return buf }, {} ) } return [] } const setSchemaFieldState = ( options: IFieldStateSetterOptions, demand = false ) => { const { request, target, runner, field, scope } = options || {} if (!request) return if (target) { if (request.state) { field.form.setFieldState(target, (state) => patchCompile( state, request.state, lazyMerge(scope, { $target: state, }) ) ) } if (request.schema) { field.form.setFieldState(target, (state) => patchSchemaCompile( state, request.schema, lazyMerge(scope, { $target: state, }), demand ) ) } if (isStr(runner) && runner) { field.form.setFieldState(target, (state) => { shallowCompile( `{{function(){${runner}}}}`, lazyMerge(scope, { $target: state, }) )() }) } } else { if (request.state) { field.setState((state) => patchCompile(state, request.state, scope)) } if (request.schema) { field.setState((state) => patchSchemaCompile(state, request.schema, scope, demand) ) } if (isStr(runner) && runner) { shallowCompile(`{{function(){${runner}}}}`, scope)() } } } const getBaseScope = ( field: Field, options: ISchemaTransformerOptions = {} ) => { const $observable = (target: any, deps?: any[]) => autorun.memo(() => observable(target), deps) const $props = (props: any) => field.setComponentProps(props) const $effect = autorun.effect const $memo = autorun.memo const $self = field const $form = field.form const $values = field.form.values return lazyMerge( { get $lookup() { return options?.scope?.$record ?? $values }, get $records() { return field.records }, get $record() { const record = field.record if (typeof record === 'object') { return lazyMerge(record, { get $lookup() { return options?.scope?.$record ?? $values }, get $index() { return field.index }, }) } return record }, get $index() { return field.index }, }, options.scope, { $form, $self, $observable, $effect, $memo, $props, $values, } ) } const getBaseReactions = (schema: ISchema, options: ISchemaTransformerOptions) => (field: Field) => { setSchemaFieldState( { field, request: { schema }, scope: getBaseScope(field, options), }, true ) } const getUserReactions = ( schema: ISchema, options: ISchemaTransformerOptions ) => { const reactions: SchemaReaction[] = toArr(schema['x-reactions']) return reactions.map((unCompiled) => { return (field: Field) => { const baseScope = getBaseScope(field, options) const reaction = shallowCompile(unCompiled, baseScope) if (!reaction) return if (isFn(reaction)) { return reaction(field, baseScope) } const { when, fulfill, otherwise, target, effects } = reaction const run = () => { const $deps = getDependencies(field, reaction.dependencies) const $dependencies = $deps const scope = lazyMerge(baseScope, { $target: null, $deps, $dependencies, }) const compiledWhen = shallowCompile(when, scope) const condition = when ? compiledWhen : true const request = condition ? fulfill : otherwise const runner = request?.run setSchemaFieldState({ field, target, request, runner, scope, }) } if (target) { reaction.effects = effects?.length ? effects : DefaultFieldEffects } if (reaction.effects) { autorun.memo(() => { untracked(() => { each(reaction.effects, (type) => { if (FieldEffects[type]) { FieldEffects[type](field.address, run) } }) }) }, []) } else { run() } } }) } export const transformFieldProps = ( schema: Schema, options: ISchemaTransformerOptions ): IFieldFactoryProps => { return { name: schema.name, reactions: [getBaseReactions(schema, options)].concat( getUserReactions(schema, options) ), } } ================================================ FILE: packages/json-schema/src/types.ts ================================================ import { IGeneralFieldState, GeneralField, FormPathPattern, } from '@formily/core' export type SchemaEnum = Array< | string | number | boolean | { label?: Message; value?: any; [key: string]: any } | { key?: any; title?: Message; [key: string]: any } > export type SchemaTypes = | 'string' | 'object' | 'array' | 'number' | 'boolean' | 'void' | 'date' | 'datetime' | (string & {}) export type SchemaProperties< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > = Record< string, ISchema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > > export type SchemaPatch = (schema: ISchema) => ISchema export type SchemaKey = string | number export type SchemaEffectTypes = | 'onFieldInit' | 'onFieldMount' | 'onFieldUnmount' | 'onFieldValueChange' | 'onFieldInputValueChange' | 'onFieldInitialValueChange' | 'onFieldValidateStart' | 'onFieldValidateEnd' | 'onFieldValidateFailed' | 'onFieldValidateSuccess' export type SchemaReaction = | { dependencies?: | Array< | string | { name?: string type?: string source?: string property?: string } > | Record when?: string | boolean target?: string effects?: (SchemaEffectTypes | (string & {}))[] fulfill?: { state?: Stringify schema?: ISchema run?: string } otherwise?: { state?: Stringify schema?: ISchema run?: string } [key: string]: any } | ((field: Field, scope: IScopeContext) => void) export type SchemaReactions = | SchemaReaction | SchemaReaction[] export type SchemaItems< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > = | ISchema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > | ISchema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message >[] export type SchemaComponents = Record export interface ISchemaFieldUpdateRequest { state?: Stringify schema?: ISchema run?: string } export interface IScopeContext { [key: string]: any } export interface IFieldStateSetterOptions { field: GeneralField target?: FormPathPattern request: ISchemaFieldUpdateRequest runner?: string scope?: IScopeContext } export interface ISchemaTransformerOptions { scope?: IScopeContext } export type Slot = { target: string isRenderProp?: boolean } export type Stringify

= { /** * Use `string & {}` instead of string to keep Literal Type for ISchema#component and ISchema#decorator */ [key in keyof P]?: P[key] | (string & {}) } export type ISchema< Decorator = any, Component = any, DecoratorProps = any, ComponentProps = any, Pattern = any, Display = any, Validator = any, Message = any, ReactionField = any > = Stringify<{ version?: string name?: SchemaKey title?: Message description?: Message default?: any readOnly?: boolean writeOnly?: boolean type?: SchemaTypes enum?: SchemaEnum const?: any multipleOf?: number maximum?: number exclusiveMaximum?: number minimum?: number exclusiveMinimum?: number maxLength?: number minLength?: number pattern?: string | RegExp maxItems?: number minItems?: number uniqueItems?: boolean maxProperties?: number minProperties?: number required?: string[] | boolean | string format?: string $ref?: string $namespace?: string /** nested json schema spec **/ definitions?: SchemaProperties< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > properties?: SchemaProperties< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > items?: SchemaItems< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > additionalItems?: ISchema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > patternProperties?: SchemaProperties< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > additionalProperties?: ISchema< Decorator, Component, DecoratorProps, ComponentProps, Pattern, Display, Validator, Message > ['x-value']?: any //顺序描述 ['x-index']?: number //交互模式 ['x-pattern']?: Pattern //展示状态 ['x-display']?: Display //校验器 ['x-validator']?: Validator //装饰器 ['x-decorator']?: Decorator | (string & {}) | ((...args: any[]) => any) //装饰器属性 ['x-decorator-props']?: DecoratorProps //组件 ['x-component']?: Component | (string & {}) | ((...args: any[]) => any) //组件属性 ['x-component-props']?: ComponentProps //组件响应器 ['x-reactions']?: SchemaReactions //内容 ['x-content']?: any ['x-data']?: any ['x-visible']?: boolean ['x-hidden']?: boolean ['x-disabled']?: boolean ['x-editable']?: boolean ['x-read-only']?: boolean ['x-read-pretty']?: boolean ['x-compile-omitted']?: string[] [key: `x-${string | number}` | symbol]: any }> ================================================ FILE: packages/json-schema/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./lib", "paths": { "@formily/*": ["packages/*", "devtools/*"] }, "declaration": true } } ================================================ FILE: packages/json-schema/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["./src/**/*.ts", "./src/**/*.tsx"], "exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"] } ================================================ FILE: packages/next/.npmignore ================================================ node_modules *.log build docs doc-site __tests__ .eslintrc jest.config.js tsconfig.json .umi src ================================================ FILE: packages/next/.umirc.js ================================================ import { resolve } from 'path' export default { mode: 'site', logo: '//img.alicdn.com/imgextra/i2/O1CN01Kq3OHU1fph6LGqjIz_!!6000000004056-55-tps-1141-150.svg', title: 'Fusion', favicon: '//img.alicdn.com/imgextra/i3/O1CN01XtT3Tv1Wd1b5hNVKy_!!6000000002810-55-tps-360-360.svg', hash: true, outputPath: './doc-site', navs: { 'en-US': [ { title: 'Alibaba Fusion', path: '/components', }, { title: 'Home Site', path: 'https://formilyjs.org', }, { title: 'GITHUB', path: 'https://github.com/alibaba/formily', }, ], 'zh-CN': [ { title: 'Alibaba Fusion', path: '/zh-CN/components', }, { title: '主站', path: 'https://formilyjs.org', }, { title: 'GITHUB', path: 'https://github.com/alibaba/formily', }, ], }, links: [ { rel: 'stylesheet', href: 'https://esm.sh/@alifd/next/dist/next-noreset.css', }, ], headScripts: [ ` function loadAd(){ var header = document.querySelector('.__dumi-default-layout-content .markdown h1') if(header && !header.querySelector('#_carbonads_js')){ var script = document.createElement('script') script.src = '//cdn.carbonads.com/carbon.js?serve=CEAICK3M&placement=formilyjsorg' script.id = '_carbonads_js' script.classList.add('head-ad') header.appendChild(script) } } var request = null var observer = new MutationObserver(function(){ cancelIdleCallback(request) request = requestIdleCallback(loadAd) }) document.addEventListener('DOMContentLoaded',function(){ loadAd() observer.observe( document.body, { childList:true, subtree:true } ) }) `, ], styles: [ `.__dumi-default-navbar-logo{ background-size: 140px!important; background-position: center left!important; background-repeat: no-repeat!important; padding-left: 150px!important;/*可根据title的宽度调整*/ font-size: 22px!important; color: #000!important; font-weight: lighter!important; } .__dumi-default-navbar{ padding: 0 28px !important; } .__dumi-default-layout-hero{ background-image: url(//img.alicdn.com/imgextra/i4/O1CN01ZcvS4e26XMsdsCkf9_!!6000000007671-2-tps-6001-4001.png); background-size: cover; background-repeat: no-repeat; padding: 120px 0 !important; } .__dumi-default-layout-hero h1{ color:#45124e !important; font-size:80px !important; padding-bottom: 30px !important; } .__dumi-default-dark-switch { display:none } nav a{ text-decoration: none !important; } #carbonads * { margin: initial; padding: initial; } #carbonads { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Helvetica, Arial, sans-serif; } #carbonads { display: flex; max-width: 330px; background-color: hsl(0, 0%, 98%); box-shadow: 0 1px 4px 1px hsla(0, 0%, 0%, 0.1); z-index: 100; float:right; } #carbonads a { color: inherit; text-decoration: none; } #carbonads a:hover { color: inherit; } #carbonads span { position: relative; display: block; overflow: hidden; } #carbonads .carbon-wrap { display: flex; } #carbonads .carbon-img { display: block; margin: 0; line-height: 1; } #carbonads .carbon-img img { display: block; } #carbonads .carbon-text { font-size: 13px; padding: 10px; margin-bottom: 16px; line-height: 1.5; text-align: left; } #carbonads .carbon-poweredby { display: block; padding: 6px 8px; background: #f1f1f2; text-align: center; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; font-size: 8px; line-height: 1; border-top-left-radius: 3px; position: absolute; bottom: 0; right: 0; } `, ], } ================================================ FILE: packages/next/LESENCE.md ================================================ The MIT License (MIT) Copyright (c) 2015-present, Alibaba Group Holding Limited. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/next/README.md ================================================ # @formily/next ### Install ```bash npm install --save @formily/next ``` ================================================ FILE: packages/next/__tests__/moment.spec.ts ================================================ import { momentable, formatMomentValue } from '../src/__builtins__/moment' import moment from 'moment' test('momentable is usable', () => { expect(moment.isMoment(momentable('2021-09-08'))).toBe(true) expect( momentable(['2021-09-08', '2021-12-29']).every((item) => moment.isMoment(item) ) ).toBe(true) expect(momentable(0)).toBe(0) }) test('formatMomentValue is usable', () => { expect(formatMomentValue('', 'YYYY-MM-DD', '~')).toBe('~') expect(formatMomentValue('2021-12-22 15:47:00', 'YYYY-MM-DD')).toBe( '2021-12-22' ) expect(formatMomentValue('2021-12-23 15:47:00', undefined)).toBe( '2021-12-23 15:47:00' ) expect(formatMomentValue('2021-12-21 15:47:00', (date: string) => date)).toBe( '2021-12-21 15:47:00' ) expect(formatMomentValue('12:11', 'HH:mm')).toBe('12:11') expect(formatMomentValue('12:11:11', 'HH:mm:ss')).toBe('12:11:11') expect(formatMomentValue(['12:11'], ['HH:mm'])).toEqual(['12:11']) expect(formatMomentValue(['12:11:11'], ['HH:mm:ss'])).toEqual(['12:11:11']) expect(formatMomentValue(1663155911097, 'YYYY-MM-DD HH:mm:ss')).toBe( moment(1663155911097).format('YYYY-MM-DD HH:mm:ss') ) expect(formatMomentValue([1663155911097], ['YYYY-MM-DD HH:mm:ss'])).toEqual([ moment(1663155911097).format('YYYY-MM-DD HH:mm:ss'), ]) expect( formatMomentValue('2022-09-15T09:56:26.000Z', 'YYYY-MM-DD HH:mm:ss') ).toBe(moment('2022-09-15T09:56:26.000Z').format('YYYY-MM-DD HH:mm:ss')) expect( formatMomentValue(['2022-09-15T09:56:26.000Z'], ['YYYY-MM-DD HH:mm:ss']) ).toEqual([moment('2022-09-15T09:56:26.000Z').format('YYYY-MM-DD HH:mm:ss')]) expect(formatMomentValue('2022-09-15 09:56:26', 'HH:mm:ss')).toBe('09:56:26') expect(formatMomentValue(['2022-09-15 09:56:26'], ['HH:mm:ss'])).toEqual([ '09:56:26', ]) expect( formatMomentValue( ['2021-12-21 15:47:00', '2021-12-29 15:47:00'], 'YYYY-MM-DD' ) ).toEqual(['2021-12-21', '2021-12-29']) expect( formatMomentValue( ['2021-12-21 16:47:00', '2021-12-29 18:47:00'], (date: string) => date ) ).toEqual(['2021-12-21 16:47:00', '2021-12-29 18:47:00']) expect( formatMomentValue( ['2021-12-21 16:47:00', '2021-12-29 18:47:00'], ['YYYY-MM-DD', (date: string) => date] ) ).toEqual(['2021-12-21', '2021-12-29 18:47:00']) expect( formatMomentValue( ['2021-12-21 16:47:00', '2021-12-29 18:47:00'], ['YYYY-MM-DD', undefined] ) ).toEqual(['2021-12-21', '2021-12-29 18:47:00']) }) ================================================ FILE: packages/next/__tests__/sideEffects.spec.ts ================================================ import SideEffectsFlagPlugin from 'webpack/lib/optimize/SideEffectsFlagPlugin' // eslint-disable-next-line @typescript-eslint/no-var-requires const { sideEffects, name: baseName } = require('../package.json') test('sideEffects should be controlled manually', () => { // if config in pkg.json changed, please ensure it is covered by jest. expect(sideEffects).toStrictEqual([ 'dist/*', 'esm/*.js', 'lib/*.js', 'src/*.ts', '*.scss', '**/*/style.js', ]) }) test('dist/*', () => { // eg. import "@formily/next/dist/next.css" expect( SideEffectsFlagPlugin.moduleHasSideEffects('dist/next.css', 'dist/*') ).toBeTruthy() expect( SideEffectsFlagPlugin.moduleHasSideEffects( 'dist/formily.next.umd.production.js', 'dist/*' ) ).toBeTruthy() expect( SideEffectsFlagPlugin.moduleHasSideEffects( 'dist/formily.next.umd.production.js', 'dist/*' ) ).toBeTruthy() }) test('esm/*.js & lib/*.js', () => { // expected to be truthy // eg. import Formilynext from "@formily/next/esm/index" expect( SideEffectsFlagPlugin.moduleHasSideEffects('esm/index.js', 'esm/*.js') ).toBeTruthy() expect( SideEffectsFlagPlugin.moduleHasSideEffects('lib/index.js', 'lib/*.js') ).toBeTruthy() // expected to be falsy // eg. import Input from "@formily/next/esm/input/index" => will be compiled to __webpack_require__("./node_modules/@formily/next/esm/input/index.js") // It should be removed by webpack if not used after imported. expect( SideEffectsFlagPlugin.moduleHasSideEffects('esm/input/index.js', 'esm/*.js') ).toBeFalsy() expect( SideEffectsFlagPlugin.moduleHasSideEffects( 'esm/array-base/index.js', 'esm/*.js' ) ).toBeFalsy() expect( SideEffectsFlagPlugin.moduleHasSideEffects('lib/input/index.js', 'lib/*.js') ).toBeFalsy() }) test('*.scss', () => { // eg. import "@formily/next/lib/input/style.scss" expect( SideEffectsFlagPlugin.moduleHasSideEffects( `${baseName}/lib/input/style.scss`, '*.scss' ) ).toBeTruthy() }) test('**/*/style.js', () => { // eg. import "@formily/next/lib/input/style" will be compiled to __webpack_require__("./node_modules/@formily/next/lib/input/style.js") // so we can match the `*style.js` only, not `**/*/style*` may be cause someting mismatch like `@formily/next/lib/xxx-style/index.js` const modulePathArr = [ 'lib/input/style.js', `${baseName}/lib/input/style.js`, `./node_modules/${baseName}/style.js`, ] modulePathArr.forEach((modulePath) => { const hasSideEffects = SideEffectsFlagPlugin.moduleHasSideEffects( modulePath, '**/*/style.js' ) expect(hasSideEffects).toBeTruthy() }) }) ================================================ FILE: packages/next/build-style.ts ================================================ import { build } from '../../scripts/build-style' build({ esStr: '@alifd/next/es/', libStr: '@alifd/next/lib/', allStylesOutputFile: 'dist/next.css', }) ================================================ FILE: packages/next/create-style.ts ================================================ import glob from 'glob' import path from 'path' import fs from 'fs-extra' glob( './*/main.scss', { cwd: path.resolve(__dirname, './src') }, (err, files) => { if (err) return console.error(err) fs.writeFile( path.resolve(__dirname, './src/style.ts'), `// auto generated code ${files .map((path) => { return `import '${path}'\n` }) .join('')}`, 'utf8' ) fs.writeFile( path.resolve(__dirname, './src/main.scss'), `// auto generated code ${files .map((path) => { return `@import '${path}';\n` }) .join('')}`, 'utf8' ) } ) ================================================ FILE: packages/next/docs/components/ArrayCards.md ================================================ # ArrayCards > Card list, it is more suitable to use ArrayCards for scenarios with a large number of fields in each row and more linkages > > Note: This component is only applicable to Schema scenarios ## Markup Schema example ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() export default () => { return ( Submit ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, 'x-decorator': 'FormItem', 'x-component-props': { title: 'String array', }, items: { type: 'void', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCards.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, 'x-decorator': 'FormItem', 'x-component-props': { title: 'Object array', }, items: { type: 'object', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCards.Addition', }, }, }, }, } export default () => { return ( Submit ) } ``` ## Effects linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/next' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm({ effects: () => { //Active linkage mode onFieldChange('array.*.aa', ['value'], (field, form) => { form.setFieldState(field.query('.bb'), (state) => { state.visible = field.value != '123' }) }) //Passive linkage mode onFieldReact('array.*.dd', (field) => { field.visible = field.query('.cc').get('value') != '123' }) }, }) export default () => { return ( Submit ) } ``` ## JSON Schema linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, title: 'Object array', items: { type: 'object', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, aa: { type: 'string', 'x-decorator': 'FormItem', title: 'AA', required: true, 'x-component': 'Input', description: 'Enter 123', }, bb: { type: 'string', title: 'BB', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-reactions': [ { dependencies: ['.aa'], when: "{{$deps[0] != '123'}}", fulfill: { schema: { title: 'BB', 'x-disabled': true, }, }, otherwise: { schema: { title: 'Changed', 'x-disabled': false, }, }, }, ], }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCards.Addition', }, }, }, }, } export default () => { return ( Submit ) } ``` ## API ### ArrayCards Extended attributes | Property name | Type | Description | Default value | | ------------- | ------------------------- | --------------- | ------------- | | onAdd | `(index: number) => void` | add method | | | onRemove | `(index: number) => void` | remove method | | | onCopy | `(index: number) => void` | copy method | | | onMoveUp | `(index: number) => void` | moveUp method | | | onMoveDown | `(index: number) => void` | moveDown method | | Other Reference https://fusion.design/pc/component/basic/card ### ArrayCards.Addition > Add button Extended attributes | Property name | Type | Description | Default value | | ------------- | -------------------- | ------------- | ------------- | | title | ReactText | Copywriting | | | method | `'push' \|'unshift'` | add method | `'push'` | | defaultValue | `any` | Default value | | Other references https://fusion.design/pc/component/basic/button Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayCards.Copy > Copy button Extended attributes | Property name | Type | Description | Default value | | ------------- | -------------------- | ----------- | ------------- | | title | ReactText | Copywriting | | | method | `'push' \|'unshift'` | add method | `'push'` | Other references https://fusion.design/pc/component/basic/button Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayCards.Remove > Delete button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayCards.MoveDown > Move down button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayCards.MoveUp > Move up button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayCards.Index > Index Renderer No attributes ### ArrayCards.useIndex > Read the React Hook of the current rendering row index ### ArrayCards.useRecord > Read the React Hook of the current rendering row ================================================ FILE: packages/next/docs/components/ArrayCards.zh-CN.md ================================================ # ArrayCards > 卡片列表,对于每行字段数量较多,联动较多的场景比较适合使用 ArrayCards > > 注意:该组件只适用于 Schema 场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() export default () => { return ( 提交 ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, 'x-decorator': 'FormItem', 'x-component-props': { title: '字符串数组', }, items: { type: 'void', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCards.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, 'x-decorator': 'FormItem', 'x-component-props': { title: '对象数组', }, items: { type: 'object', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCards.Addition', }, }, }, }, } export default () => { return ( 提交 ) } ``` ## Effects 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/next' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm({ effects: () => { //主动联动模式 onFieldChange('array.*.aa', ['value'], (field, form) => { form.setFieldState(field.query('.bb'), (state) => { state.visible = field.value != '123' }) }) //被动联动模式 onFieldReact('array.*.dd', (field) => { field.visible = field.query('.cc').get('value') != '123' }) }, }) export default () => { return ( 提交 ) } ``` ## JSON Schema 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, title: '对象数组', items: { type: 'object', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, aa: { type: 'string', 'x-decorator': 'FormItem', title: 'AA', required: true, 'x-component': 'Input', description: '输入123', }, bb: { type: 'string', title: 'BB', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-reactions': [ { dependencies: ['.aa'], when: "{{$deps[0] != '123'}}", fulfill: { schema: { title: 'BB', 'x-disabled': true, }, }, otherwise: { schema: { title: 'Changed', 'x-disabled': false, }, }, }, ], }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCards.Addition', }, }, }, }, } export default () => { return ( 提交 ) } ``` ## API ### ArrayCards 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ---------- | ------------------------- | ------------ | ------ | | onAdd | `(index: number) => void` | 增加方法 | | | onRemove | `(index: number) => void` | 删除方法 | | | onCopy | `(index: number) => void` | 复制方法 | | | onMoveUp | `(index: number) => void` | 向上移动方法 | | | onMoveDown | `(index: number) => void` | 向下移动方法 | | 其余参考 https://fusion.design/pc/component/basic/card ### ArrayCards.Addition > 添加按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | | defaultValue | `any` | 默认值 | | 其余参考 https://fusion.design/pc/component/basic/button 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCards.Copy > 复制按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | 其余参考 https://fusion.design/pc/component/basic/button 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCards.Remove > 删除按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCards.MoveDown > 下移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCards.MoveUp > 上移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCards.Index > 索引渲染器 无属性 ### ArrayCards.useIndex > 读取当前渲染行索引的 React Hook ### ArrayCards.useRecord > 读取当前渲染记录的 React Hook ================================================ FILE: packages/next/docs/components/ArrayCollapse.md ================================================ # ArrayCollapse > Folding panel, it is more suitable to use ArrayCollapse for scenes with more fields in each row and more linkage > > Note: This component is only applicable to Schema scenarios ## Markup Schema example ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() export default () => { return ( Submit ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'void', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { title: 'String array', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCollapse.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { title: 'Object array', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCollapse.Addition', }, }, }, array_unshift: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { title: 'Object array', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry (unshift)', 'x-component': 'ArrayCollapse.Addition', 'x-component-props': { method: 'unshift', }, }, }, }, }, } export default () => { return ( Submit ) } ``` ## Effects linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/next' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm({ effects: () => { //Active linkage mode onFieldChange('array.*.aa', ['value'], (field, form) => { form.setFieldState(field.query('.bb'), (state) => { state.visible = field.value != '123' }) }) //Passive linkage mode onFieldReact('array.*.dd', (field) => { field.visible = field.query('.cc').get('value') != '123' }) }, }) export default () => { return ( Submit ) } ``` ## JSON Schema linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, title: 'Object array', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { title: 'Object array', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, aa: { type: 'string', 'x-decorator': 'FormItem', title: 'AA', required: true, 'x-component': 'Input', description: 'Enter 123', }, bb: { type: 'string', title: 'BB', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-reactions': [ { dependencies: ['.aa'], when: "{{$deps[0] != '123'}}", fulfill: { schema: { title: 'BB', 'x-disabled': true, }, }, otherwise: { schema: { title: 'Changed', 'x-disabled': false, }, }, }, ], }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCollapse.Addition', }, }, }, }, } export default () => { return ( Submit ) } ``` ## API ### ArrayCollapse Reference https://fusion.design/pc/component/collapse Extended attributes | Property name | Type | Description | Default value | | --------------------- | ------ | ---------------------------- | ------------- | | defaultOpenPanelCount | number | Default expanded Panel count | 5 | ### ArrayCollapse.CollapsePanel Reference https://fusion.design/pc/component/collapse ### ArrayCollapse.Addition > Add button Extended attributes | Property name | Type | Description | Default value | | ------------- | -------------------- | ------------- | ------------- | | title | ReactText | Copywriting | | | method | `'push' \|'unshift'` | add method | `'push'` | | defaultValue | `any` | Default value | | Other references https://fusion.design/pc/component/basic/button Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayCollapse.Remove > Delete button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayCollapse.MoveDown > Move down button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayCollapse.MoveUp > Move up button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayCollapse.Index > Index Renderer No attributes ### ArrayCollapse.useIndex > Read the React Hook of the current rendering row index ### ArrayCollapse.useRecord > Read the React Hook of the current rendering row ================================================ FILE: packages/next/docs/components/ArrayCollapse.zh-CN.md ================================================ # ArrayCollapse > 折叠面板,对于每行字段数量较多,联动较多的场景比较适合使用 ArrayCollapse > > 注意:该组件只适用于 Schema 场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Radio, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { Radio, FormItem, Input, ArrayCollapse, }, }) const form = createForm() export default () => { return ( 提交 ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'void', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { title: '字符串数组', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCollapse.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { title: '对象数组', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCollapse.Addition', }, }, }, array_unshift: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { title: '对象数组', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目(unshift)', 'x-component': 'ArrayCollapse.Addition', 'x-component-props': { method: 'unshift', }, }, }, }, }, } export default () => { return ( 提交 ) } ``` ## Effects 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/next' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm({ effects: () => { //主动联动模式 onFieldChange('array.*.aa', ['value'], (field, form) => { form.setFieldState(field.query('.bb'), (state) => { state.visible = field.value != '123' }) }) //被动联动模式 onFieldReact('array.*.dd', (field) => { field.visible = field.query('.cc').get('value') != '123' }) }, }) export default () => { return ( 提交 ) } ``` ## JSON Schema 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, title: '对象数组', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { title: '对象数组', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, aa: { type: 'string', 'x-decorator': 'FormItem', title: 'AA', required: true, 'x-component': 'Input', description: '输入123', }, bb: { type: 'string', title: 'BB', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-reactions': [ { dependencies: ['.aa'], when: "{{$deps[0] != '123'}}", fulfill: { schema: { title: 'BB', 'x-disabled': true, }, }, otherwise: { schema: { title: 'Changed', 'x-disabled': false, }, }, }, ], }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCollapse.Addition', }, }, }, }, } export default () => { return ( 提交 ) } ``` ## API ### ArrayCollapse 参考 https://fusion.design/pc/component/collapse 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | --------------------- | ------ | ------------------- | ------ | | defaultOpenPanelCount | number | 默认展开 Panel 数量 | 5 | ### ArrayCollapse.CollapsePanel 参考 https://fusion.design/pc/component/collapse ### ArrayCollapse.Addition > 添加按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | | defaultValue | `any` | 默认值 | | 其余参考 https://fusion.design/pc/component/basic/button 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCollapse.Remove > 删除按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCollapse.MoveDown > 下移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCollapse.MoveUp > 上移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCollapse.Index > 索引渲染器 无属性 ### ArrayCollapse.useIndex > 读取当前渲染行索引的 React Hook ### ArrayCollapse.useRecord > 读取当前渲染记录的 React Hook ================================================ FILE: packages/next/docs/components/ArrayItems.md ================================================ # ArrayItems > Self-increment list, suitable for simple self-increment editing scenes, or for scenes with high space requirements > > Note: This component is only applicable to Schema scenarios ## Markup Schema example ```tsx import React from 'react' import { FormItem, Input, Editable, Select, DatePicker, ArrayItems, FormButtonGroup, Submit, Space, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, DatePicker, Editable, Space, Input, Select, ArrayItems, }, }) const form = createForm() export default () => { return ( (field.title = field.value?.input || field.title) } > Submit ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormItem, Editable, Input, Select, Radio, DatePicker, ArrayItems, FormButtonGroup, Submit, Space, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Editable, DatePicker, Space, Radio, Input, Select, ArrayItems, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', title: 'String array', items: { type: 'void', 'x-component': 'Space', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, input: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, properties: { add: { type: 'void', title: 'Add entry', 'x-component': 'ArrayItems.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', title: 'Object array', items: { type: 'object', properties: { space: { type: 'void', 'x-component': 'Space', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, date: { type: 'string', title: 'Date', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { style: { width: 160, }, }, }, input: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Input', }, select: { type: 'string', title: 'drop-down box', enum: [ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 160, }, }, }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, }, }, properties: { add: { type: 'void', title: 'Add entry', 'x-component': 'ArrayItems.Addition', }, }, }, array2: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', 'x-component-props': { style: { width: 300 } }, title: 'Object array', items: { type: 'object', 'x-decorator': 'ArrayItems.Item', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, input: { type: 'string', title: 'input box', 'x-decorator': 'Editable', 'x-component': 'Input', }, config: { type: 'object', title: 'Configure complex data', 'x-component': 'Editable.Popover', 'x-reactions': '{{(field)=>field.title = field.value && field.value.input || field.title}}', properties: { date: { type: 'string', title: 'Date', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { style: { width: 160, }, followTrigger: true, }, }, input: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Input', }, select: { type: 'string', title: 'drop-down box', enum: [ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 160, }, }, }, }, }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, properties: { add: { type: 'void', title: 'Add entry', 'x-component': 'ArrayItems.Addition', }, }, }, }, } export default () => { return ( Submit ) } ``` ## Effects linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayItems, Editable, FormButtonGroup, Submit, Space, } from '@formily/next' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Space, Editable, FormItem, Input, ArrayItems, }, }) const form = createForm({ effects: () => { //Active linkage mode onFieldChange('array.*.aa', ['value'], (field, form) => { form.setFieldState(field.query('.bb'), (state) => { state.visible = field.value != '123' }) }) //Passive linkage mode onFieldReact('array.*.dd', (field) => { field.visible = field.query('.cc').get('value') != '123' }) }, }) export default () => { return ( Submit ) } ``` ## JSON Schema linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayItems, Editable, FormButtonGroup, Submit, Space, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Space, Editable, FormItem, Input, ArrayItems, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', maxItems: 3, title: 'Object array', 'x-component-props': { style: { width: 300 } }, items: { type: 'object', 'x-decorator': 'ArrayItems.Item', properties: { left: { type: 'void', 'x-component': 'Space', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, index: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Index', }, }, }, edit: { type: 'void', 'x-component': 'Editable.Popover', title: 'Configuration data', properties: { aa: { type: 'string', 'x-decorator': 'FormItem', title: 'AA', required: true, 'x-component': 'Input', description: 'Enter 123', }, bb: { type: 'string', title: 'BB', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-reactions': [ { dependencies: ['.aa'], when: "{{$deps[0] != '123'}}", fulfill: { schema: { title: 'BB', 'x-disabled': true, }, }, otherwise: { schema: { title: 'Changed', 'x-disabled': false, }, }, }, ], }, }, }, right: { type: 'void', 'x-component': 'Space', properties: { remove: { type: 'void', 'x-component': 'ArrayItems.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayItems.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayItems.MoveDown', }, }, }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayItems.Addition', }, }, }, }, } export default () => { return ( Submit ) } ``` ## API ### ArrayItems Extended attributes | Property name | Type | Description | Default value | | ------------- | ------------------------- | --------------- | ------------- | | onAdd | `(index: number) => void` | add method | | | onRemove | `(index: number) => void` | remove method | | | onCopy | `(index: number) => void` | copy method | | | onMoveUp | `(index: number) => void` | moveUp method | | | onMoveDown | `(index: number) => void` | moveDown method | | Other Inherit HTMLDivElement Props ### ArrayItems.Item > List block Inherit HTMLDivElement Props Extended attributes | Property name | Type | Description | Default value | | ------------- | ------------------- | --------------------- | ------------- | | type | `'card' \|'divide'` | card or dividing line | | ### ArrayItems.SortHandle > Drag handle Reference https://ant.design/components/icon-cn/ ### ArrayItems.Addition > Add button Extended attributes | Property name | Type | Description | Default value | | ------------- | -------------------- | ------------- | ------------- | | title | ReactText | Copywriting | | | method | `'push' \|'unshift'` | add method | `'push'` | | defaultValue | `any` | Default value | | Other references https://fusion.design/pc/component/basic/button Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayItems.Copy > Copy button Extended attributes | Property name | Type | Description | Default value | | ------------- | -------------------- | ----------- | ------------- | | title | ReactText | Copywriting | | | method | `'push' \|'unshift'` | add method | `'push'` | Other references https://fusion.design/pc/component/basic/button Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayItems.Remove > Delete button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayItems.MoveDown > Move down button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayItems.MoveUp > Move up button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayItems.Index > Index Renderer No attributes ### ArrayItems.useIndex > Read the React Hook of the current rendering row index ### ArrayItems.useRecord > Read the React Hook of the current rendering row ================================================ FILE: packages/next/docs/components/ArrayItems.zh-CN.md ================================================ # ArrayItems > 自增列表,对于简单的自增编辑场景比较适合,或者对于空间要求高的场景比较适合 > > 注意:该组件只适用于 Schema 场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormItem, Input, Editable, Select, DatePicker, ArrayItems, FormButtonGroup, Submit, Space, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, DatePicker, Editable, Space, Input, Select, ArrayItems, }, }) const form = createForm() export default () => { return ( (field.title = field.value?.input || field.title) } > 提交 ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormItem, Editable, Input, Select, Radio, DatePicker, ArrayItems, FormButtonGroup, Submit, Space, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Editable, DatePicker, Space, Radio, Input, Select, ArrayItems, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', title: '字符串数组', items: { type: 'void', 'x-component': 'Space', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, input: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, properties: { add: { type: 'void', title: '添加条目', 'x-component': 'ArrayItems.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', title: '对象数组', items: { type: 'object', properties: { space: { type: 'void', 'x-component': 'Space', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, date: { type: 'string', title: '日期', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { style: { width: 160, }, }, }, input: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'Input', }, select: { type: 'string', title: '下拉框', enum: [ { label: '选项1', value: 1 }, { label: '选项2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 160, }, }, }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, }, }, properties: { add: { type: 'void', title: '添加条目', 'x-component': 'ArrayItems.Addition', }, }, }, array2: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', 'x-component-props': { style: { width: 300 } }, title: '对象数组', items: { type: 'object', 'x-decorator': 'ArrayItems.Item', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, input: { type: 'string', title: '输入框', 'x-decorator': 'Editable', 'x-component': 'Input', }, config: { type: 'object', title: '配置复杂数据', 'x-component': 'Editable.Popover', 'x-reactions': '{{(field)=>field.title = field.value && field.value.input || field.title}}', properties: { date: { type: 'string', title: '日期', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { style: { width: 160, }, followTrigger: true, }, }, input: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'Input', }, select: { type: 'string', title: '下拉框', enum: [ { label: '选项1', value: 1 }, { label: '选项2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 160, }, }, }, }, }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, properties: { add: { type: 'void', title: '添加条目', 'x-component': 'ArrayItems.Addition', }, }, }, }, } export default () => { return ( 提交 ) } ``` ## Effects 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayItems, Editable, FormButtonGroup, Submit, Space, } from '@formily/next' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Space, Editable, FormItem, Input, ArrayItems, }, }) const form = createForm({ effects: () => { //主动联动模式 onFieldChange('array.*.aa', ['value'], (field, form) => { form.setFieldState(field.query('.bb'), (state) => { state.visible = field.value != '123' }) }) //被动联动模式 onFieldReact('array.*.dd', (field) => { field.visible = field.query('.cc').get('value') != '123' }) }, }) export default () => { return ( 提交 ) } ``` ## JSON Schema 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayItems, Editable, FormButtonGroup, Submit, Space, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Space, Editable, FormItem, Input, ArrayItems, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', maxItems: 3, title: '对象数组', 'x-component-props': { style: { width: 300 } }, items: { type: 'object', 'x-decorator': 'ArrayItems.Item', properties: { left: { type: 'void', 'x-component': 'Space', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, index: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Index', }, }, }, edit: { type: 'void', 'x-component': 'Editable.Popover', title: '配置数据', properties: { aa: { type: 'string', 'x-decorator': 'FormItem', title: 'AA', required: true, 'x-component': 'Input', description: '输入123', }, bb: { type: 'string', title: 'BB', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-reactions': [ { dependencies: ['.aa'], when: "{{$deps[0] != '123'}}", fulfill: { schema: { title: 'BB', 'x-disabled': true, }, }, otherwise: { schema: { title: 'Changed', 'x-disabled': false, }, }, }, ], }, }, }, right: { type: 'void', 'x-component': 'Space', properties: { remove: { type: 'void', 'x-component': 'ArrayItems.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayItems.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayItems.MoveDown', }, }, }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayItems.Addition', }, }, }, }, } export default () => { return ( 提交 ) } ``` ## API ### ArrayItems 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ---------- | ------------------------- | ------------ | ------ | | onAdd | `(index: number) => void` | 增加方法 | | | onRemove | `(index: number) => void` | 删除方法 | | | onCopy | `(index: number) => void` | 复制方法 | | | onMoveUp | `(index: number) => void` | 向上移动方法 | | | onMoveDown | `(index: number) => void` | 向下移动方法 | | 其余继承 HTMLDivElement Props ### ArrayItems.Item > 列表区块 继承 HTMLDivElement Props 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------ | -------------------- | -------------- | ------ | | type | `'card' \| 'divide'` | 卡片或者分割线 | | ### ArrayItems.SortHandle > 拖拽手柄 参考 https://ant.design/components/icon-cn/ ### ArrayItems.Addition > 添加按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | | defaultValue | `any` | 默认值 | | 其余参考 https://fusion.design/pc/component/basic/button 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayItems.Copy > 复制按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | 其余参考 https://fusion.design/pc/component/basic/button 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayItems.Remove > 删除按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayItems.MoveDown > 下移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayItems.MoveUp > 上移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayItems.Index > 索引渲染器 无属性 ### ArrayItems.useIndex > 读取当前渲染行索引的 React Hook ### ArrayItems.useRecord > 读取当前渲染记录的 React Hook ================================================ FILE: packages/next/docs/components/ArrayTable.md ================================================ # ArrayTable > Self-increasing table, it is more suitable to use this component for scenes with a large amount of data. Although the amount of data is large to a certain extent, it will be a little bit stuck, but it will not affect the basic operation > > Note: This component is only applicable to Schema scenarios and can only be an array of objects ## Markup Schema example ```tsx import React from 'react' import { FormItem, Input, ArrayTable, Editable, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button, Message } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, Editable, Input, ArrayTable, }, }) const form = createForm() const range = (count: number) => Array.from(new Array(count)).map((_, key) => ({ aaa: key, })) export default () => { return ( Submit Note: Open the formily plug-in page, because there is data communication in the background, which will occupy the browser's computing power, it is best to test in the incognito mode (without the formily plug-in) ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormItem, Input, ArrayTable, Editable, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Editable, Input, ArrayTable, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'ArrayTable', 'x-component-props': { pagination: { pageSize: 10 }, style: { width: '100%' }, }, items: { type: 'object', properties: { column1: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 80, title: 'Sort', align: 'center' }, properties: { sort: { type: 'void', 'x-component': 'ArrayTable.SortHandle', }, }, }, column2: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 120, title: 'Index', align: 'center', }, properties: { index: { type: 'void', 'x-component': 'ArrayTable.Index', }, }, }, column3: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A1' }, properties: { a1: { type: 'string', 'x-decorator': 'Editable', 'x-component': 'Input', }, }, }, column4: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A2' }, properties: { a2: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, column5: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A3' }, properties: { a3: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, column6: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Operations', dataIndex: 'operations', width: 200, fixed: 'right', }, properties: { item: { type: 'void', 'x-component': 'FormItem', properties: { remove: { type: 'void', 'x-component': 'ArrayTable.Remove', }, moveDown: { type: 'void', 'x-component': 'ArrayTable.MoveDown', }, moveUp: { type: 'void', 'x-component': 'ArrayTable.MoveUp', }, }, }, }, }, }, }, properties: { add: { type: 'void', 'x-component': 'ArrayTable.Addition', title: 'Add entry', }, }, }, }, } export default () => { return ( Submit ) } ``` ## Effects linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayTable, Switch, FormButtonGroup, Submit, } from '@formily/next' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, Switch, Input, Button, ArrayTable, }, }) const form = createForm({ effects: () => { //Active linkage mode onFieldChange('hideFirstColumn', ['value'], (field) => { field.query('array.column4').take((target) => { target.visible = !field.value }) field.query('array.*.a2').take((target) => { target.visible = !field.value }) }) //Passive linkage mode onFieldReact('array.*.a2', (field) => { field.visible = !field.query('.a1').get('value') }) }, }) export default () => { return ( A2', dataIndex: 'a1', width: 100, }} > Submit ) } ``` ## JSON Schema linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayTable, Switch, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Switch, Input, ArrayTable, }, }) const form = createForm() const schema = { type: 'object', properties: { hideFirstColumn: { type: 'boolean', title: 'Hide A2', 'x-decorator': 'FormItem', 'x-component': 'Switch', }, array: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'ArrayTable', 'x-component-props': { pagination: { pageSize: 10 }, style: { width: '100%' }, }, items: { type: 'object', properties: { column1: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 80, title: 'Sort', align: 'center' }, properties: { sort: { type: 'void', 'x-component': 'ArrayTable.SortHandle', }, }, }, column2: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 120, title: 'Index', align: 'center', }, properties: { index: { type: 'void', 'x-component': 'ArrayTable.Index', }, }, }, column3: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 100, title: 'Explicitly hidden->A2' }, properties: { a1: { type: 'boolean', 'x-decorator': 'FormItem', 'x-component': 'Switch', }, }, }, column4: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A2' }, 'x-reactions': [ { dependencies: ['hideFirstColumn'], when: '{{$deps[0]}}', fulfill: { schema: { 'x-visible': false, }, }, otherwise: { schema: { 'x-visible': true, }, }, }, ], properties: { a2: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', required: true, 'x-reactions': [ { dependencies: ['.a1', 'hideFirstColumn'], when: '{{$deps[1] || $deps[0]}}', fulfill: { schema: { 'x-visible': false, }, }, otherwise: { schema: { 'x-visible': true, }, }, }, ], }, }, }, column5: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A3' }, properties: { a3: { type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, column6: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Operations', dataIndex: 'operations', width: 200, fixed: 'right', }, properties: { item: { type: 'void', 'x-component': 'FormItem', properties: { remove: { type: 'void', 'x-component': 'ArrayTable.Remove', }, moveDown: { type: 'void', 'x-component': 'ArrayTable.MoveDown', }, moveUp: { type: 'void', 'x-component': 'ArrayTable.MoveUp', }, }, }, }, }, }, }, properties: { add: { type: 'void', 'x-component': 'ArrayTable.Addition', title: 'Add entry', }, }, }, }, } export default () => { return ( Submit ) } ``` ## API ### ArrayTable > Form Components Extended attributes | Property name | Type | Description | Default value | | ------------- | ------------------------- | --------------- | ------------- | | onAdd | `(index: number) => void` | add method | | | onRemove | `(index: number) => void` | remove method | | | onCopy | `(index: number) => void` | copy method | | | onMoveUp | `(index: number) => void` | moveUp method | | | onMoveDown | `(index: number) => void` | moveDown method | | Other Reference https://fusion.design/pc/component/basic/table ### ArrayTable.Column > Table Column Reference https://fusion.design/pc/component/basic/table ### ArrayTable.SortHandle > Drag handle Reference https://ant.design/components/icon-cn/ ### ArrayTable.Addition > Add button Extended attributes | Property name | Type | Description | Default value | | ------------- | -------------------- | ------------- | ------------- | | title | ReactText | Copywriting | | | method | `'push' \|'unshift'` | add method | `'push'` | | defaultValue | `any` | Default value | | Other references https://fusion.design/pc/component/basic/button Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayTable.Remove > Delete button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayTable.MoveDown > Move down button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayTable.MoveUp > Move up button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayTable.Index > Index Renderer No attributes ### ArrayTable.useIndex > Read the React Hook of the current rendering row index ### ArrayTable.useRecord > Read the React Hook of the current rendering row ================================================ FILE: packages/next/docs/components/ArrayTable.zh-CN.md ================================================ # ArrayTable > 自增表格,对于数据量超大的场景比较适合使用该组件,虽然数据量大到一定程度会有些许卡顿,但是不会影响基本操作 > > 注意:该组件只适用于 Schema 场景,且只能是对象数组 ## Markup Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayTable, Editable, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button, Message } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, Editable, Input, ArrayTable, }, }) const form = createForm() const range = (count: number) => Array.from(new Array(count)).map((_, key) => ({ aaa: key, })) export default () => { return ( 提交 注意:开启formily插件的页面,因为后台有数据通信,会占用浏览器算力,最好在无痕模式(无formily插件)下测试 ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayTable, Editable, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Editable, Input, ArrayTable, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'ArrayTable', 'x-component-props': { pagination: { pageSize: 10 }, style: { width: '100%' }, }, items: { type: 'object', properties: { column1: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 80, title: 'Sort', align: 'center' }, properties: { sort: { type: 'void', 'x-component': 'ArrayTable.SortHandle', }, }, }, column2: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 120, title: 'Index', align: 'center', }, properties: { index: { type: 'void', 'x-component': 'ArrayTable.Index', }, }, }, column3: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A1' }, properties: { a1: { type: 'string', 'x-decorator': 'Editable', 'x-component': 'Input', }, }, }, column4: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A2' }, properties: { a2: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, column5: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A3' }, properties: { a3: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, column6: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Operations', dataIndex: 'operations', width: 200, fixed: 'right', }, properties: { item: { type: 'void', 'x-component': 'FormItem', properties: { remove: { type: 'void', 'x-component': 'ArrayTable.Remove', }, moveDown: { type: 'void', 'x-component': 'ArrayTable.MoveDown', }, moveUp: { type: 'void', 'x-component': 'ArrayTable.MoveUp', }, }, }, }, }, }, }, properties: { add: { type: 'void', 'x-component': 'ArrayTable.Addition', title: '添加条目', }, }, }, }, } export default () => { return ( 提交 ) } ``` ## Effects 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayTable, Switch, FormButtonGroup, Submit, } from '@formily/next' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, Switch, Input, Button, ArrayTable, }, }) const form = createForm({ effects: () => { //主动联动模式 onFieldChange('hideFirstColumn', ['value'], (field) => { field.query('array.column4').take((target) => { target.visible = !field.value }) field.query('array.*.a2').take((target) => { target.visible = !field.value }) }) //被动联动模式 onFieldReact('array.*.a2', (field) => { field.visible = !field.query('.a1').get('value') }) }, }) export default () => { return ( A2', dataIndex: 'a1', width: 100, }} > 提交 ) } ``` ## JSON Schema 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayTable, Switch, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Switch, Input, ArrayTable, }, }) const form = createForm() const schema = { type: 'object', properties: { hideFirstColumn: { type: 'boolean', title: '隐藏A2', 'x-decorator': 'FormItem', 'x-component': 'Switch', }, array: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'ArrayTable', 'x-component-props': { pagination: { pageSize: 10 }, style: { width: '100%' }, }, items: { type: 'object', properties: { column1: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 80, title: 'Sort', align: 'center' }, properties: { sort: { type: 'void', 'x-component': 'ArrayTable.SortHandle', }, }, }, column2: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 120, title: 'Index', align: 'center', }, properties: { index: { type: 'void', 'x-component': 'ArrayTable.Index', }, }, }, column3: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 100, title: '显隐->A2' }, properties: { a1: { type: 'boolean', 'x-decorator': 'FormItem', 'x-component': 'Switch', }, }, }, column4: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A2' }, 'x-reactions': [ { dependencies: ['hideFirstColumn'], when: '{{$deps[0]}}', fulfill: { schema: { 'x-visible': false, }, }, otherwise: { schema: { 'x-visible': true, }, }, }, ], properties: { a2: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', required: true, 'x-reactions': [ { dependencies: ['.a1', 'hideFirstColumn'], when: '{{$deps[1] || $deps[0]}}', fulfill: { schema: { 'x-visible': false, }, }, otherwise: { schema: { 'x-visible': true, }, }, }, ], }, }, }, column5: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { width: 200, title: 'A3' }, properties: { a3: { type: 'string', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, column6: { type: 'void', 'x-component': 'ArrayTable.Column', 'x-component-props': { title: 'Operations', dataIndex: 'operations', width: 200, fixed: 'right', }, properties: { item: { type: 'void', 'x-component': 'FormItem', properties: { remove: { type: 'void', 'x-component': 'ArrayTable.Remove', }, moveDown: { type: 'void', 'x-component': 'ArrayTable.MoveDown', }, moveUp: { type: 'void', 'x-component': 'ArrayTable.MoveUp', }, }, }, }, }, }, }, properties: { add: { type: 'void', 'x-component': 'ArrayTable.Addition', title: '添加条目', }, }, }, }, } export default () => { return ( 提交 ) } ``` ## API ### ArrayTable > 表格组件 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ---------- | ------------------------- | ------------ | ------ | | onAdd | `(index: number) => void` | 增加方法 | | | onRemove | `(index: number) => void` | 删除方法 | | | onCopy | `(index: number) => void` | 复制方法 | | | onMoveUp | `(index: number) => void` | 向上移动方法 | | | onMoveDown | `(index: number) => void` | 向下移动方法 | | 其余参考 https://fusion.design/pc/component/basic/table ### ArrayTable.Column > 表格列 参考 https://fusion.design/pc/component/basic/table ### ArrayTable.SortHandle > 拖拽手柄 参考 https://ant.design/components/icon-cn/ ### ArrayTable.Addition > 添加按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | | defaultValue | `any` | 默认值 | | 其余参考 https://fusion.design/pc/component/basic/button 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayTable.Remove > 删除按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayTable.MoveDown > 下移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayTable.MoveUp > 上移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayTable.Index > 索引渲染器 无属性 ### ArrayTable.useIndex > 读取当前渲染行索引的 React Hook ### ArrayTable.useRecord > 读取当前渲染记录的 React Hook ================================================ FILE: packages/next/docs/components/Cascader.md ================================================ # Cascader > Cascade selector ## Markup Schema example ```tsx import React from 'react' import { Cascader, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm, onFieldReact, FormPathPattern } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Cascader, FormItem, }, }) const useAddress = (pattern: FormPathPattern) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } onFieldReact(pattern, (field) => { field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }) } const form = createForm({ effects: () => { useAddress('address') }, }) export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Cascader, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Cascader, FormItem, }, }) const transformAddress = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transformAddress(cities) const _districts = transformAddress(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } const useAsyncDataSource = (url: string, transform: (data: any) => any) => (field) => { field.loading = true fetch(url) .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) } const form = createForm() const schema = { type: 'object', properties: { address: { type: 'string', title: 'Address Selection', 'x-decorator': 'FormItem', 'x-component': 'Cascader', 'x-component-props': { style: { width: 240, }, }, 'x-reactions': [ '{{useAsyncDataSource("//unpkg.com/china-location/dist/location.json",transformAddress)}}', ], }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Cascader, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm, onFieldReact, FormPathPattern } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { action } from '@formily/reactive' const useAddress = (pattern: FormPathPattern) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } onFieldReact(pattern, (field) => { field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }) } const form = createForm({ effects: () => { useAddress('address') }, }) export default () => ( Submit ) ``` ## API Reference https://fusion.design/pc/component/basic/cascader-select ================================================ FILE: packages/next/docs/components/Cascader.zh-CN.md ================================================ # Cascader > 联级选择器 ## Markup Schema 案例 ```tsx import React from 'react' import { Cascader, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm, onFieldReact, FormPathPattern } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Cascader, FormItem, }, }) const useAddress = (pattern: FormPathPattern) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } onFieldReact(pattern, (field) => { field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }) } const form = createForm({ effects: () => { useAddress('address') }, }) export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Cascader, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Cascader, FormItem, }, }) const transformAddress = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transformAddress(cities) const _districts = transformAddress(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } const useAsyncDataSource = (url: string, transform: (data: any) => any) => (field) => { field.loading = true fetch(url) .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) } const form = createForm() const schema = { type: 'object', properties: { address: { type: 'string', title: '地址选择', 'x-decorator': 'FormItem', 'x-component': 'Cascader', 'x-component-props': { style: { width: 240, }, }, 'x-reactions': [ '{{useAsyncDataSource("//unpkg.com/china-location/dist/location.json",transformAddress)}}', ], }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Cascader, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm, onFieldReact, FormPathPattern } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { action } from '@formily/reactive' const useAddress = (pattern: FormPathPattern) => { const transform = (data = {}) => { return Object.entries(data).reduce((buf, [key, value]) => { if (typeof value === 'string') return buf.concat({ label: value, value: key, }) const { name, code, cities, districts } = value const _cities = transform(cities) const _districts = transform(districts) return buf.concat({ label: name, value: code, children: _cities.length ? _cities : _districts.length ? _districts : undefined, }) }, []) } onFieldReact(pattern, (field) => { field.loading = true fetch('//unpkg.com/china-location/dist/location.json') .then((res) => res.json()) .then( action.bound((data) => { field.dataSource = transform(data) field.loading = false }) ) }) } const form = createForm({ effects: () => { useAddress('address') }, }) export default () => ( 提交 ) ``` ## API 参考 https://fusion.design/pc/component/basic/cascader-select ================================================ FILE: packages/next/docs/components/Checkbox.md ================================================ # Checkbox > Checkbox ## Markup Schema example ```tsx import React from 'react' import { Checkbox, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Checkbox, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Checkbox, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Checkbox, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { single: { type: 'boolean', title: 'Are you sure?', 'x-decorator': 'FormItem', 'x-component': 'Checkbox', }, multiple: { type: 'array', title: 'Check', enum: [ { label: 'Option 1', value: 1, }, { label: 'Option 2', value: 2, }, ], 'x-decorator': 'FormItem', 'x-component': 'Checkbox.Group', }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Checkbox, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://fusion.design/pc/component/basic/checkbox ================================================ FILE: packages/next/docs/components/Checkbox.zh-CN.md ================================================ # Checkbox > 复选框 ## Markup Schema 案例 ```tsx import React from 'react' import { Checkbox, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Checkbox, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Checkbox, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Checkbox, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { single: { type: 'boolean', title: '是否确认', 'x-decorator': 'FormItem', 'x-component': 'Checkbox', }, multiple: { type: 'array', title: '复选', enum: [ { label: '选项1', value: 1, }, { label: '选项2', value: 2, }, ], 'x-decorator': 'FormItem', 'x-component': 'Checkbox.Group', }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Checkbox, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://fusion.design/pc/component/basic/checkbox ================================================ FILE: packages/next/docs/components/DatePicker.md ================================================ # DatePicker > Date Picker ## Markup Schema example ```tsx import React from 'react' import { DatePicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { DatePicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { date: { title: 'Normal date', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', type: 'string', }, week: { title: 'Week Selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.WeekPicker', type: 'string', }, month: { title: 'Month Selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.MonthPicker', type: 'string', }, year: { title: 'Year selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.YearPicker', type: 'string', }, '[startDate,endDate]': { title: 'Date range', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { showTime: true, }, type: 'string', }, range_month: { title: 'Month Range Selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { type: 'month', }, type: 'string', }, range_year: { name: 'range_year', title: 'Year range selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { type: 'year', }, type: 'string', }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { DatePicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://fusion.design/pc/component/basic/date-picker ================================================ FILE: packages/next/docs/components/DatePicker.zh-CN.md ================================================ # DatePicker > 日期选择器 ## Markup Schema 案例 ```tsx import React from 'react' import { DatePicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { DatePicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { date: { title: '普通日期', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', type: 'string', }, week: { title: '周选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.WeekPicker', type: 'string', }, month: { title: '月选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.MonthPicker', type: 'string', }, year: { title: '年选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.YearPicker', type: 'string', }, '[startDate,endDate]': { title: '日期范围', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { showTime: true, }, type: 'string', }, range_month: { title: '月范围选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { type: 'month', }, type: 'string', }, range_year: { name: 'range_year', title: '年范围选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { type: 'year', }, type: 'string', }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { DatePicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://fusion.design/pc/component/basic/date-picker ================================================ FILE: packages/next/docs/components/DatePicker2.md ================================================ # DatePicker2 > Date Picker ## Markup Schema example ```tsx import React from 'react' import { DatePicker2, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker2, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { DatePicker2, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker2, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { date: { title: 'Normal date', 'x-decorator': 'FormItem', 'x-component': 'DatePicker2', type: 'string', }, week: { title: 'Week Selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker2.WeekPicker', type: 'string', }, month: { title: 'Month Selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker2.MonthPicker', type: 'string', }, year: { title: 'Year selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker2.YearPicker', type: 'string', }, '[startDate,endDate]': { title: 'Date range', 'x-decorator': 'FormItem', 'x-component': 'DatePicker2.RangePicker', 'x-component-props': { showTime: true, }, type: 'string', }, range_month: { title: 'Month Range Selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker2.RangePicker', 'x-component-props': { mode: 'month', }, type: 'string', }, range_year: { name: 'range_year', title: 'Year range selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker2.RangePicker', 'x-component-props': { mode: 'year', }, type: 'string', }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { DatePicker2, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://fusion.design/pc/component/basic/date-picker2 ================================================ FILE: packages/next/docs/components/DatePicker2.zh-CN.md ================================================ # DatePicker2 > 日期选择器 ## Markup Schema 案例 ```tsx import React from 'react' import { DatePicker2, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker2, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { DatePicker2, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker2, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { date: { title: '普通日期', 'x-decorator': 'FormItem', 'x-component': 'DatePicker2', type: 'string', }, week: { title: '周选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker2.WeekPicker', type: 'string', }, month: { title: '月选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker2.MonthPicker', type: 'string', }, year: { title: '年选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker2.YearPicker', type: 'string', }, '[startDate,endDate]': { title: '日期范围', 'x-decorator': 'FormItem', 'x-component': 'DatePicker2.RangePicker', 'x-component-props': { showTime: true, }, type: 'string', }, range_month: { title: '月范围选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker2.RangePicker', 'x-component-props': { mode: 'month', }, type: 'string', }, range_year: { name: 'range_year', title: '年范围选择', 'x-decorator': 'FormItem', 'x-component': 'DatePicker2.RangePicker', 'x-component-props': { mode: 'year', }, type: 'string', }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { DatePicker2, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://fusion.design/pc/component/basic/date-picker2 ================================================ FILE: packages/next/docs/components/Editable.md ================================================ # Editable > Partial editor, you can use this component for some form areas with high space requirements > > Editable component is equivalent to a variant of FormItem component, so it is usually placed in decorator ## Markup Schema example ```tsx import React from 'react' import { Input, DatePicker, Editable, FormItem, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, Editable, Input, FormItem, }, }) const form = createForm() export default () => ( { field.title = field.query('.void.date2').get('value') || field.title }} > { field.title = field.value?.date || field.title }} > Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Input, DatePicker, Editable, FormItem, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, Editable, Input, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { date: { type: 'string', title: 'Date', 'x-decorator': 'Editable', 'x-component': 'DatePicker', }, input: { type: 'string', title: 'input box', 'x-decorator': 'Editable', 'x-component': 'Input', }, void: { type: 'void', title: 'Virtual Node Container', 'x-component': 'Editable.Popover', 'x-reactions': "{{(field) => field.title = field.query('.void.date2').get('value') || field.title}}", properties: { date2: { type: 'string', title: 'Date', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', 'x-component-props': { followTrigger: true, }, }, input2: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, iobject: { type: 'object', title: 'Object node container', 'x-component': 'Editable.Popover', 'x-reactions': '{{(field) => field.title = field.value && field.value.date || field.title}}', properties: { date: { type: 'string', title: 'Date', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', 'x-component-props': { followTrigger: true, }, }, input: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Input, DatePicker, Editable, FormItem, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field, VoidField, ObjectField } from '@formily/react' const form = createForm() export default () => ( { field.title = field.query('.void.date2').get('value') || field.title }} component={[Editable.Popover]} > { return field.value?.date }, }, ]} > Submit ) ``` ## API ### Editable > Inline editing Refer to the FormItem property in https://fusion.design/pc/component/basic/form ### Editable.Popover > Floating layer editing | Property name | Type | Description | Default value | | ------------- | --------------------------------- | ---------------- | ------------- | | renderPreview | `(field:GeneralField)=>ReactNode` | Preview renderer | | Note: If there is a floating layer component such as Select/DatePicker inside the Popover, followTrigger=true needs to be configured on the floating layer component Other references https://fusion.design/pc/component/basic/balloon ================================================ FILE: packages/next/docs/components/Editable.zh-CN.md ================================================ # Editable > 局部编辑器,对于一些空间要求较高的表单区域可以使用该组件 > > Editable 组件相当于是 FormItem 组件的变体,所以通常放在 decorator 中 ## Markup Schema 案例 ```tsx import React from 'react' import { Input, DatePicker, Editable, FormItem, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, Editable, Input, FormItem, }, }) const form = createForm() export default () => ( { field.title = field.query('.void.date2').get('value') || field.title }} > { field.title = field.value?.date || field.title }} > 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Input, DatePicker, Editable, FormItem, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, Editable, Input, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { date: { type: 'string', title: '日期', 'x-decorator': 'Editable', 'x-component': 'DatePicker', }, input: { type: 'string', title: '输入框', 'x-decorator': 'Editable', 'x-component': 'Input', }, void: { type: 'void', title: '虚拟节点容器', 'x-component': 'Editable.Popover', 'x-reactions': "{{(field) => field.title = field.query('.void.date2').get('value') || field.title}}", properties: { date2: { type: 'string', title: '日期', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', 'x-component-props': { followTrigger: true, }, }, input2: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, iobject: { type: 'object', title: '对象节点容器', 'x-component': 'Editable.Popover', 'x-reactions': '{{(field) => field.title = field.value && field.value.date || field.title}}', properties: { date: { type: 'string', title: '日期', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', 'x-component-props': { followTrigger: true, }, }, input: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Input, DatePicker, Editable, FormItem, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field, VoidField, ObjectField } from '@formily/react' const form = createForm() export default () => ( { field.title = field.query('.void.date2').get('value') || field.title }} component={[Editable.Popover]} > { return field.value?.date }, }, ]} > 提交 ) ``` ## API ### Editable > 内联编辑 参考 https://fusion.design/pc/component/basic/form 中的 FormItem 属性 ### Editable.Popover > 浮层编辑 | 属性名 | 类型 | 描述 | 默认值 | | ------------- | --------------------------------- | ---------- | ------ | | renderPreview | `(field:GeneralField)=>ReactNode` | 预览渲染器 | | 注意:如果在 Popover 内部有 Select/DatePicker 之类的浮层组件,需要在浮层组件上配置 followTrigger=true 其余参考 https://fusion.design/pc/component/basic/balloon ================================================ FILE: packages/next/docs/components/Form.md ================================================ # Form > The combination of FormProvider + FormLayout + form tags can help us quickly implement forms that are submitted with carriage return and can be laid out in batches ## Use Cases ```tsx import React from 'react' import { Input, Select, Form, FormItem, FormGrid, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { Field } from '@formily/react' const form = createForm() export default () => ( Query ) ``` ## Fusion Multilingual ```tsx import React from 'react' import { Input, Form, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { Field } from '@formily/react' import { ConfigProvider } from '@alifd/next' import enUS from '@alifd/next/lib/locale/en-us' const form = createForm() export default () => (

Submit ) ``` Note: To realize the carriage return submission, we cannot pass the onSubmit event to it when using the Submit component, otherwise the carriage return submission will become invalid. The purpose of this is to prevent users from writing onSubmit event listeners in multiple places at the same time, and processing logic If they are inconsistent, it is difficult to locate the problem when submitting. ## API For layout-related API properties, we can refer to [FormLayout](./form-layout), and the rest are the unique API properties of the Form component | Property name | Type | Description | Default value | | ---------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | ------------- | | form | [Form](https://core.formilyjs.org/api/models/form) | Form example | - | | component | string | Rendering component, can be specified as custom component rendering | `form` | | previewTextPlaceholder | ReactNode | Preview State Placeholder | `N/A` | | onAutoSubmit | `(values:any)=>any` | Carriage return submit event callback | - | | onAutoSubmitFailed | (feedbacks: [IFormFeedback](https://core.formilyjs.org/api/models/form#iformfeedback)[]) => void | Carriage return submission verification failure event callback | - | ================================================ FILE: packages/next/docs/components/Form.zh-CN.md ================================================ # Form > FormProvider + FormLayout + form 标签的组合组件,可以帮助我们快速实现带回车提交的且能批量布局的表单 ## 使用案例 ```tsx import React from 'react' import { Input, Select, Form, FormItem, FormGrid, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { Field } from '@formily/react' const form = createForm() export default () => (
查询
) ``` ## Fusion 多语言 ```tsx import React from 'react' import { Input, Form, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { Field } from '@formily/react' import { ConfigProvider } from '@alifd/next' import enUS from '@alifd/next/lib/locale/en-us' const form = createForm() export default () => (
Submit
) ``` 注意:想要实现回车提交,我们在使用Submit组件的时候不能给其传onSubmit事件,否则回车提交会失效,这样做的目的是为了防止用户同时在多处写onSubmit事件监听器,处理逻辑不一致的话,提交时很难定位问题。 ## API 布局相关的 API 属性,我们参考 [FormLayout](./form-layout)即可,剩下是 Form 组件独有的 API 属性 | 属性名 | 类型 | 描述 | 默认值 | | ---------------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------- | ------ | | form | [Form](https://core.formilyjs.org/zh-CN/api/models/form) | Form 实例 | - | | component | string | 渲染组件,可以指定为自定义组件渲染 | `form` | | previewTextPlaceholder | ReactNode | 预览态占位符 | `N/A` | | onAutoSubmit | `(values:any)=>any` | 回车提交事件回调 | - | | onAutoSubmitFailed | (feedbacks: [IFormFeedback](https://core.formilyjs.org/zh-CN/api/models/form#iformfeedback)[]) => void | 回车提交校验失败事件回调 | - | ================================================ FILE: packages/next/docs/components/FormButtonGroup.md ================================================ # FormButtonGroup > Form button group layout component ## Common case ```tsx import React from 'react' import { FormButtonGroup, Submit, Reset, FormItem, Input, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const form = createForm() export default () => { return ( Submit Reset ) } ``` ## Suction bottom case ```tsx import React from 'react' import { FormButtonGroup, Submit, Reset, FormItem, FormLayout, Input, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const form = createForm() export default () => { return ( Submit Reset ) } ``` ## Suction bottom centering case ```tsx import React from 'react' import { FormButtonGroup, Submit, Reset, FormItem, FormLayout, Input, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const form = createForm() export default () => { return ( Submit Reset ) } ``` ## API ### FormButtonGroup > This component is mainly used to handle the button group gap | Property name | Type | Description | Default value | | ------------- | --------------------------- | ----------- | ------------- | | gutter | number | Gap size | 8px | | align | `'left'\|'center'\|'right'` | Alignment | `'left'` | ### FormButtonGroup.FormItem > This component is mainly used to deal with the alignment of the button group and the main form FormItem Refer to [FormItem](/components/form-item) property ### FormButtonGroup.Sticky > This component is mainly used to deal with the floating positioning problem of the button group | Property name | Type | Description | Default value | | ------------- | --------------------------- | ----------- | ------------- | | align | `'left'\|'center'\|'right'` | Alignment | `'left'` | ================================================ FILE: packages/next/docs/components/FormButtonGroup.zh-CN.md ================================================ # FormButtonGroup > 表单按钮组布局组件 ## 普通案例 ```tsx import React from 'react' import { FormButtonGroup, Submit, Reset, FormItem, Input, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const form = createForm() export default () => { return ( 提交 重置 ) } ``` ## 吸底案例 ```tsx import React from 'react' import { FormButtonGroup, Submit, Reset, FormItem, FormLayout, Input, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const form = createForm() export default () => { return ( 提交 重置 ) } ``` ## 吸底居中案例 ```tsx import React from 'react' import { FormButtonGroup, Submit, Reset, FormItem, FormLayout, Input, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const form = createForm() export default () => { return ( 提交 重置 ) } ``` ## API ### FormButtonGroup > 该组件主要用来处理按钮组间隙 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------------------------- | -------- | -------- | | gutter | number | 间隙大小 | 8px | | align | `'left'\|'center'\|'right'` | 对齐方式 | `'left'` | ### FormButtonGroup.FormItem > 该组件主要用来处理按钮组与主表单 FormItem 对齐问题 参考 [FormItem](/components/form-item) 属性 ### FormButtonGroup.Sticky > 该组件主要用来处理按钮组浮动定位问题 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------------------------- | -------- | -------- | | align | `'left'\|'center'\|'right'` | 对齐方式 | `'left'` | ================================================ FILE: packages/next/docs/components/FormCollapse.md ================================================ # FormCollapse > Folding panel, usually used in form scenes with high layout space requirements > > Note: Can only be used in Schema scenarios ## Markup Schema example ```tsx import React from 'react' import { FormCollapse, FormItem, Input, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, FormCollapse, Input, }, }) const form = createForm() const formCollapse = FormCollapse.createFormCollapse() export default () => { return ( Submit ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormCollapse, FormItem, Input, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, FormCollapse, Input, }, }) const form = createForm() const formCollapse = FormCollapse.createFormCollapse() const schema = { type: 'object', properties: { collapse: { type: 'void', title: 'Folding Panel', 'x-decorator': 'FormItem', 'x-component': 'FormCollapse', 'x-component-props': { formCollapse: '{{formCollapse}}', }, properties: { panel1: { type: 'void', 'x-component': 'FormCollapse.CollapsePanel', 'x-component-props': { title: 'A1', }, properties: { aaa: { type: 'string', title: 'AAA', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, panel2: { type: 'void', 'x-component': 'FormCollapse.CollapsePanel', 'x-component-props': { title: 'A2', }, properties: { bbb: { type: 'string', title: 'BBB', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, panel3: { type: 'void', 'x-component': 'FormCollapse.CollapsePanel', 'x-component-props': { title: 'A3', }, properties: { ccc: { type: 'string', title: 'CCC', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, }, }, }, } export default () => { return ( Submit ) } ``` ## API ### FormCollapse | Property name | Type | Description | Default value | | ------------- | ------------- | --------------------------------------------------------------- | ------------- | | formCollapse | IFormCollapse | Pass in the model created by createFormCollapse/useFormCollapse | | Other references https://fusion.design/pc/component/basic/collapse ### FormCollapse.CollapsePanel Reference https://fusion.design/pc/component/basic/collapse ### FormCollapse.createFormCollapse ```ts pure type ActiveKey = string | number type ActiveKeys = string | number | Array interface createFormCollapse { (defaultActiveKeys?: ActiveKeys): IFormCollpase } interface IFormCollapse { //Activate the primary key list activeKeys: ActiveKeys //Does the activation key exist? hasActiveKey(key: ActiveKey): boolean //Set the list of active primary keys setActiveKeys(keys: ActiveKeys): void //Add activation key addActiveKey(key: ActiveKey): void //Delete the active primary key removeActiveKey(key: ActiveKey): void //Switch to activate the main key toggleActiveKey(key: ActiveKey): void } ``` ================================================ FILE: packages/next/docs/components/FormCollapse.zh-CN.md ================================================ # FormCollapse > 折叠面板,通常用在布局空间要求较高的表单场景 > > 注意:只能用在 Schema 场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormCollapse, FormItem, Input, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, FormCollapse, Input, }, }) const form = createForm() const formCollapse = FormCollapse.createFormCollapse() export default () => { return ( 提交 ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormCollapse, FormItem, Input, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, FormCollapse, Input, }, }) const form = createForm() const formCollapse = FormCollapse.createFormCollapse() const schema = { type: 'object', properties: { collapse: { type: 'void', title: '折叠面板', 'x-decorator': 'FormItem', 'x-component': 'FormCollapse', 'x-component-props': { formCollapse: '{{formCollapse}}', }, properties: { panel1: { type: 'void', 'x-component': 'FormCollapse.CollapsePanel', 'x-component-props': { title: 'A1', }, properties: { aaa: { type: 'string', title: 'AAA', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, panel2: { type: 'void', 'x-component': 'FormCollapse.CollapsePanel', 'x-component-props': { title: 'A2', }, properties: { bbb: { type: 'string', title: 'BBB', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, panel3: { type: 'void', 'x-component': 'FormCollapse.CollapsePanel', 'x-component-props': { title: 'A3', }, properties: { ccc: { type: 'string', title: 'CCC', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, }, }, }, } export default () => { return ( 提交 ) } ``` ## API ### FormCollapse | 属性名 | 类型 | 描述 | 默认值 | | ------------ | ------------- | ---------------------------------------------------------- | ------ | | formCollapse | IFormCollapse | 传入通过 createFormCollapse/useFormCollapse 创建出来的模型 | | 其余参考 https://fusion.design/pc/component/basic/collapse ### FormCollapse.CollapsePanel 参考 https://fusion.design/pc/component/basic/collapse ### FormCollapse.createFormCollapse ```ts pure type ActiveKey = string | number type ActiveKeys = string | number | Array interface createFormCollapse { (defaultActiveKeys?: ActiveKeys): IFormCollpase } interface IFormCollapse { //激活主键列表 activeKeys: ActiveKeys //是否存在该激活主键 hasActiveKey(key: ActiveKey): boolean //设置激活主键列表 setActiveKeys(keys: ActiveKeys): void //添加激活主键 addActiveKey(key: ActiveKey): void //删除激活主键 removeActiveKey(key: ActiveKey): void //开关切换激活主键 toggleActiveKey(key: ActiveKey): void } ``` ================================================ FILE: packages/next/docs/components/FormDialog.md ================================================ # FormDialog > Pop-up form, mainly used in simple event to open the form scene ## Markup Schema example ```tsx import React from 'react' import { FormDialog, FormItem, Input, FormLayout } from '@formily/next' import { createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) export default () => { return ( ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormDialog, FormItem, Input, FormLayout } from '@formily/next' import { createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const schema = { type: 'object', properties: { aaa: { type: 'string', title: 'input box 1', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, bbb: { type: 'string', title: 'input box 2', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, ccc: { type: 'string', title: 'input box 3', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, ddd: { type: 'string', title: 'input box 4', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, } export default () => { return ( ) } ``` ## Pure JSX case ```tsx import React from 'react' import { FormDialog, FormItem, Input, FormLayout } from '@formily/next' import { Field } from '@formily/react' import { Button } from '@alifd/next' export default () => { return ( ) } ``` ## Use Fusion Context ```tsx import React from 'react' import { FormDialog, FormItem, Input, FormLayout } from '@formily/next' import { Field } from '@formily/react' import { Button, ConfigProvider } from '@alifd/next' export default () => { return ( ) } ``` ## API ### FormDialog ```ts pure import { IFormProps, Form } from '@formily/core' type FormDialogRenderer = | React.ReactElement | ((form: Form) => React.ReactElement) interface IFormDialog { forOpen( middleware: ( props: IFormProps, next: (props?: IFormProps) => Promise ) => any ): any //Middleware interceptor, can intercept Dialog to open forConfirm( middleware: (props: Form, next: (props?: Form) => Promise) => any ): any //Middleware interceptor, which can intercept Dialog confirmation forCancel( middleware: (props: Form, next: (props?: Form) => Promise) => any ): any //Middleware interceptor, can intercept Dialog to cancel //Open the pop-up window to receive form attributes, you can pass in initialValues/values/effects etc. open(props: IFormProps): Promise //return form data //Close the pop-up window close(): void } interface IDialogProps extends DialogProps { onOk?: (event: React.MouseEvent) => void | boolean // return false can prevent onOk onCancel?: (event: React.MouseEvent) => void | boolean // return false can prevent onCancel loadingText?: React.ReactText } interface FormDialog { (title: IDialogProps, id: string, renderer: FormDialogRenderer): IFormDialog (title: IDialogProps, renderer: FormDialogRenderer): IFormDialog (title: ModalTitle, id: string, renderer: FormDialogRenderer): IFormDialog (title: ModalTitle, renderer: FormDialogRenderer): IFormDialog } ``` `DialogProps` type definition reference fusion [Dialog API](https://fusion.design/pc/component/dialog?themeid=2#API) ### FormDialog.Footer No attributes, only child nodes are received ### FormDialog.Portal 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 ================================================ FILE: packages/next/docs/components/FormDialog.zh-CN.md ================================================ # FormDialog > 弹窗表单,主要用在简单的事件打开表单场景 ## Markup Schema 案例 ```tsx import React, { createContext, useContext } from 'react' import { FormDialog, FormItem, Input, FormLayout } from '@formily/next' import { createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const Context = createContext() const PortalId = '可以传,也可以不传的ID,默认是form-dialog' export default () => { return ( ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormDialog, FormItem, Input, FormLayout } from '@formily/next' import { createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const schema = { type: 'object', properties: { aaa: { type: 'string', title: '输入框1', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, bbb: { type: 'string', title: '输入框2', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, ccc: { type: 'string', title: '输入框3', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, ddd: { type: 'string', title: '输入框4', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, } export default () => { return ( ) } ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { FormDialog, FormItem, Input, FormLayout } from '@formily/next' import { Field } from '@formily/react' import { Button } from '@alifd/next' export default () => { return ( ) } ``` ## 使用 Fusion Context ```tsx import React from 'react' import { FormDialog, FormItem, Input, FormLayout } from '@formily/next' import { Field } from '@formily/react' import { Button, ConfigProvider } from '@alifd/next' export default () => { return ( ) } ``` ## API ### FormDialog ```ts pure import { IFormProps, Form } from '@formily/core' type FormDialogRenderer = | React.ReactElement | ((form: Form) => React.ReactElement) interface IFormDialog { forOpen( middleware: ( props: IFormProps, next: (props?: IFormProps) => Promise ) => any ): any //中间件拦截器,可以拦截Dialog打开 forConfirm( middleware: (props: Form, next: (props?: Form) => Promise) => any ): any //中间件拦截器,可以拦截Dialog确认 forCancel( middleware: (props: Form, next: (props?: Form) => Promise) => any ): any //中间件拦截器,可以拦截Dialog取消 //打开弹窗,接收表单属性,可以传入initialValues/values/effects etc. open(props: IFormProps): Promise //返回表单数据 //关闭弹窗 close(): void } interface IDialogProps extends DialogProps { onOk?: (event: React.MouseEvent) => void | boolean // return false can prevent onOk onCancel?: (event: React.MouseEvent) => void | boolean // return false can prevent onCancel loadingText?: React.ReactText } interface FormDialog { (title: IDialogProps, id: string, renderer: FormDialogRenderer): IFormDialog (title: IDialogProps, renderer: FormDialogRenderer): IFormDialog (title: ModalTitle, id: string, renderer: FormDialogRenderer): IFormDialog (title: ModalTitle, renderer: FormDialogRenderer): IFormDialog } ``` `DialogProps` 类型定义参考 fusion [Dialog API](https://fusion.design/pc/component/dialog?themeid=2#API) ### FormDialog.Footer 无属性,只接收子节点 ### FormDialog.Portal 接收可选的 id 属性,默认值为`form-dialog`,如果一个应用存在多个 prefixCls,不同区域的弹窗内部 prefixCls 不一样,那推荐指定 id 为区域级 id ================================================ FILE: packages/next/docs/components/FormDrawer.md ================================================ # FormDrawer > Drawer form, mainly used in simple event to open form scene ## Markup Schema example ```tsx import React from 'react' import { FormDrawer, FormItem, Input, Submit, Reset, FormButtonGroup, FormLayout, } from '@formily/next' import { createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) export default () => { return ( ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormDrawer, FormItem, Input, Submit, Reset, FormButtonGroup, FormLayout, } from '@formily/next' import { createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const schema = { type: 'object', properties: { aaa: { type: 'string', title: 'input box 1', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, bbb: { type: 'string', title: 'input box 2', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, ccc: { type: 'string', title: 'input box 3', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, ddd: { type: 'string', title: 'input box 4', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, } export default () => { return ( ) } ``` ## Pure JSX case ```tsx import React from 'react' import { FormDrawer, FormItem, Input, Submit, Reset, FormButtonGroup, FormLayout, } from '@formily/next' import { Field } from '@formily/react' import { Button } from '@alifd/next' export default () => { return ( ) } ``` ## Use Fusion Context ```tsx import React from 'react' import { FormDrawer, FormItem, Input, Submit, Reset, FormButtonGroup, FormLayout, } from '@formily/next' import { Field } from '@formily/react' import { Button, ConfigProvider } from '@alifd/next' export default () => { return ( ) } ``` ## API ### FormDrawer ```ts pure import { IFormProps, Form } from '@formily/core' type FormDrawerRenderer = | React.ReactElement | ((form: Form) => React.ReactElement) interface IFormDrawer { forOpen( middleware: ( props: IFormProps, next: (props?: IFormProps) => Promise ) => any ): any //Middleware interceptor, can intercept Drawer to open //Open the pop-up window to receive form attributes, you can pass in initialValues/values/effects etc. open(props: IFormProps): Promise //return form data //Close the pop-up window close(): void } interface IDrawerProps extends DrawerProps { onClose?: (reason: string, e: React.MouseEvent) => void | boolean // return false can prevent onClose loadingText?: React.ReactNode } interface FormDrawer { (title: IDrawerProps, id: string, renderer: FormDrawerRenderer): IFormDrawer (title: IDrawerProps, renderer: FormDrawerRenderer): IFormDrawer (title: ModalTitle, id: string, renderer: FormDrawerRenderer): IFormDrawer (title: ModalTitle, renderer: FormDrawerRenderer): IFormDrawer } ``` `DrawerProps` type definition reference ant design [Drawer API](https://fusion.design/pc/component/drawer?themeid=2#API) ### FormDrawer.Footer No attributes, only child nodes are received ### FormDrawer.Portal Receive an optional id attribute, the default value is `form-drawer`, 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 ================================================ FILE: packages/next/docs/components/FormDrawer.zh-CN.md ================================================ # FormDrawer > 抽屉表单,主要用在简单的事件打开表单场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormDrawer, FormItem, Input, Submit, Reset, FormButtonGroup, FormLayout, } from '@formily/next' import { createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) export default () => { return ( ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormDrawer, FormItem, Input, Submit, Reset, FormButtonGroup, FormLayout, } from '@formily/next' import { createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, Input, }, }) const schema = { type: 'object', properties: { aaa: { type: 'string', title: '输入框1', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, bbb: { type: 'string', title: '输入框2', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, ccc: { type: 'string', title: '输入框3', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, ddd: { type: 'string', title: '输入框4', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, } export default () => { return ( ) } ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { FormDrawer, FormItem, Input, Submit, Reset, FormButtonGroup, FormLayout, } from '@formily/next' import { Field } from '@formily/react' import { Button } from '@alifd/next' export default () => { return ( ) } ``` ## 使用 Fusion Context ```tsx import React from 'react' import { FormDrawer, FormItem, Input, Submit, Reset, FormButtonGroup, FormLayout, } from '@formily/next' import { Field } from '@formily/react' import { Button, ConfigProvider } from '@alifd/next' export default () => { return ( ) } ``` ## API ### FormDrawer ## API ### FormDrawer ```ts pure import { IFormProps, Form } from '@formily/core' type FormDrawerRenderer = | React.ReactElement | ((form: Form) => React.ReactElement) interface IFormDrawer { forOpen( middleware: ( props: IFormProps, next: (props?: IFormProps) => Promise ) => any ): any //中间件拦截器,可以拦截Drawer打开 //打开弹窗,接收表单属性,可以传入initialValues/values/effects etc. open(props: IFormProps): Promise //返回表单数据 //关闭弹窗 close(): void } interface IDrawerProps extends DrawerProps { onClose?: (reason: string, e: React.MouseEvent) => void | boolean // return false can prevent onClose loadingText?: React.ReactNode } interface FormDrawer { (title: IDrawerProps, id: string, renderer: FormDrawerRenderer): IFormDrawer (title: IDrawerProps, renderer: FormDrawerRenderer): IFormDrawer (title: ModalTitle, id: string, renderer: FormDrawerRenderer): IFormDrawer (title: ModalTitle, renderer: FormDrawerRenderer): IFormDrawer } ``` `DrawerProps` 类型定义参考 fusion [Drawer API](https://fusion.design/pc/component/drawer?themeid=2#API) ### FormDrawer.Footer 无属性,只接收子节点 ### FormDrawer.Portal 接收可选的 id 属性,默认值为`form-drawer`,如果一个应用存在多个 prefixCls,不同区域的弹窗内部 prefixCls 不一样,那推荐指定 id 为区域级 id ================================================ FILE: packages/next/docs/components/FormGrid.md ================================================ # FormGrid > FormGrid component ## Markup Schema example ```tsx import React from 'react' import { FormItem, Input, FormGrid } from '@formily/next' import { FormProvider, createSchemaField } from '@formily/react' import { createForm } from '@formily/core' const SchemaField = createSchemaField({ components: { FormItem, Input, FormGrid, }, }) const form = createForm() export default () => { return ( ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormItem, Input, FormGrid } from '@formily/next' import { FormProvider, createSchemaField } from '@formily/react' import { createForm } from '@formily/core' const SchemaField = createSchemaField({ components: { FormItem, Input, FormGrid, }, }) const form = createForm() const schema = { type: 'object', properties: { grid: { type: 'void', 'x-component': 'FormGrid', 'x-component-props': { minColumns: [4, 6, 10], }, properties: { aaa: { type: 'string', title: 'AAA', 'x-decorator': 'FormItem', 'x-component': 'Input', }, bbb: { type: 'string', title: 'BBB', 'x-decorator': 'FormItem', 'x-component': 'Input', }, ccc: { type: 'string', title: 'CCC', 'x-decorator': 'FormItem', 'x-component': 'Input', }, ddd: { type: 'string', title: 'DDD', 'x-decorator': 'FormItem', 'x-component': 'Input', }, eee: { type: 'string', title: 'EEE', 'x-decorator': 'FormItem', 'x-component': 'Input', }, fff: { type: 'string', title: 'FFF', 'x-decorator': 'FormItem', 'x-component': 'Input', }, ggg: { type: 'string', title: 'GGG', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, }, } export default () => { return ( ) } ``` ## Native case ```tsx import React from 'react' import { FormGrid } from '@formily/next' const { GridColumn } = FormGrid const Cell = ({ children }) => { return (
{children}
) } export default () => { return (

maxColumns 3 + minColumns 2

1 2 3 4 5 6

maxColumns 3

1 2 3 4 5 6

minColumns 2

1 2 3 4 5 6

Null

1 2 3 4 5 6

minWidth 150 +maxColumns 3

1 2 3 4 5 6

maxWidth 120+minColumns 2

1 2 3 4 5 6

maxWidth 120 + gridSpan -1

1 2 3
) } ``` ## Query Form case ```tsx import React, { useMemo, Fragment } from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormProvider, observer } from '@formily/react' import { Form, Input, Select, DatePicker, FormItem, FormGrid, Submit, Reset, FormButtonGroup, } from '@formily/next' const useCollapseGrid = (maxRows: number) => { const grid = useMemo( () => FormGrid.createFormGrid({ maxColumns: 4, maxWidth: 240, maxRows: maxRows, shouldVisible: (node, grid) => { if (node.index === grid.childSize - 1) return true if (grid.maxRows === Infinity) return true return node.shadowRow < maxRows + 1 }, }), [] ) const expanded = grid.maxRows === Infinity const realRows = grid.shadowRows const computeRows = grid.fullnessLastColumn ? grid.shadowRows - 1 : grid.shadowRows const toggle = () => { if (grid.maxRows === Infinity) { grid.maxRows = maxRows } else { grid.maxRows = Infinity } } const takeType = () => { if (realRows < maxRows + 1) return 'incomplete-wrap' if (computeRows > maxRows) return 'collapsible' return 'complete-wrap' } return { grid, expanded, toggle, type: takeType(), } } const QueryForm: React.FC = observer((props) => { const { grid, expanded, toggle, type } = useCollapseGrid(1) const renderActions = () => { return ( Query Reset ) } const renderButtonGroup = () => { if (type === 'incomplete-wrap') { return ( {renderActions()} ) } if (type === 'collapsible') { return ( { e.preventDefault() toggle() }} > {expanded ? 'Fold' : 'UnFold'} {renderActions()} ) } return ( {renderActions()} ) } return (
{props.children} {renderButtonGroup()}
) }) const SchemaField = createSchemaField({ components: { QueryForm, Input, Select, DatePicker, FormItem, }, }) export default () => { const form = useMemo(() => createForm(), []) return ( ) } ``` ## API ### FormGrid | Property name | Type | Description | Default value | | ------------- | ---------------------- | --------------------------------------------------------------------------------- | ----------------- | | minWidth | `number \| number[]` | Minimum element width | 100 | | maxWidth | `number \| number[]` | Maximum element width | - | | minColumns | `number \| number[]` | Minimum number of columns | 0 | | maxColumns | `number \| number[]` | Maximum number of columns | - | | breakpoints | number[] | Container size breakpoints | `[720,1280,1920]` | | columnGap | number | Column spacing | 8 | | rowGap | number | Row spacing | 4 | | colWrap | boolean | Wrap | true | | strictAutoFit | boolean | Is width strictly limited by maxWidth | false | | shouldVisible | `(node,grid)=>boolean` | Whether to show the current node | `()=>true` | | grid | `Grid` | Grid instance passed in from outside, used to implement more complex layout logic | - | note: - minWidth takes priority over minColumn - maxWidth has priority over maxColumn - The array format of minWidth/maxWidth/minColumns/maxColumns represents the mapping with the breakpoint array ### FormGrid.GridColumn | Property name | Type | Description | Default value | | ------------- | ------ | ------------------------------------------------------------------------------------------------------------------------ | ------------- | | gridSpan | number | The number of columns spanned by the element, if it is -1, it will automatically fill the cell across columns in reverse | 1 | ### FormGrid.createFormGrid Read the Grid instance from the context ```ts interface createFormGrid { (props: IGridProps): Grid } ``` - IGridProps reference FormGrid properties - Grid instance attribute method reference https://github.com/alibaba/formily/tree/formily_next/packages/grid ### FormGrid.useFormGrid Read the Grid instance from the context ```ts interface useFormGrid { (): Grid } ``` - Grid instance attribute method reference https://github.com/alibaba/formily/tree/formily_next/packages/grid ================================================ FILE: packages/next/docs/components/FormGrid.zh-CN.md ================================================ # FormGrid > FormGrid 组件 ## Markup Schema 案例 ```tsx import React from 'react' import { FormItem, Input, FormGrid } from '@formily/next' import { FormProvider, createSchemaField } from '@formily/react' import { createForm } from '@formily/core' const SchemaField = createSchemaField({ components: { FormItem, Input, FormGrid, }, }) const form = createForm() export default () => { return ( ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormItem, Input, FormGrid } from '@formily/next' import { FormProvider, createSchemaField } from '@formily/react' import { createForm } from '@formily/core' const SchemaField = createSchemaField({ components: { FormItem, Input, FormGrid, }, }) const form = createForm() const schema = { type: 'object', properties: { grid: { type: 'void', 'x-component': 'FormGrid', 'x-component-props': { minColumns: [4, 6, 10], }, properties: { aaa: { type: 'string', title: 'AAA', 'x-decorator': 'FormItem', 'x-component': 'Input', }, bbb: { type: 'string', title: 'BBB', 'x-decorator': 'FormItem', 'x-component': 'Input', }, ccc: { type: 'string', title: 'CCC', 'x-decorator': 'FormItem', 'x-component': 'Input', }, ddd: { type: 'string', title: 'DDD', 'x-decorator': 'FormItem', 'x-component': 'Input', }, eee: { type: 'string', title: 'EEE', 'x-decorator': 'FormItem', 'x-component': 'Input', }, fff: { type: 'string', title: 'FFF', 'x-decorator': 'FormItem', 'x-component': 'Input', }, ggg: { type: 'string', title: 'GGG', 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, }, } export default () => { return ( ) } ``` ## 原生 案例 ```tsx import React from 'react' import { FormGrid } from '@formily/next' const { GridColumn } = FormGrid const Cell = ({ children }) => { return (
{children}
) } export default () => { return (

maxColumns 3 + minColumns 2

1 2 3 4 5 6

maxColumns 3

1 2 3 4 5 6

minColumns 2

1 2 3 4 5 6

Null

1 2 3 4 5 6

minWidth 150 +maxColumns 3

1 2 3 4 5 6

maxWidth 120+minColumns 2

1 2 3 4 5 6

maxWidth 120 + gridSpan -1

1 2 3
) } ``` ## 查询表单实现案例 ```tsx import React, { useMemo, Fragment } from 'react' import { createForm } from '@formily/core' import { createSchemaField, FormProvider, observer } from '@formily/react' import { Form, Input, Select, DatePicker, FormItem, FormGrid, Submit, Reset, FormButtonGroup, } from '@formily/next' const useCollapseGrid = (maxRows: number) => { const grid = useMemo( () => FormGrid.createFormGrid({ maxColumns: 4, maxWidth: 240, maxRows: maxRows, shouldVisible: (node, grid) => { if (node.index === grid.childSize - 1) return true if (grid.maxRows === Infinity) return true return node.shadowRow < maxRows + 1 }, }), [] ) const expanded = grid.maxRows === Infinity const realRows = grid.shadowRows const computeRows = grid.fullnessLastColumn ? grid.shadowRows - 1 : grid.shadowRows const toggle = () => { if (grid.maxRows === Infinity) { grid.maxRows = maxRows } else { grid.maxRows = Infinity } } const takeType = () => { if (realRows < maxRows + 1) return 'incomplete-wrap' if (computeRows > maxRows) return 'collapsible' return 'complete-wrap' } return { grid, expanded, toggle, type: takeType(), } } const QueryForm: React.FC = observer((props) => { const { grid, expanded, toggle, type } = useCollapseGrid(1) const renderActions = () => { return ( 查询 重置 ) } const renderButtonGroup = () => { if (type === 'incomplete-wrap') { return ( {renderActions()} ) } if (type === 'collapsible') { return ( { e.preventDefault() toggle() }} > {expanded ? '收起' : '展开'} {renderActions()} ) } return ( {renderActions()} ) } return (
{props.children} {renderButtonGroup()}
) }) const SchemaField = createSchemaField({ components: { QueryForm, Input, Select, DatePicker, FormItem, }, }) export default () => { const form = useMemo(() => createForm(), []) return ( ) } ``` ## API ### FormGrid | 属性名 | 类型 | 描述 | 默认值 | | ------------- | ---------------------- | -------------------------------------------------------------- | ----------------- | | minWidth | `number \| number[]` | 元素最小宽度 | 100 | | maxWidth | `number \| number[]` | 元素最大宽度 | - | | minColumns | `number \| number[]` | 最小列数 | 0 | | maxColumns | `number \| number[]` | 最大列数 | - | | breakpoints | number[] | 容器尺寸断点 | `[720,1280,1920]` | | columnGap | number | 列间距 | 8 | | rowGap | number | 行间距 | 4 | | colWrap | boolean | 自动换行 | true | | strictAutoFit | boolean | GridItem 宽度是否严格受限于 maxWidth,不受限的话会自动占满容器 | false | | shouldVisible | `(node,grid)=>boolean` | 是否需要显示当前节点 | `()=>true` | | grid | `Grid` | 外部传入 Grid 实例,用于实现更复杂的布局逻辑 | - | 注意: - minWidth 生效优先级高于 minColumn - maxWidth 优先级高于 maxColumn - minWidth/maxWidth/minColumns/maxColumns 的数组格式代表与断点数组映射 ### FormGrid.GridColumn | 属性名 | 类型 | 描述 | 默认值 | | -------- | ------ | ---------------------------------------------------- | ------ | | gridSpan | number | 元素所跨列数,如果为-1,那么会自动反向跨列填补单元格 | 1 | ### FormGrid.createFormGrid 从上下文中读取 Grid 实例 ```ts interface createFormGrid { (props: IGridProps): Grid } ``` - IGridProps 参考 FormGrid 属性 - Grid 实例属性方法参考 https://github.com/alibaba/formily/tree/formily_next/packages/grid ### FormGrid.useFormGrid 从上下文中读取 Grid 实例 ```ts interface useFormGrid { (): Grid } ``` - Grid 实例属性方法参考 https://github.com/alibaba/formily/tree/formily_next/packages/grid ================================================ FILE: packages/next/docs/components/FormItem.md ================================================ # FormItem > The brand new FormItem component, compared to Fusion Next’s FormItem, it supports more functions. At the same time, it is positioned as a pure style component and does not manage form status, so it will be lighter and more convenient for customization ## Markup Schema example ```tsx import React from 'react' import { Input, Select, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, Select, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JOSN Schema case ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { input: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { style: { width: 240, }, }, }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## Commonly used attribute cases ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, FormItem, NumberPicker, Switch, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const Title = (props) =>

{props.text}

const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, NumberPicker, Switch, Radio, FormItem, Title, }, }) const form = createForm() export default () => { return ( ) } ``` ## Borderless case Set to remove the component border ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, FormItem, NumberPicker, Switch, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const Title = (props) =>

{props.text}

const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, NumberPicker, Switch, Radio, FormItem, Title, }, }) const form = createForm() export default () => { return ( ) } ``` ## Embedded mode case Set the form component to inline mode ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, FormItem, NumberPicker, Switch, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const Title = (props) =>

{props.text}

const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, NumberPicker, Switch, Radio, FormItem, Title, }, }) const form = createForm() export default () => { return ( ) } ``` ## Feedback Customization Case The button for specifying feedback can be passed in through `feedbackIcon` ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, TimePicker, FormItem, FormLayout, NumberPicker, Switch, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { CheckCircleFilled, LoadingOutlined } from '@ant-design/icons' const Title = (props) =>

{props.text}

const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, TimePicker, NumberPicker, Switch, Radio, FormItem, Title, FormLayout, }, }) const form = createForm() export default () => { return ( , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> ) } ``` ## Size control case ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, FormItem, NumberPicker, Switch, } from '@formily/next' import { createForm, onFieldChange } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const Div = (props) =>
const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, NumberPicker, Switch, Radio, FormItem, Div, }, }) const form = createForm({ values: { size: 'default', }, effects: () => { onFieldChange('size', ['value'], (field, form) => { form.setFieldState('sizeWrap.*', (state) => { if (state.decorator[1]) { state.decorator[1].size = field.value } }) }) }, }) export default () => { return ( ) } ``` ## API ### FormItem | Property name | Type | Description | Default value | | -------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ------------- | | label | ReactNode | label | - | | style | CSSProperties | Style | - | | labelStyle | CSSProperties | Label style | - | | wrapperStyle | CSSProperties | Component container style | - | | className | string | Component style class name | - | | colon | boolean | colon | true | | tooltip | ReactNode | Question mark prompt | - | | tooltipLayout | `"icon" \| "text"` | Ask the prompt layout | `"icon"` | | tooltipIcon | ReactNode | Ask the prompt icon | `?` | | labelAlign | `"left"` \| `"right"` | Label text alignment | `"right"` | | labelWrap | boolean | Label change, otherwise an ellipsis appears, hover has tooltip | false | | labelWidth | `number \| string` | Label fixed width | - | | wrapperWidth | `number \| string` | Content fixed width | - | | labelCol | number | The number of columns occupied by the label grid, and the number of content columns add up to 24 | - | | wrapperCol | number | The number of columns occupied by the content grid, and the number of label columns add up to 24 | - | | wrapperAlign | `"left"` \| `"right"` | Content text alignment ⻬ | `"left"` | | wrapperWrap | boolean | Change the content, otherwise an ellipsis appears, and hover has tooltip | false | | fullness | boolean | fullness | true | | addonBefore | ReactNode | Prefix content | - | | addonAfter | ReactNode | Suffix content | - | | size | `"small"` \| `"default"` \| `"large"` | 尺⼨ | - | | inset | boolean | Is it an inline layout | false | | extra | ReactNode | Extended description script | - | | feedbackText | ReactNode | Feedback Case | - | | feedbackLayout | `"loose"` \| `"terse"` \| `"popover" \| "none"` | Feedback layout | - | | feedbackStatus | `"error"` \| `"warning"` \| `"success"` \| `"pending"` | Feedback layout | - | | feedbackIcon | ReactNode | Feedback icon | - | | asterisk | boolean | Asterisk reminder | - | | gridSpan | number | Grid layout occupies width | - | | bordered | boolean | Is there a border | - | ### FormItem.BaseItem Pure style components, the properties are the same as FormItem, and Formily Core does not do state bridging. It is mainly used for scenarios that need to rely on the style layout capabilities of FormItem but do not want to access the Field state. ================================================ FILE: packages/next/docs/components/FormItem.zh-CN.md ================================================ # FormItem > 全新的 FormItem 组件,相比于 Fusion Next 的 FormItem,它支持的功能更多,同时它的定位是纯样式组件,不管理表单状态,所以也会更轻量,更方便定制 ## Markup Schema 案例 ```tsx import React from 'react' import { Input, Select, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, Select, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JOSN Schema 案例 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { input: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { style: { width: 240, }, }, }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## 常用属性案例 ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, FormItem, NumberPicker, Switch, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const Title = (props) =>

{props.text}

const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, NumberPicker, Switch, Radio, FormItem, Title, }, }) const form = createForm() export default () => { return ( ) } ``` ## 无边框案例 设置去除组件边框 ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, FormItem, NumberPicker, Switch, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const Title = (props) =>

{props.text}

const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, NumberPicker, Switch, Radio, FormItem, Title, }, }) const form = createForm() export default () => { return ( ) } ``` ## 内嵌模式案例 设置表单组件为内嵌模式 ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, FormItem, NumberPicker, Switch, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const Title = (props) =>

{props.text}

const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, NumberPicker, Switch, Radio, FormItem, Title, }, }) const form = createForm() export default () => { return ( ) } ``` ## 反馈信息定制案例 可通过 `feedbackIcon` 传入指定反馈的按钮 ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, TimePicker, FormItem, FormLayout, NumberPicker, Switch, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { CheckCircleFilled, LoadingOutlined } from '@ant-design/icons' const Title = (props) =>

{props.text}

const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, TimePicker, NumberPicker, Switch, Radio, FormItem, Title, FormLayout, }, }) const form = createForm() export default () => { return ( , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> , }} /> ) } ``` ## 尺寸控制案例 ```tsx import React from 'react' import { Input, Radio, TreeSelect, Cascader, Select, DatePicker, FormItem, NumberPicker, Switch, } from '@formily/next' import { createForm, onFieldChange } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const Div = (props) =>
const SchemaField = createSchemaField({ components: { Input, Select, Cascader, TreeSelect, DatePicker, NumberPicker, Switch, Radio, FormItem, Div, }, }) const form = createForm({ values: { size: 'default', }, effects: () => { onFieldChange('size', ['value'], (field, form) => { form.setFieldState('sizeWrap.*', (state) => { if (state.decorator[1]) { state.decorator[1].size = field.value } }) }) }, }) export default () => { return ( ) } ``` ## API ### FormItem | 属性名 | 类型 | 描述 | 默认值 | | -------------- | ------------------------------------------------------ | ------------------------------------------- | --------- | | label | ReactNode | 标签 | - | | style | CSSProperties | 样式 | - | | labelStyle | CSSProperties | 标签样式 | - | | wrapperStyle | CSSProperties | 组件容器样式 | - | | className | string | 组件样式类名 | - | | colon | boolean | 冒号 | true | | tooltip | ReactNode | 问号提示 | - | | tooltipLayout | `"icon" \| "text"` | 问号提示布局 | `"icon"` | | tooltipIcon | ReactNode | 问号提示图标 | `?` | | labelAlign | `"left"` \| `"right"` | 标签文本对齐方式 | `"right"` | | labelWrap | boolean | 标签换⾏,否则出现省略号,hover 有 tooltip | false | | labelWidth | `number \| string` | 标签固定宽度 | - | | wrapperWidth | `number \| string` | 内容固定宽度 | - | | labelCol | number | 标签⽹格所占列数,和内容列数加起来总和为 24 | - | | wrapperCol | number | 内容⽹格所占列数,和标签列数加起来总和为 24 | - | | wrapperAlign | `"left"` \| `"right"` | 内容文本对齐方式⻬ | `"left"` | | wrapperWrap | boolean | 内容换⾏,否则出现省略号,hover 有 tooltip | false | | fullness | boolean | 内容撑满 | true | | addonBefore | ReactNode | 前缀内容 | - | | addonAfter | ReactNode | 后缀内容 | - | | size | `"small"` \| `"default"` \| `"large"` | 尺⼨ | - | | inset | boolean | 是否是内嵌布局 | false | | extra | ReactNode | 扩展描述⽂案 | - | | feedbackText | ReactNode | 反馈⽂案 | - | | feedbackLayout | `"loose"` \| `"terse"` \| `"popover" \| "none"` | 反馈布局 | - | | feedbackStatus | `"error"` \| `"warning"` \| `"success"` \| `"pending"` | 反馈布局 | - | | feedbackIcon | ReactNode | 反馈图标 | - | | asterisk | boolean | 星号提醒 | - | | gridSpan | number | ⽹格布局占宽 | - | | bordered | boolean | 是否有边框 | - | ### FormItem.BaseItem 纯样式组件,属性与 FormItem 一样,与 Formily Core 不做状态桥接,主要用于一些需要依赖 FormItem 的样式布局能力,但不希望接入 Field 状态的场景 ================================================ FILE: packages/next/docs/components/FormLayout.md ================================================ # FormLayout > Block-level layout batch control component, with the help of this component, we can easily control the layout mode of all FormItem components enclosed by FormLayout ## Markup Schema example ```tsx import React from 'react' import { Input, Select, FormItem, FormLayout } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, Select, FormItem, FormLayout, }, }) const form = createForm() export default () => ( 123
, }} x-component="Input" required /> ) ``` ## JSON Schema case ```tsx import React from 'react' import { Input, Select, FormItem, FormLayout } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, Select, FormItem, FormLayout, }, }) const schema = { type: 'object', properties: { layout: { type: 'void', 'x-component': 'FormLayout', 'x-component-props': { labelCol: 6, wrapperCol: 10, layout: 'vertical', }, properties: { input: { type: 'string', title: 'input box', required: true, 'x-decorator': 'FormItem', 'x-decorator-props': { tooltip:
123
, }, 'x-component': 'Input', }, select: { type: 'string', title: 'Select box', required: true, 'x-decorator': 'FormItem', 'x-component': 'Select', }, }, }, }, } const form = createForm() export default () => ( ) ``` ## Pure JSX case ```tsx import React from 'react' import { Input, Select, FormItem, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API | Property name | Type | Description | Default value | | -------------- | -------------------------------------------------------------------------------------- | ------------------------------------- | ------------- | | style | CSSProperties | Style | - | | className | string | class name | - | | colon | boolean | Is there a colon | true | | labelAlign | `'right' \| 'left' \| ('right' \| 'left')[]` | Label content alignment | - | | wrapperAlign | `'right' \| 'left' \| ('right' \| 'left')[]` | Component container content alignment | - | | labelWrap | boolean | Wrap label content | false | | labelWidth | number | Label width (px) | - | | wrapperWidth | number | Component container width (px) | - | | wrapperWrap | boolean | Component container wrap | false | | labelCol | `number \| number[]` | Label width (24 column) | - | | wrapperCol | `number \| number[]` | Component container width (24 column) | - | | fullness | boolean | Component container width 100% | false | | size | `'small' \|'default' \|'large'` | component size | default | | layout | `'vertical' \| 'horizontal' \| 'inline' \| ('vertical' \| 'horizontal' \| 'inline')[]` | layout mode | horizontal | | direction | `'rtl' \|'ltr'` | direction (not supported yet) | ltr | | inset | boolean | Inline layout | false | | shallow | boolean | shallow context transfer | true | | feedbackLayout | `'loose' \|'terse' \|'popover' \|'none'` | feedback layout | true | | tooltipLayout | `"icon" \| "text"` | Ask the prompt layout | `"icon"` | | tooltipIcon | ReactNode | Ask the prompt icon | - | | bordered | boolean | Is there a border | true | | breakpoints | number[] | Container size breakpoints | - | | gridColumnGap | number | Grid Column Gap | 8 | | gridRowGap | number | Grid Row Gap | 4 | | spaceGap | number | Space Gap | 8 | ================================================ FILE: packages/next/docs/components/FormLayout.zh-CN.md ================================================ # FormLayout > 区块级布局批量控制组件,借助该组件,我们可以轻松的控制被 FormLayout 圈住的所有 FormItem 组件的布局模式 ## Markup Schema 案例 ```tsx import React from 'react' import { Input, Select, FormItem, FormLayout } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, Select, FormItem, FormLayout, }, }) const form = createForm() export default () => ( 123
, }} x-component="Input" required />
) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Input, Select, FormItem, FormLayout } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, Select, FormItem, FormLayout, }, }) const schema = { type: 'object', properties: { layout: { type: 'void', 'x-component': 'FormLayout', 'x-component-props': { labelCol: 6, wrapperCol: 10, layout: 'vertical', }, properties: { input: { type: 'string', title: '输入框', required: true, 'x-decorator': 'FormItem', 'x-decorator-props': { tooltip:
123
, }, 'x-component': 'Input', }, select: { type: 'string', title: '选择框', required: true, 'x-decorator': 'FormItem', 'x-component': 'Select', }, }, }, }, } const form = createForm() export default () => ( ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Input, Select, FormItem, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API | 属性名 | 类型 | 描述 | 默认值 | | -------------- | ------------------------------------------------------------------------------------- | ----------------------- | ---------- | | style | CSSProperties | 样式 | - | | className | string | 类名 | - | | colon | boolean | 是否有冒号 | true | | labelAlign | `'right' \| 'left' \| ('right' \| 'left')[]` | 标签内容对齐 | - | | wrapperAlign | `'right' \| 'left' \| ('right' \| 'left')[]` | 组件容器内容对齐 | - | | labelWrap | boolean | 标签内容换行 | false | | labelWidth | number | 标签宽度(px) | - | | wrapperWidth | number | 组件容器宽度(px) | - | | wrapperWrap | boolean | 组件容器换行 | false | | labelCol | `number \| number[]` | 标签宽度(24 column) | - | | wrapperCol | `number \| number[]` | 组件容器宽度(24 column) | - | | fullness | boolean | 组件容器宽度 100% | false | | size | `'small' \| 'default' \| 'large'` | 组件尺寸 | default | | layout | `'vertical' \| 'horizontal' \| 'inline' \|('vertical' \| 'horizontal' \| 'inline')[]` | 布局模式 | horizontal | | direction | `'rtl' \| 'ltr'` | 方向(暂不支持) | ltr | | inset | boolean | 内联布局 | false | | shallow | boolean | 上下文浅层传递 | true | | feedbackLayout | `'loose' \| 'terse' \| 'popover' \| 'none'` | 反馈布局 | true | | tooltipLayout | `"icon" \| "text"` | 问号提示布局 | `"icon"` | | tooltipIcon | ReactNode | 问号提示图标 | - | | bordered | boolean | 是否有边框 | true | | breakpoints | number[] | 容器尺寸断点 | - | | gridColumnGap | number | 网格布局列间距 | 8 | | gridRowGap | number | 网格布局行间距 | 4 | | spaceGap | number | 弹性间距 | 8 | ================================================ FILE: packages/next/docs/components/FormStep.md ================================================ # FormStep > Step-by-step form components > > Note: This component can only be used in Schema scenarios ## Markup Schema example ```tsx import React from 'react' import { FormStep, FormItem, Input, FormButtonGroup } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, FormConsumer, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, FormStep, Input, }, }) const form = createForm() const formStep = FormStep.createFormStep() export default () => { return ( {() => ( )} ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormStep, FormItem, Input, FormButtonGroup } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, FormConsumer, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, FormStep, Input, }, }) const form = createForm() const formStep = FormStep.createFormStep() const schema = { type: 'object', properties: { step: { type: 'void', 'x-component': 'FormStep', 'x-component-props': { formStep: '{{formStep}}', }, properties: { step1: { type: 'void', 'x-component': 'FormStep.StepPane', 'x-component-props': { title: 'First Step', }, properties: { aaa: { type: 'string', title: 'AAA', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, step2: { type: 'void', 'x-component': 'FormStep.StepPane', 'x-component-props': { title: 'Second Step', }, properties: { bbb: { type: 'string', title: 'AAA', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, step3: { type: 'void', 'x-component': 'FormStep.StepPane', 'x-component-props': { title: 'The third step', }, properties: { ccc: { type: 'string', title: 'AAA', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, }, }, }, } export default () => { return ( {() => ( )} ) } ``` ## API ### FormStep | Property name | Type | Description | Default value | | ------------- | --------- | ------------------------------------------------------- | ------------- | | formStep | IFormStep | Pass in the model created by createFormStep/useFormStep | | Other references https://fusion.design/pc/component/basic/step ### FormStep.StepPane Refer to https://fusion.design/pc/component/basic/step Steps.Step properties ### FormStep.createFormStep ```ts pure import { Form } from '@formily/core' interface createFormStep { (current?: number): IFormStep } interface IFormTab { //Current index current: number //Whether to allow backwards allowNext: boolean //Whether to allow forward allowBack: boolean //Set the current index setCurrent(key: number): void //submit Form submit: Form['submit'] //backward next(): void //forward back(): void } ``` ================================================ FILE: packages/next/docs/components/FormStep.zh-CN.md ================================================ # FormStep > 分步表单组件 > > 注意:该组件只能用在 Schema 场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormStep, FormItem, Input, FormButtonGroup } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, FormConsumer, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, FormStep, Input, }, }) const form = createForm() const formStep = FormStep.createFormStep() export default () => { return ( {() => ( )} ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormStep, FormItem, Input, FormButtonGroup } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, FormConsumer, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, FormStep, Input, }, }) const form = createForm() const formStep = FormStep.createFormStep() const schema = { type: 'object', properties: { step: { type: 'void', 'x-component': 'FormStep', 'x-component-props': { formStep: '{{formStep}}', }, properties: { step1: { type: 'void', 'x-component': 'FormStep.StepPane', 'x-component-props': { title: '第一步', }, properties: { aaa: { type: 'string', title: 'AAA', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, step2: { type: 'void', 'x-component': 'FormStep.StepPane', 'x-component-props': { title: '第二步', }, properties: { bbb: { type: 'string', title: 'AAA', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, step3: { type: 'void', 'x-component': 'FormStep.StepPane', 'x-component-props': { title: '第三步', }, properties: { ccc: { type: 'string', title: 'AAA', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, }, }, }, } export default () => { return ( {() => ( )} ) } ``` ## API ### FormStep | 属性名 | 类型 | 描述 | 默认值 | | -------- | --------- | -------------------------------------------------- | ------ | | formStep | IFormStep | 传入通过 createFormStep/useFormStep 创建出来的模型 | | 其余参考 https://fusion.design/pc/component/basic/step ### FormStep.StepPane 参考 https://fusion.design/pc/component/basic/step Steps.Step 属性 ### FormStep.createFormStep ```ts pure import { Form } from '@formily/core' interface createFormStep { (current?: number): IFormStep } interface IFormTab { //当前索引 current: number //是否允许向后 allowNext: boolean //是否允许向前 allowBack: boolean //设置当前索引 setCurrent(key: number): void //提交表单 submit: Form['submit'] //向后 next(): void //向前 back(): void } ``` ================================================ FILE: packages/next/docs/components/FormTab.md ================================================ # FormTab > Tab form > > Note: This component is only applicable to Schema scenarios ## Markup Schema example ```tsx import React from 'react' import { FormTab, FormItem, Input, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, FormTab, Input, }, }) const form = createForm() const formTab = FormTab.createFormTab() export default () => { return ( Submit ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormTab, FormItem, Input, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, FormTab, Input, }, }) const form = createForm() const formTab = FormTab.createFormTab() const schema = { type: 'object', properties: { collapse: { type: 'void', 'x-component': 'FormTab', 'x-component-props': { formTab: '{{formTab}}', }, properties: { tab1: { type: 'void', 'x-component': 'FormTab.TabPane', 'x-component-props': { tab: 'A1', }, properties: { aaa: { type: 'string', title: 'AAA', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, tab2: { type: 'void', 'x-component': 'FormTab.TabPane', 'x-component-props': { tab: 'A2', }, properties: { bbb: { type: 'string', title: 'BBB', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, tab3: { type: 'void', 'x-component': 'FormTab.TabPane', 'x-component-props': { tab: 'A3', }, properties: { ccc: { type: 'string', title: 'CCC', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, }, }, }, } export default () => { return ( Submit ) } ``` ## API ### FormTab | Property name | Type | Description | Default value | | ------------- | -------- | ----------------------------------------------------- | ------------- | | formTab | IFormTab | Pass in the model created by createFormTab/useFormTab | | Other references https://fusion.design/pc/component/basic/tab ### FormTab.TabPane Refer to the Item property of https://fusion.design/pc/component/basic/tab ### FormTab.createFormTab ```ts pure type ActiveKey = string | number interface createFormTab { (defaultActiveKey?: ActiveKey): IFormTab } interface IFormTab { //Activate the primary key activeKey: ActiveKey //Set the activation key setActiveKey(key: ActiveKey): void } ``` ================================================ FILE: packages/next/docs/components/FormTab.zh-CN.md ================================================ # FormTab > 选项卡表单 > > 注意:该组件只适用于 Schema 场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormTab, FormItem, Input, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, FormTab, Input, }, }) const form = createForm() const formTab = FormTab.createFormTab() export default () => { return ( 提交 ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormTab, FormItem, Input, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, FormTab, Input, }, }) const form = createForm() const formTab = FormTab.createFormTab() const schema = { type: 'object', properties: { collapse: { type: 'void', 'x-component': 'FormTab', 'x-component-props': { formTab: '{{formTab}}', }, properties: { tab1: { type: 'void', 'x-component': 'FormTab.TabPane', 'x-component-props': { tab: 'A1', }, properties: { aaa: { type: 'string', title: 'AAA', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, tab2: { type: 'void', 'x-component': 'FormTab.TabPane', 'x-component-props': { tab: 'A2', }, properties: { bbb: { type: 'string', title: 'BBB', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, tab3: { type: 'void', 'x-component': 'FormTab.TabPane', 'x-component-props': { tab: 'A3', }, properties: { ccc: { type: 'string', title: 'CCC', 'x-decorator': 'FormItem', required: true, 'x-component': 'Input', }, }, }, }, }, }, } export default () => { return ( 提交 ) } ``` ## API ### FormTab | 属性名 | 类型 | 描述 | 默认值 | | ------- | -------- | ------------------------------------------------ | ------ | | formTab | IFormTab | 传入通过 createFormTab/useFormTab 创建出来的模型 | | 其余参考 https://fusion.design/pc/component/basic/tab ### FormTab.TabPane 参考 https://fusion.design/pc/component/basic/tab 的 Item 属性 ### FormTab.createFormTab ```ts pure type ActiveKey = string | number interface createFormTab { (defaultActiveKey?: ActiveKey): IFormTab } interface IFormTab { //激活主键 activeKey: ActiveKey //设置激活主键 setActiveKey(key: ActiveKey): void } ``` ================================================ FILE: packages/next/docs/components/Input.md ================================================ # Input > Text input box ## Markup Schema example ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { input: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { style: { width: 240, }, }, }, textarea: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Input.TextArea', 'x-component-props': { style: { width: 240, }, }, }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://fusion.design/pc/component/basic/input ================================================ FILE: packages/next/docs/components/Input.zh-CN.md ================================================ # Input > 文本输入框 ## Markup Schema 案例 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { input: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-component-props': { style: { width: 240, }, }, }, textarea: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'Input.TextArea', 'x-component-props': { style: { width: 240, }, }, }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://fusion.design/pc/component/basic/input ================================================ FILE: packages/next/docs/components/NumberPicker.md ================================================ # NumberPicker > Number input box ## Markup Schema example ```tsx import React from 'react' import { NumberPicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { NumberPicker, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { NumberPicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { NumberPicker, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { input: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', 'x-component-props': { style: { width: 240, }, }, }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { NumberPicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://fusion.design/pc/component/basic/number-picker ================================================ FILE: packages/next/docs/components/NumberPicker.zh-CN.md ================================================ # NumberPicker > 数字输入框 ## Markup Schema 案例 ```tsx import React from 'react' import { NumberPicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { NumberPicker, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { NumberPicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { NumberPicker, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { input: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'NumberPicker', 'x-component-props': { style: { width: 240, }, }, }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { NumberPicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://fusion.design/pc/component/basic/number-picker ================================================ FILE: packages/next/docs/components/Password.md ================================================ # Password > Password input box ## Markup Schema example ```tsx import React from 'react' import { Password, FormItem, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Password, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Password, FormItem, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Password, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { input: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Password', 'x-component-props': { checkStrength: true, }, }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Password, FormItem, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://fusion.design/pc/component/basic/input ================================================ FILE: packages/next/docs/components/Password.zh-CN.md ================================================ # Password > 密码输入框 ## Markup Schema 案例 ```tsx import React from 'react' import { Password, FormItem, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Password, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Password, FormItem, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Password, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { input: { type: 'string', title: '输入框', 'x-decorator': 'FormItem', 'x-component': 'Password', 'x-component-props': { checkStrength: true, }, }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Password, FormItem, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://fusion.design/pc/component/basic/input ================================================ FILE: packages/next/docs/components/PreviewText.md ================================================ # PreviewText > Reading state components, mainly used to implement the reading state of these components of class Input and DatePicker ## Simple use case ```tsx import React from 'react' import { PreviewText, FormItem, FormLayout } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, PreviewText, }, }) const form = createForm() export default () => { return ( ) } ``` ## Extended reading mode ```tsx import React from 'react' import { PreviewText, FormItem, FormButtonGroup, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, mapReadPretty, connect, createSchemaField, } from '@formily/react' import { Button, Input as NextInput } from '@alifd/next' const Input = connect(NextInput, mapReadPretty(PreviewText.Input)) const SchemaField = createSchemaField({ components: { Input, FormItem, PreviewText, }, }) const form = createForm() export default () => { return ( ) } ``` ## API ### PreviewText.Input Reference https://fusion.design/pc/component/basic/input ### PreviewText.Select Reference https://fusion.design/pc/component/basic/select ### PreviewText.TreeSelect Reference https://fusion.design/pc/component/basic/tree-select ### PreviewText.Cascader Reference https://fusion.design/pc/component/basic/cascader-select ### PreviewText.DatePicker Reference https://fusion.design/pc/component/basic/date-picker ### PreviewText.DateRangePicker Reference https://fusion.design/pc/component/basic/date-picker ### PreviewText.TimePicker Reference https://fusion.design/pc/component/basic/time-picker ### PreviewText.NumberPicker Reference https://fusion.design/pc/component/basic/number-picker ### PreviewText.Placeholder | Property name | Type | Description | Default value | | ------------- | ------ | ------------------- | ------------- | | value | stirng | Default placeholder | N/A | ### PreviewText.usePlaceholder ```ts pure interface usePlaceholder { (): string } ``` ================================================ FILE: packages/next/docs/components/PreviewText.zh-CN.md ================================================ # PreviewText > 阅读态组件,主要用来实现类 Input,类 DatePicker 这些组件的阅读态 ## 简单用例 ```tsx import React from 'react' import { PreviewText, FormItem, FormLayout } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, PreviewText, }, }) const form = createForm() export default () => { return ( ) } ``` ## 扩展阅读态 ```tsx import React from 'react' import { PreviewText, FormItem, FormButtonGroup, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, mapReadPretty, connect, createSchemaField, } from '@formily/react' import { Button, Input as NextInput } from '@alifd/next' const Input = connect(NextInput, mapReadPretty(PreviewText.Input)) const SchemaField = createSchemaField({ components: { Input, FormItem, PreviewText, }, }) const form = createForm() export default () => { return ( ) } ``` ## API ### PreviewText.Input 参考 https://fusion.design/pc/component/basic/input ### PreviewText.Select 参考 https://fusion.design/pc/component/basic/select ### PreviewText.TreeSelect 参考 https://fusion.design/pc/component/basic/tree-select ### PreviewText.Cascader 参考 https://fusion.design/pc/component/basic/cascader-select ### PreviewText.DatePicker 参考 https://fusion.design/pc/component/basic/date-picker ### PreviewText.DateRangePicker 参考 https://fusion.design/pc/component/basic/date-picker ### PreviewText.TimePicker 参考 https://fusion.design/pc/component/basic/time-picker ### PreviewText.NumberPicker 参考 https://fusion.design/pc/component/basic/number-picker ### PreviewText.Placeholder | 属性名 | 类型 | 描述 | 默认值 | | ------ | ------ | ---------- | ------ | | value | stirng | 缺省占位符 | N/A | ### PreviewText.usePlaceholder ```ts pure interface usePlaceholder { (): string } ``` ================================================ FILE: packages/next/docs/components/Radio.md ================================================ # Radio > Single selection box ## Markup Schema example ```tsx import React from 'react' import { Radio, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Radio, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Radio, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Radio, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { radio: { type: 'number', title: 'Single selection', enum: [ { label: 'Option 1', value: 1, }, { label: 'Option 2', value: 2, }, ], 'x-decorator': 'FormItem', 'x-component': 'Radio.Group', }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Radio, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://fusion.design/pc/component/basic/radio ================================================ FILE: packages/next/docs/components/Radio.zh-CN.md ================================================ # Radio > 单选框 ## Markup Schema 案例 ```tsx import React from 'react' import { Radio, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Radio, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Radio, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Radio, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { radio: { type: 'number', title: '单选', enum: [ { label: '选项1', value: 1, }, { label: '选项2', value: 2, }, ], 'x-decorator': 'FormItem', 'x-component': 'Radio.Group', }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Radio, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://fusion.design/pc/component/basic/radio ================================================ FILE: packages/next/docs/components/Reset.md ================================================ # Reset > Reset button ## Normal reset > Controls with default values cannot be cleared ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Reset } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( Reset ) ``` ## Force empty reset ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Reset } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( Reset ) ``` ## Reset and verify ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Reset } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( Reset ) ``` ## Force empty reset and verify ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Reset } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( Reset ) ``` ## API ### Reset Other API reference https://fusion.design/pc/component/basic/button | Property name | Type | Description | Default value | | ---------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | ------------- | | onClick | `(event: MouseEvent) => void \| boolean` | Click event, if it returns false, it can block resetting | - | | onResetValidateSuccess | (payload: any) => void | Reset validation success event | - | | onResetValidateFailed | (feedbacks: [IFormFeedback](https://core.formilyjs.org/api/models/form#iformfeedback)[]) => void | Reset validation failure event | - | ================================================ FILE: packages/next/docs/components/Reset.zh-CN.md ================================================ # Reset > 重置按钮 ## 普通重置 > 有默认值的控件无法被清空 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Reset } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( 重置 ) ``` ## 强制清空重置 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Reset } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( 重置 ) ``` ## 重置并校验 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Reset } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( 重置 ) ``` ## 强制清空重置并校验 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Reset } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( 重置 ) ``` ## API ### Reset 其余 API 参考 https://fusion.design/pc/component/basic/button | 属性名 | 类型 | 描述 | 默认值 | | ---------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------- | ------ | | onClick | `(event: MouseEvent) => void \| boolean` | 点击事件,如果返回 false 可以阻塞重置 | - | | onResetValidateSuccess | (payload: any) => void | 重置校验成功事件 | - | | onResetValidateFailed | (feedbacks: [IFormFeedback](https://core.formilyjs.org/zh-CN/api/models/form#iformfeedback)[]) => void | 重置校验失败事件 | - | ================================================ FILE: packages/next/docs/components/Select.md ================================================ # Select > Drop-down box components ## Markup Schema synchronization data source case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## Markup Schema Asynchronous Linkage Data Source Case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm, onFieldReact, FormPathPattern, Field } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: Field) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', }, { label: 'BBB', value: 'ccc', }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', }, { label: 'DDD', value: 'ddd', }, ]) } }, 1500) }) }) }, }) export default () => ( Submit ) ``` ## JSON Schema synchronization data source case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { select: { type: 'string', title: 'Select box', 'x-decorator': 'FormItem', 'x-component': 'Select', enum: [ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 }, ], 'x-component-props': { style: { width: 120, }, }, }, }, } export default () => ( Submit ) ``` ## JSON Schema asynchronous linkage data source case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const loadData = async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', }, { label: 'BBB', value: 'ccc', }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', }, { label: 'DDD', value: 'ddd', }, ]) } }, 1500) }) } const useAsyncDataSource = (service) => (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) } const form = createForm() const schema = { type: 'object', properties: { linkage: { type: 'string', title: 'Linkage selection box', enum: [ { label: 'Request 1', value: 1 }, { label: 'Request 2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 120, }, }, }, select: { type: 'string', title: 'Asynchronous selection box', 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 120, }, }, 'x-reactions': ['{{useAsyncDataSource(loadData)}}'], }, }, } export default () => ( Submit ) ``` ## Pure JSX synchronization data source case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## Pure JSX asynchronous linkage data source case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm, onFieldReact, FormPathPattern, FieldType, } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { action } from '@formily/reactive' const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: FieldType) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', }, { label: 'BBB', value: 'ccc', }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', }, { label: 'DDD', value: 'ddd', }, ]) } }, 1500) }) }) }, }) export default () => ( Submit ) ``` ## API Reference https://fusion.design/pc/component/basic/select ================================================ FILE: packages/next/docs/components/Select.zh-CN.md ================================================ # Select > 下拉框组件 ## Markup Schema 同步数据源案例 ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## Markup Schema 异步联动数据源案例 ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm, onFieldReact, FormPathPattern, Field } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: Field) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', }, { label: 'BBB', value: 'ccc', }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', }, { label: 'DDD', value: 'ddd', }, ]) } }, 1500) }) }) }, }) export default () => ( 提交 ) ``` ## JSON Schema 同步数据源案例 ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { select: { type: 'string', title: '选择框', 'x-decorator': 'FormItem', 'x-component': 'Select', enum: [ { label: '选项1', value: 1 }, { label: '选项2', value: 2 }, ], 'x-component-props': { style: { width: 120, }, }, }, }, } export default () => ( 提交 ) ``` ## JSON Schema 异步联动数据源案例 ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const loadData = async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', }, { label: 'BBB', value: 'ccc', }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', }, { label: 'DDD', value: 'ddd', }, ]) } }, 1500) }) } const useAsyncDataSource = (service) => (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) } const form = createForm() const schema = { type: 'object', properties: { linkage: { type: 'string', title: '联动选择框', enum: [ { label: '发请求1', value: 1 }, { label: '发请求2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 120, }, }, }, select: { type: 'string', title: '异步选择框', 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 120, }, }, 'x-reactions': ['{{useAsyncDataSource(loadData)}}'], }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 同步数据源案例 ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## 纯 JSX 异步联动数据源案例 ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm, onFieldReact, FormPathPattern, FieldType, } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { action } from '@formily/reactive' const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: FieldType) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', }, { label: 'BBB', value: 'ccc', }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', }, { label: 'DDD', value: 'ddd', }, ]) } }, 1500) }) }) }, }) export default () => ( 提交 ) ``` ## API 参考 https://fusion.design/pc/component/basic/select ================================================ FILE: packages/next/docs/components/SelectTable.md ================================================ # SelectTable > Optional table components ## Markup Schema single case ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, SelectTable, }, }) const form = createForm() export default () => { return ( Submit ) } ``` ## Markup Schema filter case ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, SelectTable, }, }) const form = createForm() export default () => { return ( Submit ) } ``` ## Markup Schema async data source case ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, SelectTable, }, }) const form = createForm() export default () => { const onSearch = (value) => { const field = form.query('selectTable').take() field.loading = true setTimeout(() => { field.setState({ dataSource: [ { key: '3', name: 'AAA' + value, description: 'aaa', }, { key: '4', name: 'BBB' + value, description: 'bbb', }, ], loading: false, }) }, 1500) } return ( Submit ) } ``` ## Markup Schema read-pretty case ```tsx import React from 'react' import { Form, FormItem, FormButtonGroup, Submit, SelectTable, } from '@formily/next' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, SelectTable, }, }) const form = createForm() export default () => { return (
Submit
) } ``` ## JSON Schema multiple case ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { SelectTable, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { selectTable: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'SelectTable', 'x-component-props': { hasBorder: false, mode: 'multiple', }, enum: [ { key: '1', name: 'title-1', description: 'description-1' }, { key: '2', name: 'Title-2', description: 'description-2' }, ], properties: { name: { title: 'Title', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '40%', }, }, description: { title: 'Description', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '60%', }, }, }, }, }, } export default () => ( Submit ) ``` ## JSON Schema custom filter case ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { SelectTable, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { selectTable: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'SelectTable', 'x-component-props': { hasBorder: false, showSearch: true, primaryKey: 'key', isTree: true, filterOption: (input, option) => option.description.toLowerCase().indexOf(input.toLowerCase()) >= 0, filterSort: (optionA, optionB) => optionA.description .toLowerCase() .localeCompare(optionB.description.toLowerCase()), optionAsValue: true, rowSelection: { checkStrictly: false, }, }, enum: [ { key: '1', name: 'title-1', description: 'A-description' }, { key: '2', name: 'title-2', description: 'X-description', children: [ { key: '2-1', name: 'title2-1', description: 'Y-description', children: [ { key: '2-1-1', name: 'title-2-1-1', description: 'Z-description', }, ], }, { key: '2-2', name: 'title2-2', description: 'YY-description', }, ], }, { key: '3', name: 'title-3', description: 'C-description' }, ], properties: { name: { title: 'Title', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '40%', }, }, description: { title: 'Description', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '60%', }, }, }, }, }, } export default () => ( Submit ) ``` ## JSON Schema async data source case ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { SelectTable, FormItem, }, }) const loadData = async (value) => { return new Promise((resolve) => { setTimeout(() => { resolve([ { key: '3', name: 'AAA' + value, description: 'aaa' }, { key: '4', name: 'BBB' + value, description: 'bbb' }, ]) }, 1500) }) } const useAsyncDataSource = (service, field) => (value) => { field.loading = true service(value).then((data) => { field.setState({ dataSource: data, loading: false, }) }) } const form = createForm() const schema = { type: 'object', properties: { selectTable: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'SelectTable', 'x-component-props': { hasBorder: false, showSearch: true, filterOption: false, onSearch: '{{useAsyncDataSource(loadData,$self)}}', }, enum: [ { key: '1', name: 'title-1', description: 'description-1' }, { key: '2', name: 'title-2', description: 'description-2' }, ], properties: { name: { title: 'Title', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '40%', }, }, description: { title: 'Description', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '60%', }, }, }, }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API ### SelectTable | Property name | Type | Description | Default value | | ------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | | mode | `'multiple' \| 'single'` | Set mode of SelectTable | `'multiple'` | | valueType | `'all' \| 'parent' \| 'child' \| 'path'` | value type, Only applies when checkStrictly is set to `false` | `'all'` | | optionAsValue | boolean | use `option` as value, Only applies when valueType is not set to `'path'` | false | | showSearch | boolean | show `Search` component | false | | searchProps | object | `Search` component props | - | | primaryKey | `string \| (record) => string` | Row's unique key | `'key'` | | filterOption | `boolean \| (inputValue, option) => boolean` | If true, filter options by input, if function, filter options against it. The function will receive two arguments, `inputValue` and `option`, if the function returns true, the option will be included in the filtered set; Otherwise, it will be excluded | | filterSort | (optionA, optionB) => number | Sort function for search options sorting, see Array.sort's compareFunction | - | | onSearch | Callback function that is fired when input changed | (inputValue) => void | - | `TableProps` type definition reference fusion https://fusion.design/pc/component/basic/table ### rowSelection | Property name | Type | Description | Default value | | ------------- | ------- | -------------------------------------------------------------------------- | ------------- | | checkStrictly | boolean | Check table row precisely; parent row and children rows are not associated | true | `rowSelectionProps` type definition reference fusion https://fusion.design/pc/component/basic/table rowSelection ### SelectTable.Column `ColumnProps` type definition reference fusion https://fusion.design/pc/component/basic/table Table.Column ================================================ FILE: packages/next/docs/components/SelectTable.zh-CN.md ================================================ # SelectTable > 表格选择组件 ## Markup Schema 单选案例 ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, SelectTable, }, }) const form = createForm() export default () => { return ( 提交 ) } ``` ## Markup Schema 筛选案例 ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, SelectTable, }, }) const form = createForm() export default () => { return ( 提交 ) } ``` ## Markup Schema 异步数据源案例 ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, SelectTable, }, }) const form = createForm() export default () => { const onSearch = (value) => { const field = form.query('selectTable').take() field.loading = true setTimeout(() => { field.setState({ dataSource: [ { key: '3', name: 'AAA' + value, description: 'aaa', }, { key: '4', name: 'BBB' + value, description: 'bbb', }, ], loading: false, }) }, 1500) } return ( 提交 ) } ``` ## Markup Schema 阅读态案例 ```tsx import React from 'react' import { Form, FormItem, FormButtonGroup, Submit, SelectTable, } from '@formily/next' import { createForm } from '@formily/core' import { createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, SelectTable, }, }) const form = createForm() export default () => { return (
提交
) } ``` ## JSON Schema 多选案例 ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { SelectTable, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { selectTable: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'SelectTable', 'x-component-props': { hasBorder: false, mode: 'multiple', }, enum: [ { key: '1', name: '标题1', description: '描述1' }, { key: '2', name: '标题2', description: '描述2' }, ], properties: { name: { title: '标题', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '40%', }, }, description: { title: '描述', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '60%', }, }, }, }, }, } export default () => ( 提交 ) ``` ## JSON Schema 自定义筛选案例 ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { SelectTable, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { selectTable: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'SelectTable', 'x-component-props': { hasBorder: false, showSearch: true, primaryKey: 'key', isTree: true, filterOption: (input, option) => option.description.toLowerCase().indexOf(input.toLowerCase()) >= 0, filterSort: (optionA, optionB) => optionA.description .toLowerCase() .localeCompare(optionB.description.toLowerCase()), optionAsValue: true, rowSelection: { checkStrictly: false, }, }, enum: [ { key: '1', name: '标题1', description: 'A-描述' }, { key: '2', name: '标题2', description: 'X-描述', children: [ { key: '2-1', name: '标题2-1', description: 'Y-描述', children: [ { key: '2-1-1', name: '标题2-1-1', description: 'Z-描述' }, ], }, { key: '2-2', name: '标题2-2', description: 'YY-描述', }, ], }, { key: '3', name: '标题3', description: 'C-描述' }, ], properties: { name: { title: '标题', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '40%', }, }, description: { title: '描述', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '60%', }, }, }, }, }, } export default () => ( 提交 ) ``` ## JSON Schema 异步数据源案例 ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { SelectTable, FormItem, }, }) const loadData = async (value) => { return new Promise((resolve) => { setTimeout(() => { resolve([ { key: '3', name: 'AAA' + value, description: 'aaa' }, { key: '4', name: 'BBB' + value, description: 'bbb' }, ]) }, 1500) }) } const useAsyncDataSource = (service, field) => (value) => { field.loading = true service(value).then((data) => { field.setState({ dataSource: data, loading: false, }) }) } const form = createForm() const schema = { type: 'object', properties: { selectTable: { type: 'array', 'x-decorator': 'FormItem', 'x-component': 'SelectTable', 'x-component-props': { hasBorder: false, showSearch: true, filterOption: false, onSearch: '{{useAsyncDataSource(loadData,$self)}}', }, enum: [ { key: '1', name: '标题1', description: '描述1' }, { key: '2', name: '标题2', description: '描述2' }, ], properties: { name: { title: '标题', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '40%', }, }, description: { title: '描述', type: 'string', 'x-component': 'SelectTable.Column', 'x-component-props': { width: '60%', }, }, }, }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { FormItem, FormButtonGroup, Submit, SelectTable } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API ### SelectTable | 属性名 | 类型 | 描述 | 默认值 | | ------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------ | | mode | `'multiple' \| 'single'` | 设置 SelectTable 模式为单选或多选 | `'multiple'` | | valueType | `'all' \| 'parent' \| 'child' \| 'path'` | 返回值类型,checkStrictly 设置为 `false` 时有效 | `'all'` | | optionAsValue | boolean | 使用表格行数据作为值,valueType 值为 `'path'` 时无效 | false | | showSearch | boolean | 是否显示搜索组件 | false | | searchProps | object | Search 组件属性 | - | | primaryKey | `string \| (record) => string` | 表格行 key 的取值 | `'key'` | | filterOption | `boolean \| (inputValue, option) => boolean` | 是否根据输入项进行筛选。当其为一个函数时,会接收 inputValue option 两个参数,当 option 符合筛选条件时,应返回 true,反之则返回 false | true | | filterSort | (optionA, optionB) => number | 搜索时对筛选结果项的排序函数, 类似 Array.sort 里的 compareFunction | - | | onSearch | 文本框值变化时回调 | (inputValue) => void | - | 参考 https://fusion.design/pc/component/basic/table ### rowSelection | 属性名 | 类型 | 描述 | 默认值 | | ------------- | ------- | ------------------------------------------------------------ | ------ | | checkStrictly | boolean | checkable 状态下节点选择完全受控(父子数据选中状态不再关联) | true | 参考 https://fusion.design/pc/component/basic/table rowSelection ### SelectTable.Column 参考 https://fusion.design/pc/component/basic/table Table.Column 属性 ================================================ FILE: packages/next/docs/components/Space.md ================================================ # Space > Super convenient Flex layout component, can help users quickly realize the layout of any element side by side next to each other ## Markup Schema example ```tsx import React from 'react' import { Input, FormItem, FormLayout, FormButtonGroup, Submit, Space, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, Space, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Input, FormItem, FormLayout, FormButtonGroup, Submit, Space, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, Space, }, }) const form = createForm() const schema = { type: 'object', properties: { name: { type: 'void', title: 'Name', 'x-decorator': 'FormItem', 'x-decorator-props': { asterisk: true, feedbackLayout: 'none', }, 'x-component': 'Space', properties: { firstName: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', required: true, }, lastName: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', required: true, }, }, }, texts: { type: 'void', title: 'Text concatenation', 'x-decorator': 'FormItem', 'x-decorator-props': { asterisk: true, feedbackLayout: 'none', }, 'x-component': 'Space', properties: { aa: { type: 'string', 'x-decorator': 'FormItem', 'x-decorator-props': { addonAfter: 'Unit', }, 'x-component': 'Input', required: true, }, bb: { type: 'string', 'x-decorator': 'FormItem', 'x-decorator-props': { addonAfter: 'Unit', }, 'x-component': 'Input', required: true, }, cc: { type: 'string', 'x-decorator': 'FormItem', 'x-decorator-props': { addonAfter: 'Unit', }, 'x-component': 'Input', required: true, }, }, }, textarea: { type: 'string', title: 'Text box', 'x-decorator': 'FormItem', 'x-component': 'Input.TextArea', 'x-component-props': { style: { width: 400, }, }, required: true, }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Input, FormItem, FormLayout, FormButtonGroup, Submit, Space, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field, VoidField } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API | Property name | Type | Description | Default value | | ------------- | ----------------------------------------- | --------------- | ------------- | | style | CSSProperties | Style | - | | className | string | class name | - | | prefix | string | style prefix | true | | size | `number \|'small' \|'large' \|'middle'` | interval size | 8px | | direction | `'horizontal' \|'vertical'` | direction | - | | align | `'start' \|'end' \|'center' \|'baseline'` | align | `'start'` | | wrap | boolean | Whether to wrap | false | ================================================ FILE: packages/next/docs/components/Space.zh-CN.md ================================================ # Space > 超级便捷的 Flex 布局组件,可以帮助用户快速实现任何元素的并排紧挨布局 ## Markup Schema 案例 ```tsx import React from 'react' import { Input, FormItem, FormLayout, FormButtonGroup, Submit, Space, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, Space, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Input, FormItem, FormLayout, FormButtonGroup, Submit, Space, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, Space, }, }) const form = createForm() const schema = { type: 'object', properties: { name: { type: 'void', title: '姓名', 'x-decorator': 'FormItem', 'x-decorator-props': { asterisk: true, feedbackLayout: 'none', }, 'x-component': 'Space', properties: { firstName: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', required: true, }, lastName: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', required: true, }, }, }, texts: { type: 'void', title: '文本串联', 'x-decorator': 'FormItem', 'x-decorator-props': { asterisk: true, feedbackLayout: 'none', }, 'x-component': 'Space', properties: { aa: { type: 'string', 'x-decorator': 'FormItem', 'x-decorator-props': { addonAfter: '单位', }, 'x-component': 'Input', required: true, }, bb: { type: 'string', 'x-decorator': 'FormItem', 'x-decorator-props': { addonAfter: '单位', }, 'x-component': 'Input', required: true, }, cc: { type: 'string', 'x-decorator': 'FormItem', 'x-decorator-props': { addonAfter: '单位', }, 'x-component': 'Input', required: true, }, }, }, textarea: { type: 'string', title: '文本框', 'x-decorator': 'FormItem', 'x-component': 'Input.TextArea', 'x-component-props': { style: { width: 400, }, }, required: true, }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Input, FormItem, FormLayout, FormButtonGroup, Submit, Space, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field, VoidField } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API | 属性名 | 类型 | 描述 | 默认值 | | --------- | -------------------------------------------- | -------- | --------- | | style | CSSProperties | 样式 | - | | className | string | 类名 | - | | prefix | string | 样式前缀 | true | | size | `number \| 'small' \| 'large' \| 'middle'` | 间隔尺寸 | 8px | | direction | `'horizontal' \| 'vertical'` | 方向 | - | | align | `'start' \| 'end' \| 'center' \| 'baseline'` | 对齐 | `'start'` | | wrap | boolean | 是否换行 | false | ================================================ FILE: packages/next/docs/components/Submit.md ================================================ # Submit > Submit button ## Ordinary submission ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## Prevent Duplicate Submission (Loading) ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( { return new Promise((resolve) => { setTimeout(() => { console.log(values) resolve() }, 2000) }) }} onSubmitFailed={console.log} > submit ) ``` ## API For button-related API properties, we can refer to https://fusion.design/pc/component/basic/button, and the rest are the unique API properties of the Submit component | Property name | Type | Description | Default value | | --------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------- | ------------- | | onClick | `(event: MouseEvent) => void \| boolean` | Click event, if it returns false, it can block submission | - | | onSubmit | `(values: any) => Promise \| any` | Submit event callback | - | | onSubmitSuccess | (payload: any) => void | Submit successful response event | - | | onSubmitFailed | (feedbacks: [IFormFeedback](https://core.formilyjs.org/api/models/form#iformfeedback)[]) => void | Submit verification failure event callback | - | ================================================ FILE: packages/next/docs/components/Submit.zh-CN.md ================================================ # Submit > 提交按钮 ## 普通提交 ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## 防重复提交(Loading) ```tsx import React from 'react' import { Input, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Input, FormItem, }, }) const form = createForm() export default () => ( { return new Promise((resolve) => { setTimeout(() => { console.log(values) resolve() }, 2000) }) }} onSubmitFailed={console.log} > 提交 ) ``` ## API 按钮相关的 API 属性,我们参考 https://fusion.design/pc/component/basic/button 即可,剩下是 Submit 组件独有的 API 属性 | 属性名 | 类型 | 描述 | 默认值 | | --------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------- | ------ | | onClick | `(event: MouseEvent) => void \| boolean` | 点击事件,如果返回 false 可以阻塞提交 | - | | onSubmit | `(values: any) => Promise \| any` | 提交事件回调 | - | | onSubmitSuccess | (payload: any) => void | 提交成功响应事件 | - | | onSubmitFailed | (feedbacks: [IFormFeedback](https://core.formilyjs.org/zh-CN/api/models/form#iformfeedback)[]) => void | 提交校验失败事件回调 | - | ================================================ FILE: packages/next/docs/components/Switch.md ================================================ # Switch > Switch Components ## Markup Schema example ```tsx import React from 'react' import { Switch, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Switch, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Switch, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Switch, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { switch: { type: 'boolean', title: 'Switch', 'x-decorator': 'FormItem', 'x-component': 'Switch', }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Switch, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://fusion.design/pc/component/basic/switch ================================================ FILE: packages/next/docs/components/Switch.zh-CN.md ================================================ # Switch > 开关组件 ## Markup Schema 案例 ```tsx import React from 'react' import { Switch, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Switch, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Switch, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Switch, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { switch: { type: 'boolean', title: '开关', 'x-decorator': 'FormItem', 'x-component': 'Switch', }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Switch, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://fusion.design/pc/component/basic/switch ================================================ FILE: packages/next/docs/components/TimePicker.md ================================================ # TimePicker > Time Picker ## Markup Schema example ```tsx import React from 'react' import { TimePicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TimePicker, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { TimePicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TimePicker, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { time: { title: 'Time', 'x-decorator': 'FormItem', 'x-component': 'TimePicker', type: 'string', }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { TimePicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://fusion.design/pc/component/basic/time-picker ================================================ FILE: packages/next/docs/components/TimePicker.zh-CN.md ================================================ # TimePicker > 时间选择器 ## Markup Schema 案例 ```tsx import React from 'react' import { TimePicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TimePicker, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { TimePicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TimePicker, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { time: { title: '时间', 'x-decorator': 'FormItem', 'x-component': 'TimePicker', type: 'string', }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { TimePicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://fusion.design/pc/component/basic/time-picker ================================================ FILE: packages/next/docs/components/TimePicker2.md ================================================ # TimePicker2 > Time 选择器 ## Markup Schema Example ```tsx import React from 'react' import { TimePicker2, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TimePicker2, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema Case ```tsx import React from 'react' import { TimePicker2, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TimePicker2, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { time: { title: 'Time', 'x-decorator': 'FormItem', 'x-component': 'TimePicker2', type: 'string', }, '[startTime,endTime]': { title: 'Time Range', 'x-decorator': 'FormItem', 'x-component': 'TimePicker2.RangePicker', type: 'string', }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { TimePicker2, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://fusion.design/pc/component/basic/time-picker2 ================================================ FILE: packages/next/docs/components/TimePicker2.zh-CN.md ================================================ # TimePicker2 > 时间选择器 ## Markup Schema 案例 ```tsx import React from 'react' import { TimePicker2, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TimePicker2, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { TimePicker2, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TimePicker2, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { time: { title: '时间', 'x-decorator': 'FormItem', 'x-component': 'TimePicker2', type: 'string', }, '[startTime,endTime]': { title: '时间范围', 'x-decorator': 'FormItem', 'x-component': 'TimePicker2.RangePicker', type: 'string', }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { TimePicker2, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://fusion.design/pc/component/basic/time-picker2 ================================================ FILE: packages/next/docs/components/Transfer.md ================================================ # Transfer > Shuttle Box ## Markup Schema example ```tsx import React from 'react' import { Transfer, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Transfer, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Transfer, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Transfer, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { transfer: { type: 'array', title: 'shuttle box', 'x-decorator': 'FormItem', 'x-component': 'Transfer', enum: [ { label: 'Option 1', value: 'aaa' }, { label: 'Option 2', value: 'bbb' }, ], }, }, } const renderTitle = (item) => item.title export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Transfer, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## API Reference https://fusion.design/pc/component/basic/transfer ================================================ FILE: packages/next/docs/components/Transfer.zh-CN.md ================================================ # Transfer > 穿梭框 ## Markup Schema 案例 ```tsx import React from 'react' import { Transfer, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Transfer, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Transfer, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Transfer, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { transfer: { type: 'array', title: '穿梭框', 'x-decorator': 'FormItem', 'x-component': 'Transfer', enum: [ { label: '选项1', value: 'aaa' }, { label: '选项2', value: 'bbb' }, ], }, }, } const renderTitle = (item) => item.title export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Transfer, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://fusion.design/pc/component/basic/transfer ================================================ FILE: packages/next/docs/components/TreeSelect.md ================================================ # TreeSelect > Tree selector ## Markup Schema synchronization data source case ```tsx import React from 'react' import { TreeSelect, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TreeSelect, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## Markup Schema Asynchronous Linkage Data Source Case ```tsx import React from 'react' import { TreeSelect, Select, FormItem, FormButtonGroup, Submit, } from '@formily/next' import { createForm, onFieldReact, FormPathPattern, Field } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, TreeSelect, FormItem, }, }) const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: Field) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', children: [ { label: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { label: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { label: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'BBB', value: 'ccc', children: [ { label: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { label: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { label: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', children: [ { label: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { label: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { label: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'DDD', value: 'ddd', children: [ { label: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { label: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { label: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } }, 1500) }) }) }, }) export default () => ( Submit ) ``` ## JSON Schema synchronization data source case ```tsx import React from 'react' import { TreeSelect, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TreeSelect, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { select: { type: 'string', label: 'Select box', 'x-decorator': 'FormItem', 'x-component': 'TreeSelect', enum: [ { label: 'Option 1', value: 1, children: [ { label: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { label: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { label: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'Option 2', value: 2, children: [ { label: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { label: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { label: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ], 'x-component-props': { style: { width: 200, }, }, }, }, } export default () => ( Submit ) ``` ## JSON Schema asynchronous linkage data source case ```tsx import React from 'react' import { TreeSelect, Select, FormItem, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, TreeSelect, FormItem, }, }) const loadData = async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', children: [ { label: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { label: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { label: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'BBB', value: 'ccc', children: [ { label: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { label: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { label: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', children: [ { label: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { label: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { label: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'DDD', value: 'ddd', children: [ { label: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { label: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { label: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } }, 1500) }) } const useAsyncDataSource = (service) => (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) } const form = createForm() const schema = { type: 'object', properties: { linkage: { type: 'string', label: 'Linkage selection box', enum: [ { label: 'Request 1', value: 1 }, { label: 'Request 2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 200, }, }, }, select: { type: 'string', label: 'Asynchronous selection box', 'x-decorator': 'FormItem', 'x-component': 'TreeSelect', 'x-component-props': { style: { width: 200, }, }, 'x-reactions': ['{{useAsyncDataSource(loadData)}}'], }, }, } export default () => ( Submit ) ``` ## Pure JSX synchronization data source case ```tsx import React from 'react' import { TreeSelect, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( Submit ) ``` ## Pure JSX asynchronous linkage data source case ```tsx import React from 'react' import { TreeSelect, Select, FormItem, FormButtonGroup, Submit, } from '@formily/next' import { createForm, onFieldReact, FormPathPattern, FieldType, } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { action } from '@formily/reactive' const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: FieldType) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', children: [ { label: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { label: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { label: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'BBB', value: 'ccc', children: [ { label: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { label: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { label: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', children: [ { label: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { label: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { label: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'DDD', value: 'ddd', children: [ { label: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { label: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { label: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } }, 1500) }) }) }, }) export default () => ( Submit ) ``` ## API Reference https://fusion.design/pc/component/basic/tree-select ================================================ FILE: packages/next/docs/components/TreeSelect.zh-CN.md ================================================ # TreeSelect > 树选择器 ## Markup Schema 同步数据源案例 ```tsx import React from 'react' import { TreeSelect, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TreeSelect, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## Markup Schema 异步联动数据源案例 ```tsx import React from 'react' import { TreeSelect, Select, FormItem, FormButtonGroup, Submit, } from '@formily/next' import { createForm, onFieldReact, FormPathPattern, Field } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, TreeSelect, FormItem, }, }) const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: Field) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', children: [ { label: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { label: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { label: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'BBB', value: 'ccc', children: [ { label: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { label: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { label: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', children: [ { label: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { label: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { label: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'DDD', value: 'ddd', children: [ { label: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { label: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { label: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } }, 1500) }) }) }, }) export default () => ( 提交 ) ``` ## JSON Schema 同步数据源案例 ```tsx import React from 'react' import { TreeSelect, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { TreeSelect, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { select: { type: 'string', label: '选择框', 'x-decorator': 'FormItem', 'x-component': 'TreeSelect', enum: [ { label: '选项1', value: 1, children: [ { label: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { label: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { label: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: '选项2', value: 2, children: [ { label: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { label: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { label: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ], 'x-component-props': { style: { width: 200, }, }, }, }, } export default () => ( 提交 ) ``` ## JSON Schema 异步联动数据源案例 ```tsx import React from 'react' import { TreeSelect, Select, FormItem, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, TreeSelect, FormItem, }, }) const loadData = async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', children: [ { label: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { label: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { label: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'BBB', value: 'ccc', children: [ { label: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { label: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { label: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', children: [ { label: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { label: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { label: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'DDD', value: 'ddd', children: [ { label: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { label: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { label: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } }, 1500) }) } const useAsyncDataSource = (service) => (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) } const form = createForm() const schema = { type: 'object', properties: { linkage: { type: 'string', label: '联动选择框', enum: [ { label: '发请求1', value: 1 }, { label: '发请求2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 200, }, }, }, select: { type: 'string', label: '异步选择框', 'x-decorator': 'FormItem', 'x-component': 'TreeSelect', 'x-component-props': { style: { width: 200, }, }, 'x-reactions': ['{{useAsyncDataSource(loadData)}}'], }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 同步数据源案例 ```tsx import React from 'react' import { TreeSelect, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( 提交 ) ``` ## 纯 JSX 异步联动数据源案例 ```tsx import React from 'react' import { TreeSelect, Select, FormItem, FormButtonGroup, Submit, } from '@formily/next' import { createForm, onFieldReact, FormPathPattern, FieldType, } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { action } from '@formily/reactive' const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: FieldType) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', children: [ { label: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { label: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { label: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'BBB', value: 'ccc', children: [ { label: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { label: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { label: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', children: [ { label: 'Child Node1', value: '0-0-0', key: '0-0-0', }, { label: 'Child Node2', value: '0-0-1', key: '0-0-1', }, { label: 'Child Node3', value: '0-0-2', key: '0-0-2', }, ], }, { label: 'DDD', value: 'ddd', children: [ { label: 'Child Node1', value: '0-1-0', key: '0-1-0', }, { label: 'Child Node2', value: '0-1-1', key: '0-1-1', }, { label: 'Child Node3', value: '0-1-2', key: '0-1-2', }, ], }, ]) } }, 1500) }) }) }, }) export default () => ( 提交 ) ``` ## API 参考 https://fusion.design/pc/component/basic/tree-select ================================================ FILE: packages/next/docs/components/Upload.md ================================================ # Upload > Upload components > > Note: Using the upload component, it is recommended that users perform secondary packaging. Users do not need to care about the data communication between the upload component and Formily, only the style and basic upload configuration are required. ## Markup Schema example ```tsx import React from 'react' import { Upload, FormItem, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' import { UploadOutlined, InboxOutlined } from '@ant-design/icons' const NormalUpload = (props) => { return ( ) } const CardUpload = (props) => { return ( ) } const DraggerUpload = (props) => { return (

click to or drag file here

supports docx, xls, PDF

) } const SchemaField = createSchemaField({ components: { NormalUpload, CardUpload, DraggerUpload, FormItem, }, }) const form = createForm() export default () => ( Submit ) ``` ## JSON Schema case ```tsx import React from 'react' import { Upload, FormItem, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' import { UploadOutlined, InboxOutlined } from '@ant-design/icons' const NormalUpload = (props) => { return ( ) } const CardUpload = (props) => { return ( ) } const DraggerUpload = (props) => { return (

click to or drag file here

supports docx, xls, PDF

) } const SchemaField = createSchemaField({ components: { NormalUpload, CardUpload, DraggerUpload, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { upload: { type: 'array', title: 'Upload', required: true, 'x-decorator': 'FormItem', 'x-component': 'NormalUpload', }, upload2: { type: 'array', title: 'Card upload', required: true, 'x-decorator': 'FormItem', 'x-component': 'CardUpload', }, upload3: { type: 'array', title: 'Drag and drop upload', required: true, 'x-decorator': 'FormItem', 'x-component': 'DraggerUpload', }, }, } export default () => ( Submit ) ``` ## Pure JSX case ```tsx import React from 'react' import { Upload, FormItem, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { Button } from '@alifd/next' import { UploadOutlined, InboxOutlined } from '@ant-design/icons' const NormalUpload = (props) => { return ( ) } const CardUpload = (props) => { return ( ) } const DraggerUpload = (props) => { return (

click to or drag file here

supports docx, xls, PDF

) } const form = createForm() export default () => ( Submit ) ``` ## API Reference https://fusion.design/pc/component/basic/upload ================================================ FILE: packages/next/docs/components/Upload.zh-CN.md ================================================ # Upload > 上传组件 > > 注意:使用上传组件,推荐用户进行二次封装,用户无需关心上传组件与 Formily 的数据通信,只需要处理样式与基本上传配置即可。 ## Markup Schema 案例 ```tsx import React from 'react' import { Upload, FormItem, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' import { UploadOutlined, InboxOutlined } from '@ant-design/icons' const NormalUpload = (props) => { return ( ) } const CardUpload = (props) => { return ( ) } const DraggerUpload = (props) => { return (

click to or drag file here

supports docx, xls, PDF

) } const SchemaField = createSchemaField({ components: { NormalUpload, CardUpload, DraggerUpload, FormItem, }, }) const form = createForm() export default () => ( 提交 ) ``` ## JSON Schema 案例 ```tsx import React from 'react' import { Upload, FormItem, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' import { UploadOutlined, InboxOutlined } from '@ant-design/icons' const NormalUpload = (props) => { return ( ) } const CardUpload = (props) => { return ( ) } const DraggerUpload = (props) => { return (

click to or drag file here

supports docx, xls, PDF

) } const SchemaField = createSchemaField({ components: { NormalUpload, CardUpload, DraggerUpload, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { upload: { type: 'array', title: '上传', required: true, 'x-decorator': 'FormItem', 'x-component': 'NormalUpload', }, upload2: { type: 'array', title: '卡片上传', required: true, 'x-decorator': 'FormItem', 'x-component': 'CardUpload', }, upload3: { type: 'array', title: '拖拽上传', required: true, 'x-decorator': 'FormItem', 'x-component': 'DraggerUpload', }, }, } export default () => ( 提交 ) ``` ## 纯 JSX 案例 ```tsx import React from 'react' import { Upload, FormItem, FormButtonGroup, Submit, FormLayout, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { Button } from '@alifd/next' import { UploadOutlined, InboxOutlined } from '@ant-design/icons' const NormalUpload = (props) => { return ( ) } const CardUpload = (props) => { return ( ) } const DraggerUpload = (props) => { return (

click to or drag file here

supports docx, xls, PDF

) } const form = createForm() export default () => ( 提交 ) ``` ## API 参考 https://fusion.design/pc/component/basic/upload ================================================ FILE: packages/next/docs/components/index.md ================================================ # Alibaba Fusion ## Introduction @formily/next is a professional component library for form scenarios based on Fusion Design encapsulation. It has the following characteristics: - Only Formily 2.x is supported - Most components are not backward compatible - Unfortunately, many components of 1.x have inherent flaws in the API design. This is also because the form scheme has been explored, so there will be version breaks. - Richer component system - Layout components - FormLayout - FormItem - FormGrid - FormButtonGroup - Space - Submit - Reset - Input controls - Input - Password - Select - TreeSelect - DatePicker - TimePicker - NumberPicker - Transfer - Cascader - Radio - Checkbox - Upload - Switch - Scene components - ArrayCards - ArrayItems - ArrayTable - FormCollapse - FormStep - FormTab - FormDialog - FormDrawer - Editable - LogicDiagram - Reading state component - PreviewText - Theme customization ability - Completely abandon the 1.x styled-components solution, follow the style system of the component library, it is more convenient to customize the theme - Support secondary packaging - All components can be repackaged, and the 1.x component system cannot be repackaged, so providing this capability makes it more convenient for users to do business customization - Support reading mode - Although 1.x also supports reading mode, 2.x provides a separate PreviewText component, users can make reading mode encapsulation based on it, which is more flexible - Type is more friendly - Each component has an extremely complete type definition, and users can feel an unprecedented intelligent reminder experience during the actual development process - More complete layout control capabilities - 1.x's layout capabilities have basically converged to FormMegaLayout. This time, we directly removed Mega. Mega is a standard component and is completely internalized into FormLayout and FormItem components. At the same time, MegaLayout's grid layout capabilities are placed in FormGrid components. In, it also provides smarter layout capabilities. - More elegant and easy-to-use APIs, such as: - FormStep in the past has many problems. First, the type is not friendly. Second, the API is too hidden. To control the forward and backwards, you need to understand a bunch of private events. In the new version of FormStep, users only need to pay attention to the FormStep Reactive Model. You can create a Reactive Model through createFormStep and pass it to the FormStep component to quickly communicate. Similarly, FormTab/FormCollapse is the same communication mode. - Pop-up forms, drawer forms, presumably in the past, users had to write a lot of code on these two scenarios almost every time. This time, an extremely simple API is directly provided for users to use, which maximizes development efficiency. ## Note Because Fusion is built on Sass, if you use Webpack configuration, please use the following two Sass tools ``` "sass": "^1.32.11", "sass-loader": "^8.0.2" ``` ## Installation ```bash $ npm install --save @alifd/next moment $ npm install --save @formily/next @formily/react ``` ## Q/A Q: I want to package a set of component libraries by myself, what should I do? Answer: If it is an open source component library, you can directly participate in the project co-construction and provide PR. If it is a private component library in the enterprise, you can refer to the source code. The source code does not have too much complicated logic. Question: Why do components such as ArrayCards/ArrayTable/FormStep only support Schema mode and not pure JSX mode? Answer: This is the core advantage of Schema mode. With the help of protocols, we can do scene-based abstraction. On the contrary, pure JSX mode is limited by the unparseability of JSX. It is difficult for us to achieve UI-level scene-based abstraction. It's just an abstract hook. Q: Why is there no ArrayTabs component? Answer: Because Fusion's Tab component does not support the ability to add Tabs, the ArrayTabs component is temporarily not supported. ================================================ FILE: packages/next/docs/components/index.zh-CN.md ================================================ # Alibaba Fusion ## 介绍 @formily/next 是基于 Fusion Design 封装的针对表单场景专业级(Professional)组件库,它主要有以下几个特点: - 仅支持 Formily2.x - 大部分组件无法向后兼容 - 很遗憾,1.x 的很多组件在 API 设计上存在本质上的缺陷,这也是因为表单方案一直在探索之中,所以才会出现版本断裂。 - 更丰富的组件体系 - 布局组件 - FormLayout - FormItem - FormGrid - FormButtonGroup - Space - Submit - Reset - 输入控件 - Input - Password - Select - TreeSelect - DatePicker - TimePicker - NumberPicker - Transfer - Cascader - Radio - Checkbox - Upload - Switch - 场景组件 - ArrayCards - ArrayItems - ArrayTable - FormCollapse - FormStep - FormTab - FormDialog - FormDrawer - Editable - LogicDiagram - 阅读态组件 - PreviewText - 主题定制能力 - 完全放弃了 1.x styled-components 方案,follow 组件库的样式体系,更方便定制主题 - 支持二次封装 - 所有组件都能二次封装,1.x 的组件体系是不能二次封装的,所以提供了这个能力则更方便用户做业务定制 - 支持阅读态 - 虽然 1.x 同样支持阅读态,但是 2.x 单独提供了 PreviewText 组件,用户可以基于它自己做阅读态封装,灵活性更强 - 类型更加友好 - 每个组件都有着极其完整的类型定义,用户在实际开发过程中,可以感受到前所未有的智能提示体验 - 更完备的布局控制能力 - 1.x 的布局能力基本上都收敛到了 FormMegaLayout 上,这次,我们直接去掉 Mega,Mega 就是标准组件,完全内化到 FormLayout 和 FormItem 组件中,同时将 MegaLayout 的网格布局能力放到了 FormGrid 组件中,也提供了更智能的布局能力。 - 更优雅易用的 API,比如: - 过去的 FormStep,有很多问题,第一,类型不友好,第二,API 隐藏太深,想要控制前进后退需要理解一堆的私有事件。新版 FormStep,用户只需要关注 FormStep Reactive Model 即可,通过 createFormStep 就可以创建出 Reactive Model,传给 FormStep 组件即可快速通讯。同理,FormTab/FormCollapse 也是一样的通讯模式。 - 弹窗表单,抽屉表单,想必过去,用户几乎每次都得在这两个场景上写大量的代码,这次直接提供了极其简易的 API 让用户使用,最大化提升开发效率。 ## 注意 因为 Fusion 是基于 Sass 构建的,如果你用 Webpack 配置请使用以下两个 Sass 工具 ``` "sass": "^1.32.11", "sass-loader": "^8.0.2" ``` ## 安装 ```bash $ npm install --save @alifd/next moment $ npm install --save @formily/next @formily/react ``` ## Q/A 问:我想自己封装一套组件库,该怎么做? 答:如果是开源组件库,可以直接参与项目共建,提供 PR,如果是企业内私有组件库,参考源码即可,源码并没有太多复杂逻辑。 问:为什么 ArrayCards/ArrayTable/FormStep 这类组件只支持 Schema 模式,不支持纯 JSX 模式? 答:这就是 Schema 模式的核心优势,借助协议,我们可以做场景化抽象,相反,纯 JSX 模式,受限于 JSX 的不可解析性,我们很难做到 UI 级别的场景化抽象,更多的只是抽象 Hook。 问:为什么没有 ArrayTabs 组件? 答:因为 Fusion 的 Tab 组件并不支持新增 Tab 能力,所以暂时不支持 ArrayTabs 组件。 ================================================ FILE: packages/next/docs/index.md ================================================ --- title: Formily-Alibaba unified front-end form solution order: 10 hero: title: Formily Fusion desc: Formily Component System Based on Alibaba Fusion Encapsulation actions: - text: Home Site link: //formilyjs.org - text: Document link: /components features: - icon: https://img.alicdn.com/imgextra/i2/O1CN016i72sH1c5wh1kyy9U_!!6000000003550-55-tps-800-800.svg title: Easier To Use desc: Out of the box, rich cases - icon: https://img.alicdn.com/imgextra/i1/O1CN01bHdrZJ1rEOESvXEi5_!!6000000005599-55-tps-800-800.svg title: More Efficient desc: Stupid writing, super high performance - icon: https://img.alicdn.com/imgextra/i3/O1CN01xlETZk1G0WSQT6Xii_!!6000000000560-55-tps-800-800.svg title: More Professional desc: complete, flexible, elegant footer: Open-source MIT Licensed | Copyright © 2019-present
Powered by self --- ## Installation ```bash $ npm install --save @alifd/next moment $ npm install --save @formily/core @formily/react @formily/next ``` ## Quick start ```tsx /** * defaultShowCode: true */ import React from 'react' import { NumberPicker, FormItem, Space } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, FormConsumer, Field } from '@formily/react' const form = createForm() export default () => ( × {(form) => ( ={` ${form.values.price * form.values.count}元`} )} ) ``` ================================================ FILE: packages/next/docs/index.zh-CN.md ================================================ --- title: Formily - 阿里巴巴统一前端表单解决方案 order: 10 hero: title: Formily Fusion desc: 基于Alibaba Fusion封装的优雅且易用的Formily2.x组件体系 actions: - text: 主站文档 link: //formilyjs.org - text: 组件文档 link: /zh-CN/components features: - icon: https://img.alicdn.com/imgextra/i2/O1CN016i72sH1c5wh1kyy9U_!!6000000003550-55-tps-800-800.svg title: 更易用 desc: 开箱即用,案例丰富 - icon: https://img.alicdn.com/imgextra/i1/O1CN01bHdrZJ1rEOESvXEi5_!!6000000005599-55-tps-800-800.svg title: 更高效 desc: 傻瓜写法,超高性能 - icon: https://img.alicdn.com/imgextra/i3/O1CN01xlETZk1G0WSQT6Xii_!!6000000000560-55-tps-800-800.svg title: 更专业 desc: 完备,灵活,优雅 footer: Open-source MIT Licensed | Copyright © 2019-present
Powered by self --- ## 安装 ```bash $ npm install --save @alifd/next moment $ npm install --save @formily/core @formily/react @formily/next ``` ## 快速开始 ```tsx /** * defaultShowCode: true */ import React from 'react' import { NumberPicker, FormItem, Space } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, FormConsumer, Field } from '@formily/react' const form = createForm() export default () => ( × {(form) => ( ={` ${form.values.price * form.values.count} 元`} )} ) ``` ================================================ FILE: packages/next/package.json ================================================ { "name": "@formily/next", "version": "2.3.7", "license": "MIT", "main": "lib", "umd:main": "dist/formily.next.umd.production.js", "unpkg": "dist/formily.next.umd.production.js", "jsdelivr": "dist/formily.next.umd.production.js", "jsnext:main": "esm", "module": "esm", "sideEffects": [ "dist/*", "esm/*.js", "lib/*.js", "src/*.ts", "*.scss", "**/*/style.js" ], "repository": { "type": "git", "url": "git+https://github.com/alibaba/formily.git" }, "types": "esm/index.d.ts", "bugs": { "url": "https://github.com/alibaba/formily/issues" }, "homepage": "https://github.com/alibaba/formily#readme", "engines": { "npm": ">=3.0.0" }, "scripts": { "start": "dumi dev", "build": "rimraf -rf lib esm dist && npm run create:style && npm run build:cjs && npm run build:esm && npm run build:umd && npm run build:style", "create:style": "ts-node create-style", "build:style": "ts-node build-style", "build:cjs": "tsc --project tsconfig.build.json", "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm", "build:umd": "rollup --config", "build:docs": "dumi build" }, "peerDependencies": { "@alifd/next": "^1.19.0", "@types/react": ">=16.8.0", "@types/react-dom": ">=16.8.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", "react-is": ">=16.8.0" }, "peerDependenciesMeta": { "@types/react": { "optional": true }, "@types/react-dom": { "optional": true } }, "devDependencies": { "@umijs/plugin-sass": "^1.1.1", "dumi": "^1.1.0-rc.8" }, "dependencies": { "@formily/core": "2.3.7", "@formily/grid": "2.3.7", "@formily/json-schema": "2.3.7", "@formily/react": "2.3.7", "@formily/reactive": "2.3.7", "@formily/reactive-react": "2.3.7", "@formily/shared": "2.3.7", "classnames": "^2.2.6", "react-sortable-hoc": "^1.11.0", "react-sticky-box": "^0.9.3" }, "publishConfig": { "access": "public" }, "gitHead": "ac79c196ae9324889aca5e0501146f9b37b04283" } ================================================ FILE: packages/next/rollup.config.js ================================================ import baseConfig, { removeImportStyleFromInputFilePlugin, } from '../../scripts/rollup.base.js' export default baseConfig( 'formily.next', 'Formily.Next', removeImportStyleFromInputFilePlugin() ) ================================================ FILE: packages/next/src/__builtins__/empty.tsx ================================================ import React from 'react' export const Empty = () => { return (
) } ================================================ FILE: packages/next/src/__builtins__/hooks/index.ts ================================================ export * from './useClickAway' export * from './usePrefixCls' ================================================ FILE: packages/next/src/__builtins__/hooks/useClickAway.ts ================================================ import { useRef, useEffect, MutableRefObject } from 'react' const defaultEvent = 'click' type EventType = MouseEvent | TouchEvent type BasicTarget = | (() => T | null) | T | null | MutableRefObject type TargetElement = HTMLElement | Element | Document | Window function getTargetElement( target?: BasicTarget, defaultElement?: TargetElement ): TargetElement | undefined | null { if (!target) { return defaultElement } let targetElement: TargetElement | undefined | null if (typeof target === 'function') { targetElement = target() } else if ('current' in target) { targetElement = target.current } else { targetElement = target } return targetElement } export const useClickAway = ( onClickAway: (event: EventType) => void, target: BasicTarget | BasicTarget[], eventName: string = defaultEvent ) => { const onClickAwayRef = useRef(onClickAway) onClickAwayRef.current = onClickAway useEffect(() => { const handler = (event: any) => { const targets = Array.isArray(target) ? target : [target] if ( targets.some((targetItem) => { const targetElement = getTargetElement(targetItem) as HTMLElement return !targetElement || targetElement?.contains(event.target) }) ) { return } onClickAwayRef.current(event) } document.addEventListener(eventName, handler) return () => { document.removeEventListener(eventName, handler) } }, [target, eventName]) } ================================================ FILE: packages/next/src/__builtins__/hooks/usePrefixCls.ts ================================================ import { ConfigProvider } from '@alifd/next' export const usePrefixCls = ( tag?: string, props?: { prefix?: string } ) => { const getContext = ConfigProvider['getContext'] const prefix = props?.prefix ?? getContext()?.prefix ?? 'next-' return `${prefix}${tag ?? ''}` } ================================================ FILE: packages/next/src/__builtins__/icons.tsx ================================================ import React from 'react' import cls from 'classnames' import { usePrefixCls } from './hooks/usePrefixCls' export type IconProps = React.HTMLAttributes & { ref?: React.ForwardedRef } export type IconType = React.ForwardRefExoticComponent export const Icon: IconType = React.forwardRef((props, ref) => { const prefix = usePrefixCls('formily-icon') return ( ) }) export const MenuOutlinedIcon: IconType = React.forwardRef((props, ref) => ( )) export const PlusOutlinedIcon: IconType = React.forwardRef((props, ref) => ( )) export const UpOutlinedIcon: IconType = React.forwardRef((props, ref) => ( )) export const DownOutlinedIcon: IconType = React.forwardRef((props, ref) => ( )) export const DeleteOutlinedIcon: IconType = React.forwardRef((props, ref) => ( )) export const CopyOutlinedIcon: IconType = React.forwardRef((props, ref) => ( )) export const QuestionCircleOutlinedIcon: IconType = React.forwardRef( (props, ref) => ( ) ) export const CloseCircleOutlinedIcon: IconType = React.forwardRef( (props, ref) => ( ) ) export const CheckCircleOutlinedIcon: IconType = React.forwardRef( (props, ref) => ( ) ) export const ExclamationCircleOutlinedIcon: IconType = React.forwardRef( (props, ref) => ( ) ) export const EditOutlinedIcon: IconType = React.forwardRef((props, ref) => ( {' '} )) export const CloseOutlinedIcon: IconType = React.forwardRef((props, ref) => ( )) export const MessageOutlinedIcon: IconType = React.forwardRef((props, ref) => ( )) ================================================ FILE: packages/next/src/__builtins__/index.ts ================================================ export * from './moment' export * from './hooks' export * from './toArray' export * from './mapStatus' export * from './mapSize' export * from './empty' export * from './loading' export * from './portal' export * from './pickDataProps' export * from './icons' ================================================ FILE: packages/next/src/__builtins__/loading.ts ================================================ import { Message } from '@alifd/next' export const loading = async ( title: React.ReactNode = 'Loading...', processor: () => Promise ) => { let loading = setTimeout(() => { Message.loading(title as any) }, 100) try { return await processor() } finally { Message.hide() clearTimeout(loading) } } ================================================ FILE: packages/next/src/__builtins__/mapSize.ts ================================================ import { useFormLayout, useFormShallowLayout } from '../form-layout' export const mapSize = (props: any) => { const layout = { ...useFormShallowLayout(), ...useFormLayout() } const takeSize = () => { return layout.size === 'default' ? 'medium' : layout.size } return { ...props, size: props.size || takeSize(), } } ================================================ FILE: packages/next/src/__builtins__/mapStatus.ts ================================================ import { Field } from '@formily/core' export const mapStatus = (props: any, field: Field) => { const takeStatus = () => { if (!field) return if (field.loading || field.validating) return 'loading' if (field.selfErrors?.length) return 'error' if (field.selfWarnings?.length) return 'warning' return field.decoratorProps?.feedbackStatus } const takeState = (state: string) => { if (state === 'validating' || state === 'pending') return 'loading' return state } return { ...props, state: takeState(props.state) || takeStatus(), } } ================================================ FILE: packages/next/src/__builtins__/moment.ts ================================================ import { isArr, isEmpty, isFn } from '@formily/shared' import Moment from 'moment' const moment = (date: any, format?: string) => { return Moment(date?.toDate ? date.toDate() : date, format) } export const momentable = (value: any, format?: string) => { return Array.isArray(value) ? value.map((val) => moment(val, format)) : value ? moment(value, format) : value } export const formatMomentValue = ( value: any, format: any, placeholder?: string ): string | string[] => { const formatDate = (date: any, format: any, i = 0) => { if (!date) return placeholder const TIME_REG = /^(?:[01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/ let _format = format if (isArr(format)) { _format = format[i] } if (isFn(_format)) { return _format(date) } if (isEmpty(_format)) { return date } // moment '19:55:22' 下需要传入第二个参数 if (TIME_REG.test(date)) { return moment(date, _format).format(_format) } return moment(date).format(_format) } if (isArr(value)) { return value.map((val, index) => { return formatDate(val, format, index) }) } else { return value ? formatDate(value, format) : value || placeholder } } ================================================ FILE: packages/next/src/__builtins__/pickDataProps.ts ================================================ export const pickDataProps = (props: any = {}) => { return Object.keys(props).reduce((buf, key) => { if (key.includes('data-')) { buf[key] = props[key] } return buf }, {}) } ================================================ FILE: packages/next/src/__builtins__/portal.tsx ================================================ import React, { Fragment } from 'react' import { createPortal } from 'react-dom' import { observable } from '@formily/reactive' import { Observer } from '@formily/react' import { render as reactRender, unmount as reactUnmount } from './render' export interface IPortalProps { id?: string | symbol } const PortalMap = observable(new Map()) export const createPortalProvider = (id: string | symbol) => { const Portal = (props: React.PropsWithChildren) => { const portalId = props.id ?? id if (portalId && !PortalMap.has(portalId)) { PortalMap.set(portalId, null) } return ( {props.children} {() => { if (!portalId) return null const portal = PortalMap.get(portalId) if (portal) return createPortal(portal, document.body) return null }} ) } return Portal } export function createPortalRoot( host: HTMLElement, id: string ) { function render(renderer?: () => T) { if (PortalMap.has(id)) { PortalMap.set(id, renderer?.()) } else if (host) { reactRender({renderer?.()}, host) } } function unmount() { if (PortalMap.has(id)) { PortalMap.set(id, null) } if (host) { const unmountResult = reactUnmount(host) if (unmountResult && host.parentNode) { host.parentNode?.removeChild(host) } } } return { render, unmount, } } ================================================ FILE: packages/next/src/__builtins__/render.ts ================================================ import { ReactElement } from 'react' import * as ReactDOM from 'react-dom' import type { Root } from 'react-dom/client' // 移植自rc-util: https://github.com/react-component/util/blob/master/src/React/render.ts type CreateRoot = (container: ContainerType) => Root // Let compiler not to search module usage const fullClone = { ...ReactDOM, } as typeof ReactDOM & { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?: { usingClientEntryPoint?: boolean } createRoot?: CreateRoot } const { version, render: reactRender, unmountComponentAtNode } = fullClone let createRoot: CreateRoot try { const mainVersion = Number((version || '').split('.')[0]) if (mainVersion >= 18 && fullClone.createRoot) { // eslint-disable-next-line @typescript-eslint/no-var-requires createRoot = fullClone.createRoot } } catch (e) { // Do nothing; } function toggleWarning(skip: boolean) { const { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } = fullClone if ( __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED && typeof __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED === 'object' ) { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = skip } } const MARK = '__antd_mobile_root__' // ========================== Render ========================== type ContainerType = (Element | DocumentFragment) & { [MARK]?: Root } function legacyRender(node: ReactElement, container: ContainerType) { reactRender(node, container) } function concurrentRender(node: ReactElement, container: ContainerType) { toggleWarning(true) const root = container[MARK] || createRoot(container) toggleWarning(false) root.render(node) container[MARK] = root } export function render(node: ReactElement, container: ContainerType) { if (createRoot as unknown) { concurrentRender(node, container) return } legacyRender(node, container) } // ========================== Unmount ========================= function legacyUnmount(container: ContainerType) { return unmountComponentAtNode(container) } async function concurrentUnmount(container: ContainerType) { // Delay to unmount to avoid React 18 sync warning return Promise.resolve().then(() => { container[MARK]?.unmount() delete container[MARK] }) } export function unmount(container: ContainerType) { if (createRoot as unknown) { return concurrentUnmount(container) } return legacyUnmount(container) } ================================================ FILE: packages/next/src/__builtins__/toArray.ts ================================================ import React from 'react' import { isFragment } from 'react-is' export interface toArrayOption { keepEmpty?: boolean } export function toArray( children: React.ReactNode, option: toArrayOption = {} ): React.ReactElement[] { let ret: React.ReactElement[] = [] React.Children.forEach(children, (child: any) => { if ((child === undefined || child === null) && !option.keepEmpty) { return } if (Array.isArray(child)) { ret = ret.concat(toArray(child)) } else if (isFragment(child) && child.props) { ret = ret.concat(toArray(child.props.children, option)) } else { ret.push(child) } }) return ret } ================================================ FILE: packages/next/src/array-base/index.tsx ================================================ import React, { createContext, useContext } from 'react' import { Button } from '@alifd/next' import { isValid, isUndef, clone } from '@formily/shared' import { ButtonProps } from '@alifd/next/lib/button' import { ArrayField } from '@formily/core' import { useField, useFieldSchema, Schema, JSXComponent } from '@formily/react' import { SortableHandle } from 'react-sortable-hoc' import { usePrefixCls, PlusOutlinedIcon, DeleteOutlinedIcon, DownOutlinedIcon, UpOutlinedIcon, MenuOutlinedIcon, CopyOutlinedIcon, } from '../__builtins__' import cls from 'classnames' export interface IArrayBaseAdditionProps extends ButtonProps { title?: string method?: 'push' | 'unshift' defaultValue?: any icon?: React.ReactNode } export interface IArrayBaseOperationProps extends ButtonProps { title?: string index?: number ref?: React.Ref ) } ArrayBase.Remove = React.forwardRef((props, ref) => { const index = useIndex(props.index) const self = useField() const array = useArray() const prefixCls = usePrefixCls('formily-array-base') if (!array) return null if (array.field?.pattern !== 'editable') return null return ( ) }) ArrayBase.Copy = React.forwardRef((props, ref) => { const index = useIndex(props.index) const self = useField() const array = useArray() const prefixCls = usePrefixCls('formily-array-base') if (!array) return null if (array.field?.pattern !== 'editable') return null return ( ) }) ArrayBase.MoveDown = React.forwardRef((props, ref) => { const index = useIndex(props.index) const self = useField() const array = useArray() const prefixCls = usePrefixCls('formily-array-base') if (!array) return null if (array.field?.pattern !== 'editable') return null return ( ) }) ArrayBase.MoveUp = React.forwardRef((props, ref) => { const index = useIndex(props.index) const self = useField() const array = useArray() const prefixCls = usePrefixCls('formily-array-base') if (!array) return null if (array.field?.pattern !== 'editable') return null return ( ) }) ArrayBase.useArray = useArray ArrayBase.useIndex = useIndex ArrayBase.useRecord = useRecord ArrayBase.mixin = (target: any) => { target.Index = ArrayBase.Index target.SortHandle = ArrayBase.SortHandle target.Addition = ArrayBase.Addition target.Copy = ArrayBase.Copy target.Remove = ArrayBase.Remove target.MoveDown = ArrayBase.MoveDown target.MoveUp = ArrayBase.MoveUp target.useArray = ArrayBase.useArray target.useIndex = ArrayBase.useIndex target.useRecord = ArrayBase.useRecord return target } export default ArrayBase ================================================ FILE: packages/next/src/array-base/main.scss ================================================ @import '~@alifd/next/lib/core/index-noreset.scss'; $array-base-prefix-cls: '#{$css-prefix}formily-array-base'; .#{$array-base-prefix-cls}-remove, .#{$array-base-prefix-cls}-copy { transition: all 0.25s ease-in-out; color: $color-text1-3; font-size: 16px; &:hover { color: $color-text1-1; } &-disabled { color: $color-text1-1; cursor: not-allowed !important; &:hover { color: $color-text1-1; } } .#{$css-prefix}formily-icon { font-size: 16px; } } .#{$array-base-prefix-cls}-addition { transition: all 0.25s ease-in-out; } .#{$array-base-prefix-cls}-move-down { transition: all 0.25s ease-in-out; color: $color-text1-3; font-size: 16px; &:hover { color: $color-text1-1; } &-disabled { color: $color-text1-1; cursor: not-allowed !important; &:hover { color: $color-text1-1; } } .#{$css-prefix}formily-icon { font-size: 16px; } } .#{$array-base-prefix-cls}-move-up { transition: all 0.25s ease-in-out; color: $color-text1-3; font-size: 16px; &:hover { color: $color-text1-1; } &-disabled { color: $color-text1-1; cursor: not-allowed !important; &:hover { color: $color-text1-1; } } .#{$css-prefix}formily-icon { font-size: 16px; } } .#{$array-base-prefix-cls}-sort-handle { cursor: move; color: #888 !important; } ================================================ FILE: packages/next/src/array-base/style.ts ================================================ import '@alifd/next/lib/button/style' import './main.scss' ================================================ FILE: packages/next/src/array-cards/index.tsx ================================================ import React from 'react' import { Card } from '@alifd/next' import { CardProps } from '@alifd/next/lib/card' import { ArrayField } from '@formily/core' import { useField, observer, useFieldSchema, RecursionField, } from '@formily/react' import { ISchema } from '@formily/json-schema' import { usePrefixCls } from '../__builtins__' import { ArrayBase, ArrayBaseMixins, IArrayBaseProps } from '../array-base' import cls from 'classnames' type ComposedArrayCards = React.FC< React.PropsWithChildren > & ArrayBaseMixins const isAdditionComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Addition') > -1 } const isIndexComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Index') > -1 } const isRemoveComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Remove') > -1 } const isCopyComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Copy') > -1 } const isMoveUpComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('MoveUp') > -1 } const isMoveDownComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('MoveDown') > -1 } const isOperationComponent = (schema: ISchema) => { return ( isAdditionComponent(schema) || isRemoveComponent(schema) || isCopyComponent(schema) || isMoveDownComponent(schema) || isMoveUpComponent(schema) ) } const Empty = () => { return (
) } export const ArrayCards: ComposedArrayCards = observer((props) => { const field = useField() const schema = useFieldSchema() const dataSource = Array.isArray(field.value) ? field.value : [] const prefixCls = usePrefixCls('formily-array-cards', props) const { onAdd, onCopy, onRemove, onMoveDown, onMoveUp } = props const renderItems = () => { return dataSource?.map((item, index) => { const items = Array.isArray(schema.items) ? schema.items[index] || schema.items[0] : schema.items const title = ( { if (!isIndexComponent(schema)) return false return true }} onlyRenderProperties /> {props.title || field.title} ) const extra = ( { if (!isOperationComponent(schema)) return false return true }} onlyRenderProperties /> {props.extra} ) const content = ( { if (isIndexComponent(schema)) return false if (isOperationComponent(schema)) return false return true }} /> ) return ( field.value?.[index]} > {}} className={cls(`${prefixCls}-item`, props.className)} title={title} extra={extra} > {content} ) }) } const renderAddition = () => { return schema.reduceProperties((addition, schema, key) => { if (isAdditionComponent(schema)) { return } return addition }, null) } const renderEmpty = () => { if (dataSource?.length) return return ( {}} > ) } return ( {renderEmpty()} {renderItems()} {renderAddition()} ) }) ArrayCards.displayName = 'ArrayCards' ArrayBase.mixin(ArrayCards) export default ArrayCards ================================================ FILE: packages/next/src/array-cards/main.scss ================================================ @import '~@alifd/next/lib/core/index-noreset.scss'; $array-cards-prefix-cls: '#{$css-prefix}formily-array-cards'; .#{$css-prefix}empty { display: flex; justify-content: center; align-items: center; &-image { display: flex; justify-content: center; align-items: center; transform: scale(0.8); .ant-empty-img-default-ellipse { fill-opacity: 0.8; fill: #f5f5f5; } .ant-empty-img-default-path-1 { fill: #aeb8c2; } .ant-empty-img-default-path-2 { fill: url(#linearGradient-1); } .ant-empty-img-default-path-3 { fill: #f5f5f7; } .ant-empty-img-default-path-4, .ant-empty-img-default-path-5 { fill: #dce0e6; } .ant-empty-img-default-g { fill: #fff; } .ant-empty-img-simple-ellipse { fill: #f5f5f5; } .ant-empty-img-simple-g { stroke: #d9d9d9; } .ant-empty-img-simple-path { fill: #fafafa; } .ant-empty-rtl { direction: rtl; } } } .#{$array-cards-prefix-cls}-remove { transition: all 0.25s ease-in-out; color: $color-text1-3; font-size: 16px; margin-left: 6px; &:hover { color: $color-text1-1; } } .#{$array-cards-prefix-cls}-addition { transition: all 0.25s ease-in-out; } .#{$array-cards-prefix-cls}-move-down { transition: all 0.25s ease-in-out; color: $color-text1-3; font-size: 16px; margin-left: 6px; &:hover { color: $color-text1-1; } } .#{$array-cards-prefix-cls}-move-up { transition: all 0.25s ease-in-out; color: $color-text1-3; font-size: 16px; margin-left: 6px; &:hover { color: $color-text1-1; } } .#{$array-cards-prefix-cls}-item { margin-bottom: 10px !important; } .next-card-extra { svg { margin-right: 6px; &:last-of-type { margin-right: 0; } } } ================================================ FILE: packages/next/src/array-cards/style.ts ================================================ import '@alifd/next/lib/button/style' import '@alifd/next/lib/card/style' import './main.scss' ================================================ FILE: packages/next/src/array-collapse/index.tsx ================================================ import React, { Fragment, useState, useEffect } from 'react' import { Badge, Card, Collapse } from '@alifd/next' import { ArrayField } from '@formily/core' import { RecursionField, useField, useFieldSchema, observer, ISchema, } from '@formily/react' import { toArr } from '@formily/shared' import cls from 'classnames' import ArrayBase, { ArrayBaseMixins, IArrayBaseProps } from '../array-base' import { usePrefixCls, Empty } from '../__builtins__' import { CollapseProps, PanelProps } from '@alifd/next/lib/collapse' export interface IArrayCollapseProps extends CollapseProps { defaultOpenPanelCount?: number } type ComposedArrayCollapse = React.FC< React.PropsWithChildren > & ArrayBaseMixins & { CollapsePanel?: React.FC> } const isAdditionComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Addition') > -1 } const isIndexComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Index') > -1 } const isRemoveComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Remove') > -1 } const isMoveUpComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('MoveUp') > -1 } const isMoveDownComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('MoveDown') > -1 } const isOperationComponent = (schema: ISchema) => { return ( isAdditionComponent(schema) || isRemoveComponent(schema) || isMoveDownComponent(schema) || isMoveUpComponent(schema) ) } const range = (count: number) => Array.from({ length: count }).map((_, i) => i) const takeDefaultExpandedKeys = ( dataSourceLength: number, defaultOpenPanelCount: number ) => { if (dataSourceLength < defaultOpenPanelCount) return range(dataSourceLength) return range(defaultOpenPanelCount) } const insertExpandedKeys = (expandedKeys: number[], index: number) => { if (expandedKeys.length <= index) return expandedKeys.concat(index) return expandedKeys.reduce((buf, key) => { if (key < index) return buf.concat(key) if (key === index) return buf.concat([key, key + 1]) return buf.concat(key + 1) }, []) } export const ArrayCollapse: ComposedArrayCollapse = observer( ({ defaultOpenPanelCount = 5, ...props }) => { const field = useField() const dataSource = Array.isArray(field.value) ? field.value : [] const [expandKeys, setExpandKeys] = useState( takeDefaultExpandedKeys(dataSource.length, defaultOpenPanelCount) ) const schema = useFieldSchema() const prefixCls = usePrefixCls('formily-array-collapse', props) useEffect(() => { if (!field.modified && dataSource.length) { setExpandKeys( takeDefaultExpandedKeys(dataSource.length, defaultOpenPanelCount) ) } }, [dataSource.length, field]) if (!schema) throw new Error('can not found schema object') const { onAdd, onCopy, onRemove, onMoveDown, onMoveUp } = props const renderAddition = () => { return schema.reduceProperties((addition, schema, key) => { if (isAdditionComponent(schema)) { return } return addition }, null) } const renderEmpty = () => { if (dataSource.length) return return ( ) } const renderItems = () => { return ( {}} expandedKeys={expandKeys.map(String)} onExpand={(keys: string[]) => setExpandKeys(toArr(keys).map(Number))} className={cls(`${prefixCls}-item`, props.className)} > {dataSource.map((item, index) => { const items = Array.isArray(schema.items) ? schema.items[index] || schema.items[0] : schema.items const panelProps = field .query(`${field.address}.${index}`) .get('componentProps') const props: PanelProps = items['x-component-props'] const title = () => { const title = `${ panelProps?.title || props?.title || field.title }` const path = field.address.concat(index) const errors = field.form.queryFeedbacks({ type: 'error', address: `${path}.**`, }) return (
{ if (!isIndexComponent(schema)) return false return true }} onlyRenderProperties /> {errors.length ? ( {title} ) : ( title )}
{ if (!isOperationComponent(schema)) return false return true }} onlyRenderProperties />
) } const content = ( { if (isIndexComponent(schema)) return false if (isOperationComponent(schema)) return false return true }} /> ) return ( {}} key={index} title={title()} > field.value?.[index]} > {content} ) })}
) } return ( { onAdd?.(index) setExpandKeys(insertExpandedKeys(expandKeys, index)) }} onCopy={onCopy} onRemove={onRemove} onMoveUp={onMoveUp} onMoveDown={onMoveDown} > {renderEmpty()} {renderItems()} {renderAddition()} ) } ) const CollapsePanel: React.FC> = ({ children, }) => { return {children} } CollapsePanel.displayName = 'CollapsePanel' ArrayCollapse.displayName = 'ArrayCollapse' ArrayCollapse.CollapsePanel = CollapsePanel ArrayBase.mixin(ArrayCollapse) export default ArrayCollapse ================================================ FILE: packages/next/src/array-collapse/main.scss ================================================ @import '~@alifd/next/lib/core/index-noreset.scss'; $array-collapse-prefix-cls: '#{$css-prefix}formily-array-collapse'; .#{$css-prefix}empty { display: flex; justify-content: center; align-items: center; &-image { display: flex; justify-content: center; align-items: center; transform: scale(0.8); .ant-empty-img-default-ellipse { fill-opacity: 0.8; fill: #f5f5f5; } .ant-empty-img-default-path-1 { fill: #aeb8c2; } .ant-empty-img-default-path-2 { fill: url(#linearGradient-1); } .ant-empty-img-default-path-3 { fill: #f5f5f7; } .ant-empty-img-default-path-4, .ant-empty-img-default-path-5 { fill: #dce0e6; } .ant-empty-img-default-g { fill: #fff; } .ant-empty-img-simple-ellipse { fill: #f5f5f5; } .ant-empty-img-simple-g { stroke: #d9d9d9; } .ant-empty-img-simple-path { fill: #fafafa; } .ant-empty-rtl { direction: rtl; } } } .#{$array-collapse-prefix-cls}-remove { transition: all 0.25s ease-in-out; color: $color-text1-3; font-size: 16px; margin-left: 6px; &:hover { color: $color-text1-1; } } .#{$array-collapse-prefix-cls}-addition { transition: all 0.25s ease-in-out; } .#{$array-collapse-prefix-cls}-move-down { transition: all 0.25s ease-in-out; color: $color-text1-3; font-size: 16px; margin-left: 6px; &:hover { color: $color-text1-1; } } .#{$array-collapse-prefix-cls}-move-up { transition: all 0.25s ease-in-out; color: $color-text1-3; font-size: 16px; margin-left: 6px; &:hover { color: $color-text1-1; } } .#{$array-collapse-prefix-cls}-item { margin-bottom: 10px !important; .#{$array-collapse-prefix-cls}-item-title { display: flex; justify-content: space-between; } } ================================================ FILE: packages/next/src/array-collapse/style.ts ================================================ import '@alifd/next/lib/collapse/style' import '@alifd/next/lib/card/style' import './main.scss' ================================================ FILE: packages/next/src/array-items/index.tsx ================================================ import React from 'react' import { ArrayField } from '@formily/core' import { useField, observer, useFieldSchema, RecursionField, } from '@formily/react' import cls from 'classnames' import { SortableContainer, SortableElement, SortableContainerProps, SortableElementProps, } from 'react-sortable-hoc' import { ISchema } from '@formily/json-schema' import { usePrefixCls } from '../__builtins__' import { ArrayBase, ArrayBaseMixins, IArrayBaseProps } from '../array-base' type ComposedArrayItems = React.FC< React.PropsWithChildren< React.HTMLAttributes & IArrayBaseProps > > & ArrayBaseMixins & { Item?: React.FC< React.HTMLAttributes & { type?: 'card' | 'divide' } > } const SortableItem: React.FC< React.PropsWithChildren> & SortableElementProps > = SortableElement( (props: React.PropsWithChildren>) => { const prefixCls = usePrefixCls('formily-array-items') return (
{props.children}
) } ) as any const SortableList: React.FC< React.PropsWithChildren> & SortableContainerProps > = SortableContainer( (props: React.PropsWithChildren>) => { const prefixCls = usePrefixCls('formily-array-items') return (
{props.children}
) } ) as any const isAdditionComponent = (schema: ISchema) => { return schema['x-component']?.indexOf('Addition') > -1 } const useAddition = () => { const schema = useFieldSchema() return schema.reduceProperties((addition, schema, key) => { if (isAdditionComponent(schema)) { return } return addition }, null) } export const ArrayItems: ComposedArrayItems = observer((props) => { const field = useField() const prefixCls = usePrefixCls('formily-array-items') const schema = useFieldSchema() const addition = useAddition() const { onAdd, onCopy, onRemove, onMoveDown, onMoveUp } = props const dataSource = Array.isArray(field.value) ? field.value : [] return (
{}} className={cls(prefixCls, props.className)} > { field.move(oldIndex, newIndex) }} > {dataSource?.map((item, index) => { const items = Array.isArray(schema.items) ? schema.items[index] || schema.items[0] : schema.items return ( field.value?.[index]} >
) })}
{addition}
) }) ArrayItems.displayName = 'ArrayItems' ArrayItems.Item = (props) => { const prefixCls = usePrefixCls('formily-array-items') return (
{}} className={cls(`${prefixCls}-${props.type || 'card'}`, props.className)} > {props.children}
) } ArrayBase.mixin(ArrayItems) export default ArrayItems ================================================ FILE: packages/next/src/array-items/main.scss ================================================ @import '~@alifd/next/lib/core/index-noreset.scss'; $array-items-prefix-cls: '#{$css-prefix}formily-array-items'; .#{$array-items-prefix-cls} { .#{$css-prefix}form-item { margin-bottom: 0; } } // fix https://github.com/alibaba/formily/issues/2891 .#{$array-items-prefix-cls}-item { z-index: 100000; } .#{$array-items-prefix-cls}-sort-handler { cursor: move; color: #888 !important; } .#{$array-items-prefix-cls}-item-inner { margin-bottom: 10px; visibility: visible; } .#{$array-items-prefix-cls}-card { display: flex; border: 1px solid #eee; margin-bottom: 10px; padding: 3px 6px; background: #fff; justify-content: space-between; align-items: center; transition: all 0.35s; .#{$css-prefix}formily-item:not(.#{$css-prefix}formily-item-feedback-layout-popover) { margin-bottom: 0 !important; position: relative; .#{$css-prefix}formily-item-help { position: absolute; font-size: 12px; top: 100%; background: #fff; width: 100%; margin-top: 3px; padding: 3px; z-index: 1; border-radius: 3px; box-shadow: 0 0 10px #eee; } } } .#{$array-items-prefix-cls}-divide { display: flex; border-bottom: 1px solid #eee; margin-bottom: 10px; padding: 10px 0; background: #fff; justify-content: space-between; align-items: center; .#{$css-prefix}formily-item:not(.#{$css-prefix}formily-item-feedback-layout-popover) { margin-bottom: 0 !important; position: relative; .#{$css-prefix}formily-item-help { position: absolute; font-size: 12px; top: 100%; background: #fff; width: 100%; margin-top: 3px; padding: 3px; z-index: 1; border-radius: 3px; box-shadow: 0 0 10px #eee; } } } ================================================ FILE: packages/next/src/array-items/style.ts ================================================ import '@alifd/next/lib/button/style' import './main.scss' ================================================ FILE: packages/next/src/array-table/index.tsx ================================================ import React, { Fragment, useState, useRef, useEffect, createContext, useContext, } from 'react' import { Table, Pagination, Select, Badge } from '@alifd/next' import { PaginationProps } from '@alifd/next/lib/pagination' import { TableProps, ColumnProps } from '@alifd/next/lib/table' import { SelectProps } from '@alifd/next/lib/select' import cls from 'classnames' import { GeneralField, FieldDisplayTypes, ArrayField } from '@formily/core' import { useField, observer, useFieldSchema, RecursionField, ReactFC, } from '@formily/react' import { isArr, isBool, isFn } from '@formily/shared' import { Schema } from '@formily/json-schema' import { usePrefixCls } from '../__builtins__' import { ArrayBase, ArrayBaseMixins, IArrayBaseProps } from '../array-base' interface ObservableColumnSource { field: GeneralField columnProps: ColumnProps schema: Schema display: FieldDisplayTypes name: string } interface IArrayTablePaginationProps extends Omit { dataSource?: any[] children?: ( dataSource: any[], pagination: React.ReactNode ) => React.ReactElement } interface IStatusSelectProps extends SelectProps { pageSize?: number } export type ExtendTableProps = { pagination?: PaginationProps } & IArrayBaseProps & TableProps type ComposedArrayTable = ReactFC & ArrayBaseMixins & { Column?: ReactFC } interface PaginationAction { totalPage?: number pageSize?: number changePage?: (page: number) => void } const isColumnComponent = (schema: Schema) => { return schema['x-component']?.indexOf('Column') > -1 } const isOperationsComponent = (schema: Schema) => { return schema['x-component']?.indexOf('Operations') > -1 } const isAdditionComponent = (schema: Schema) => { return schema['x-component']?.indexOf('Addition') > -1 } const useArrayTableSources = () => { const arrayField = useField() const schema = useFieldSchema() const parseSources = (schema: Schema): ObservableColumnSource[] => { if ( isColumnComponent(schema) || isOperationsComponent(schema) || isAdditionComponent(schema) ) { if (!schema['x-component-props']?.['dataIndex'] && !schema['name']) return [] const name = schema['x-component-props']?.['dataIndex'] || schema['name'] const field = arrayField.query(arrayField.address.concat(name)).take() const columnProps = field?.component?.[1] || schema['x-component-props'] || {} const display = field?.display || schema['x-display'] || 'visible' return [ { name, display, field, schema, columnProps, }, ] } else if (schema.properties) { return schema.reduceProperties((buf, schema) => { return buf.concat(parseSources(schema)) }, []) } } const parseArrayItems = (schema: Schema['items']) => { if (!schema) return [] const sources: ObservableColumnSource[] = [] const items = isArr(schema) ? schema : [schema] return items.reduce((columns, schema) => { const item = parseSources(schema) if (item) { return columns.concat(item) } return columns }, sources) } return parseArrayItems(schema.items) } const useArrayTableColumns = ( dataSource: any[], field: ArrayField, sources: ObservableColumnSource[] ): TableProps['columns'] => { return sources.reduce((buf, { name, columnProps, schema, display }, key) => { if (display !== 'visible') return buf if (!isColumnComponent(schema)) return buf return buf.concat({ ...columnProps, key, dataIndex: name, cell: (value: any, _: number, record: any) => { const index = dataSource?.indexOf(record) const children = ( field.value?.[index]} > ) return children }, }) }, []) } const useAddition = () => { const schema = useFieldSchema() return schema.reduceProperties((addition, schema, key) => { if (isAdditionComponent(schema)) { return } return addition }, null) } const schedulerRequest = { request: null, } const StatusSelect: ReactFC = observer( ({ pageSize, ...props }) => { const field = useField() const prefixCls = usePrefixCls('formily-array-table') const errors = field.errors const parseIndex = (address: string) => { return Number( address .slice(address.indexOf(field.address.toString()) + 1) .match(/(\d+)/)?.[1] ) } const options = props.dataSource?.map(({ label, value }) => { const hasError = errors.some(({ address }) => { const currentIndex = parseIndex(address) const startIndex = (value - 1) * pageSize const endIndex = value * pageSize return currentIndex >= startIndex && currentIndex <= endIndex }) return { label: hasError ? {label} : label, value, } }) return ( { obs.value = e.target.value }} />
{obs.value}
) }) ``` ### Note `observer` can only receive callable function components, and does not support packaged components such as `React.forwardRef` | `React.memo`. ## Observer ### Description Similar to Vue's responsive slot, it receives a Function RenderProps, as long as any responsive data consumed inside the Function, it will be automatically re-rendered as the data changes, and it is easier to achieve local accurate rendering In fact, the function of this API is basically the same as that of FormConsumer, except that FormConsumer reveals the form instance of the current context in the RenderProps parameter. ### Signature ```ts interface IObserverProps { children?: () => React.ReactElement } type Observer = React.FC> ``` ### Example ```tsx /** * defaultShowCode: true */ import React from 'react' import { observable } from '@formily/reactive' import { Observer } from '@formily/react' const obs = observable({ value: 'Hello world', }) export default () => { return (
{() => ( { obs.value = e.target.value }} /> )}
{() =>
{obs.value}
}
) } ``` ================================================ FILE: packages/react/docs/api/shared/observer.zh-CN.md ================================================ # observer ## observer ### 描述 observer 是一个 [HOC](https://reactjs.bootcss.com/docs/higher-order-components.html),用于为 react 函数组件添加 reactive 特性。 ### 什么时候使用 当一个组件内部使用了 [observable](https://reactive.formilyjs.org/zh-CN/api/observable) 对象,而你希望组件响应 observable 对象的变化时。 ### API 定义 ```ts interface IObserverOptions { // 是否需要 observer 使用 forwardRef 传递 ref 属性 forwardRef?: boolean scheduler?: (updater: () => void) => void displayName?: string } function observer( component: React.FunctionComponent

, options?: Options ): React.MemoExoticComponent< React.FunctionComponent< Options extends { forwardRef: true } ? React.PropsWithRef

: React.PropsWithoutRef

> > ``` ### 用例 ```tsx /** * defaultShowCode: true */ import React from 'react' import { observable } from '@formily/reactive' import { observer } from '@formily/reactive-react' const obs = observable({ value: 'Hello world', }) export default observer(() => { return (

{ obs.value = e.target.value }} />
{obs.value}
) }) ``` ### 注意 `observer` 只能接收 callable 函数组件,不支持 `React.forwardRef` | `React.memo` 等包裹的组件。 ## Observer ### 描述 类似于 Vue 的响应式 Slot,它接收一个 Function RenderProps,只要在 Function 内部消费到的任何响应式数据,都会随数据变化而自动重新渲染,也更容易实现局部精确渲染 其实该 API 与 FormConsumer 的作用基本一致,只是 FormConsumer 在 RenderProps 参数中透出了当前上下文的 form 实例 ### 签名 ```ts interface IObserverProps { children?: () => React.ReactElement } type Observer = React.FC> ``` ### 用例 ```tsx /** * defaultShowCode: true */ import React from 'react' import { observable } from '@formily/reactive' import { Observer } from '@formily/react' const obs = observable({ value: 'Hello world', }) export default () => { return (
{() => ( { obs.value = e.target.value }} /> )}
{() =>
{obs.value}
}
) } ``` ================================================ FILE: packages/react/docs/guide/architecture.md ================================================ # Core Architecture The architecture of @formily/react is not complicated compared to @formily/core. First look at the architecture diagram: ![](https://img.alicdn.com/imgextra/i1/O1CN013jbRfk1l5n6N7jYH8_!!6000000004768-55-tps-2200-1637.svg) From this architecture diagram, we can see that @formily/react supports two types of users, one is pure source code development users, they only need to use the Field/ArrayField/ObjectField/VoidField component. The other type is users who do dynamic development based on JSON-Schema. They mainly rely on the SchemaField component. However, both types of users need to use a FormProvider component to uniformly deliver the context. Then there is the SchemaField component, which is actually the dependent Field/ArrayField/ObjectField/VoidField component inside. ================================================ FILE: packages/react/docs/guide/architecture.zh-CN.md ================================================ # 核心架构 @formily/react 的架构相比于@formily/core 并不复杂,先看架构图: ![](https://img.alicdn.com/imgextra/i1/O1CN013jbRfk1l5n6N7jYH8_!!6000000004768-55-tps-2200-1637.svg) 从这张架构图中我们可以看到,@formily/react 支持了两类用户,一类就是纯源码开发用户,他们只需要使用 Field/ArrayField/ObjectField/VoidField 组件。另一类就是基于 JSON-Schema 做动态开发的用户,他们依赖的主要是 SchemaField 组件,但是,这两类用户都需要使用一个 FormProvider 的组件来统一下发上下文。然后是 SchemaField 组件,它内部其实是依赖的 Field/ArrayField/ObjectField/VoidField 组件。 ================================================ FILE: packages/react/docs/guide/concept.md ================================================ # Core idea The architecture of @formily/react itself is not complicated, because it only provides a series of components and Hooks for users to use, but we still need to understand the following concepts: - Form context - Field context - Protocol context - Model binding - Protocol driven - Three development modes ## Form context From the [architecture diagram](/guide/architecture) we can see that FormProvider exists as a unified context for forms, and its position is very important. It is mainly used to create [Form](//core. formilyjs.org/api/models/form) instances are distributed to all sub-components, whether in built-in components or user-extended components, can be read through [useForm](/api/hooks/use-form) [ Form](//core.formilyjs.org/api/models/form) instance ## Field context From the [architecture diagram](/guide/architecture) we can see that whether it is Field/ArrayField/ObjectField/VoidField, a FieldContext will be issued to the subtree. We can read the current field model in the custom component, mainly Use [useField](/api/hooks/use-field) to read, which is very convenient for model mapping ## Protocol context From the [architecture diagram](/guide/architecture) we can see that [RecursionField](/api/components/recursion-field) will send a FieldSchemaContext to the subtree, and we can read the current field in the custom component The Schema description is mainly read using [useFieldSchema](/api/hooks/useFieldSchema). Note that this Hook can only be used in the [SchemaField](/api/components/SchemaField) and [RecursionField](/api/components/recursion-field) subtrees ## Model binding To understand model binding, you need to understand what [MVVM](//core.formilyjs.org/guide/mvvm) is. After understanding, let’s take a look at this picture: ![](https://img.alicdn.com/imgextra/i1/O1CN01A03C191KwT1raxnDg_!!6000000001228-55-tps-2200-869.svg) In Formily, @formily/core is ViewModel, Component and Decorator are View, @formily/react is the glue layer that binds ViewModel and View, and the binding of ViewModel and View is called model binding, which implements model binding. The main methods are [useField](/api/hooks/use-field), and [connect](/api/shared/connect) and [mapProps](/api/shared/map-props) can also be used. Note that Component only needs to support the value/onChange property to automatically realize the two-way binding of the data layer. ## JSON Schema Driver Protocol-driven rendering is the most expensive part of @formily/react, but after learning it, the benefits it brings to the business are also very high. A total of 4 core concepts need to be understood: - Schema - Recursive rendering - Protocol binding - Three development modes ### Schema Formily’s protocol driver is mainly based on the standard JSON Schema to drive rendering. At the same time, we have extended some `x-*` attributes to express the UI on top of the standard, so that the entire protocol can fully describe a complex form. Schema protocol, refer to [Schema](/api/shared/schema) API document ### Recursive rendering What is recursive rendering? Recursive rendering means that component A will continue to use component A to render content under certain conditions. Take a look at the following pseudo code: ```json {<---- RecursionField (condition: object; rendering right: RecursionField) "type":"object", "properties":{ "username":{ <---- RecursionField (condition: string; rendering right: RecursionField) "type":"string", "x-component":"Input" }, "phone":{ <---- RecursionField (condition: string; rendering right: RecursionField) "type":"string", "x-component":"Input", "x-validator":"phone" }, "email":{ <---- RecursionField (condition: string; rendering right: RecursionField) "type":"string", "x-component":"Input", "x-validator":"email" }, "contacts":{ <---- RecursionField (condition: array; rendering right: RecursionField) "type":"array", "x-component":"ArrayTable", "items":{ <---- RecursionField (condition: object; rendering rights: ArrayTable component) "type":"object", "properties":{ "username":{ <---- RecursionField (condition: string; rendering right: RecursionField) "type":"string", "x-component":"Input" }, "phone":{ <---- RecursionField (condition: string; rendering right: RecursionField) "type":"string", "x-component":"Input", "x-validator":"phone" }, "email":{ <---- RecursionField (condition: string; rendering right: RecursionField) "type":"string", "x-component":"Input", "x-validator":"email" }, } } } } } ``` @formily/react The entry point for recursive rendering is [SchemaField](/api/components/schema-field), but it actually uses [RecursionField](/api/components/recursion-field) to render internally, because of JSON-Schema It is a recursive structure, so [RecursionField](/api/components/recursion-field) will be parsed from the top-level Schema node when rendering. If it is a non-object and array type, it will directly render the specific component. If it is an object, it will traverse. properties Continue to use [RecursionField](/api/components/recursion-field) to render child Schema nodes. A special case here is the rendering of the array type auto-increment list, which requires the user to use [RecursionField](/api/components/recursion-field) in the custom component for recursive rendering, because the UI of the auto-increment list is very customized High, so the recursive rendering rights are handed over to the user to render, so the design can also make protocol-driven rendering more flexible. What is the difference between SchemaField and RecursionField? There are two main points: - SchemaField supports Markup grammar, it will parse Markup grammar in advance to generate [JSON Schema](/api/shared/schema) and transfer it to RecursionField for rendering, so RecursionField can only be rendered based on [JSON Schema](/api/shared/schema) - SchemaField renders the overall Schema protocol, while RecursionField renders the partial Schema protocol ### Protocol binding I talked about model binding, and protocol binding is the process of converting Schema protocol into model binding, because JSON-Schema protocol is a JSON string and can be stored offline, while model binding is a binding between memory The relationship is at the Runtime layer. For example, `x-component` is the string identifier of the component in the Schema, but the component in the model requires component reference, so the JSON string and the Runtime layer need to be converted. Then we can continue to improve the above model binding diagram: ![](https://img.alicdn.com/imgextra/i3/O1CN01jLCRxH1aa3V0x6nw4_!!6000000003345-55-tps-2200-1147.svg) To sum up, in @formily/react, there are mainly two layers of binding relationships, Schema binding model, model binding component, the glue layer that realizes the binding is @formily/react, it should be noted that Schema binds the field model After that, the Schema is not perceptible in the field model. For example, if you want to modify the `enum`, you need to modify the `dataSource` attribute in the field model. In short, if you want to update the field model, refer to [Field](//core.formilyjs. org/api/models/field), you can refer to [Schema](/api/shared/schema) document if you want to understand the mapping relationship between Schema and field model ## Three development models From the [architecture diagram](/guide/architecture), we have actually seen that the entire @formily/react has three development modes, corresponding to different users: - JSX development model - JSON Schema development mode - Markup Schema development mode We can look at specific examples #### JSX development model This mode mainly uses Field/ArrayField/ObjectField/VoidField components ```tsx import React from 'react' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { Input } from 'antd' const form = createForm() export default () => ( ) ``` #### JSON Schema Development Mode This mode is to pass JSON Schema to the schema attribute of SchemaField ```tsx import React from 'react' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Input } from 'antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, }, }) export default () => ( ) ``` #### Markup Schema Development Mode This mode can be regarded as a Schema development mode that is more friendly to source code development, and it also uses the SchemaField component. Because it is difficult to get the best smart prompt experience in the JSX environment with JSON Schema, and it is inconvenient to maintain, the maintainability in the form of tags will be better, and the smart prompt is also very strong. Markup Schema mode mainly has the following characteristics: - Mainly rely on description tags such as SchemaField.String/SchemaField.Array/SchemaField.Object... to express Schema - Each description tag represents a Schema node, which is equivalent to JSON-Schema - SchemaField child nodes cannot insert UI elements at will, because SchemaField will only parse all the Schema description tags of the child nodes, and then convert them into JSON Schema, and finally give it to [RecursionField](/api/components/recursion-field) for rendering, if you want Insert UI elements, you can upload the `x-content` attribute in VoidDield to insert UI elements ```tsx import React from 'react' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Input } from 'antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, }, }) export default () => (
I will not be rendered
I will be rendered} />
) ``` ================================================ FILE: packages/react/docs/guide/concept.zh-CN.md ================================================ # 核心概念 @formily/react 本身架构不复杂,因为它只是提供了一系列的组件和 Hooks 给用户使用,但是我们还是需要理解以下几个概念: - 表单上下文 - 字段上下文 - 协议上下文 - 模型绑定 - 协议驱动 - 三种开发模式 ## 表单上下文 从[架构图](/guide/architecture)中我们可以看到 FormProvider 是作为表单统一上下文而存在,它的地位非常重要,主要用于将@formily/core 创建出来的[Form](//core.formilyjs.org/zh-CN/api/models/form)实例下发到所有子组件中,不管是在内置组件还是用户扩展的组件,都能通过[useForm](/api/hooks/use-form)读取到[Form](//core.formilyjs.org/zh-CN/api/models/form)实例 ## 字段上下文 从[架构图](/guide/architecture)中我们可以看到不管是 Field/ArrayField/ObjectField/VoidField,会给子树下发一个 FieldContext,我们可以在自定义组件中读取到当前字段模型,主要是使用[useField](/api/hooks/use-field)来读取,这样非常方便于做模型映射 ## 协议上下文 从[架构图](/guide/architecture)中我们可以看到[RecursionField](/api/components/recursion-field)会给子树下发一个 FieldSchemaContext,我们可以在自定义组件中读取到当前字段的 Schema 描述,主要是使用[useFieldSchema](/api/hooks/useFieldSchema)来读取。注意,该 Hook 只能用在[SchemaField](/api/components/SchemaField)和[RecursionField](/api/components/recursion-field)子树中使用 ## 模型绑定 想要理解模型绑定,需要先理解什么是[MVVM](//core.formilyjs.org/zh-CN/guide/mvvm),理解了之后我们再看看这张图: ![](https://img.alicdn.com/imgextra/i1/O1CN01A03C191KwT1raxnDg_!!6000000001228-55-tps-2200-869.svg) 在 Formily 中,@formily/core 就是 ViewModel,Component 和 Decorator 就是 View,@formily/react 就是将 ViewModel 和 View 绑定起来的胶水层,ViewModel 和 View 的绑定就叫做模型绑定,实现模型绑定的手段主要有[useField](/api/hooks/use-field),也能使用[connect](/api/shared/connect)和[mapProps](/api/shared/map-props),需要注意的是,Component 只需要支持 value/onChange 属性即可自动实现数据层的双向绑定。 ## 协议驱动 协议驱动渲染算是@formily/react 中学习成本最高的部分了,但是学会了之后,它给业务带来的收益也是很高,总共需要理解 4 个核心概念: - Schema - 递归渲染 - 协议绑定 - 三种开发模式 ### Schema formily 的协议驱动主要是基于标准 JSON Schema 来进行驱动渲染的,同时我们在标准之上又扩展了一些`x-*`属性来表达 UI,使得整个协议可以具备完整描述一个复杂表单的能力,具体 Schema 协议,参考[Schema](/api/shared/schema) API 文档 ### 递归渲染 何为递归渲染?递归渲染就是组件 A 在某些条件下会继续用组件 A 来渲染内容,看看以下伪代码: ```json { <---- RecursionField(条件:object;渲染权:RecursionField) "type":"object", "properties":{ "username":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input" }, "phone":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input", "x-validator":"phone" }, "email":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input", "x-validator":"email" }, "contacts":{ <---- RecursionField(条件:array;渲染权:RecursionField) "type":"array", "x-component":"ArrayTable", "items":{ <---- RecursionField(条件:object;渲染权:ArrayTable组件) "type":"object", "properties":{ "username":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input" }, "phone":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input", "x-validator":"phone" }, "email":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input", "x-validator":"email" }, } } } } } ``` @formily/react 递归渲染的入口是[SchemaField](/api/components/schema-field),但它内部实际是使用 [RecursionField](/api/components/recursion-field) 来渲染的,因为 JSON-Schema 就是一个递归型结构,所以 [RecursionField](/api/components/recursion-field) 在渲染的时候会从顶层 Schema 节点解析,如果是非 object 和 array 类型则直接渲染具体组件,如果是 object,则会遍历 properties 继续用 [RecursionField](/api/components/recursion-field) 渲染子级 Schema 节点。 这里有点特殊的情况是 array 类型的自增列表渲染,需要用户在自定义组件内使用[RecursionField](/api/components/recursion-field)进行递归渲染,因为自增列表的 UI 个性化定制程度很高,所以就把递归渲染权交给用户来渲染了,这样设计也能让协议驱动渲染变得更加灵活。 那 SchemaField 和 RecursionField 有啥差别呢?主要有两点: - SchemaField 是支持 Markup 语法的,它会提前解析 Markup 语法生成[JSON Schema](/api/shared/schema)移交给 RecursionField 渲染,所以 RecursionField 只能基于 [JSON Schema](/api/shared/schema) 渲染 - SchemaField 渲染的是整体的 Schema 协议,而 RecursionField 渲染的是局部 Schema 协议 ### 协议绑定 前面讲了模型绑定,而协议绑定则是将 Schema 协议转换成模型绑定的过程,因为 JSON-Schema 协议是 JSON 字符串,可离线存储的,而模型绑定则是内存间的绑定关系,是 Runtime 层的,比如`x-component`在 Schema 中是组件的字符串标识,但是在模型中的 component 则是需要组件引用,所以 JSON 字符串与 Runtime 层是需要转换的。然后我们就可以继续完善一下以上模型绑定的图: ![](https://img.alicdn.com/imgextra/i3/O1CN01jLCRxH1aa3V0x6nw4_!!6000000003345-55-tps-2200-1147.svg) 总结下来,在@formily/react 中,主要有 2 层绑定关系,Schema 绑定模型,模型绑定组件,实现绑定的胶水层就是@formily/react,需要注意的是,Schema 绑定字段模型之后,字段模型中是感知不到 Schema 的,比如要修改`enum`,就是修改字段模型中的`dataSource`属性了,总之,想要更新字段模型,参考[Field](//core.formilyjs.org/zh-CN/api/models/field),想要理解 Schema 与字段模型的映射关系可以参考[Schema](/api/shared/schema)文档 ## 三种开发模式 从[架构图](/guide/architecture)中我们其实已经看到整个@formily/react 是有三种开发模式的,对应不同用户: - JSX 开发模式 - JSON Schema 开发模式 - Markup Schema 开发模式 我们可以看看具体例子 #### JSX 开发模式 该模式主要是使用 Field/ArrayField/ObjectField/VoidField 组件 ```tsx import React from 'react' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { Input } from 'antd' const form = createForm() export default () => ( ) ``` #### JSON Schema 开发模式 该模式是给 SchemaField 的 schema 属性传递 JSON Schema 即可 ```tsx import React from 'react' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Input } from 'antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, }, }) export default () => ( ) ``` #### Markup Schema 开发模式 该模式算是一个对源码开发比较友好的 Schema 开发模式,同样是使用 SchemaField 组件。 因为用 JSON Schema 在 JSX 环境下很难得到最好的智能提示体验,而且也不方便维护,用标签的形式可维护性会更好,智能提示也很强。 Markup Schema 模式主要有以下几个特点: - 主要依赖 SchemaField.String/SchemaField.Array/SchemaField.Object...这类描述标签来表达 Schema - 每个描述标签都代表一个 Schema 节点,与 JSON-Schema 等价 - SchemaField 子节点不能随意插 UI 元素,因为 SchemaField 只会解析子节点的所有 Schema 描述标签,然后转换成 JSON Schema,最终交给[RecursionField](/api/components/recursion-field)渲染,如果想要插入 UI 元素,可以在 VoidField 上传`x-content`属性来插入 UI 元素 ```tsx import React from 'react' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Input } from 'antd' const form = createForm() const SchemaField = createSchemaField({ components: { Input, }, }) export default () => (
我不会被渲染
我会被渲染} />
) ``` ================================================ FILE: packages/react/docs/guide/index.md ================================================ # Introduction The core positioning of @formily/react is to realize a state binding relationship between ViewModel ([@formily/core](//core.formilyjs.org)) and components. It is not responsible for managing form data and form verification. It is only A rendering glue layer, but such a layer of glue is not dirty, it will elegantly decouple a lot of dirty logic and become maintainable. ## Ultra high performance With the responsive model of [@formily/core](//core.formilyjs.org), @formily/react can obtain super high performance advantages without any optimization, relying on tracking, accurate updates, on-demand rendering, let us The form of really does only need to focus on business logic, without considering performance issues. ## Out of the box @formily/react provides a series of React components, such as Field/ArrayField/ObjectField/VoidField. When using it, users only need to pass the component property to the Field component (supporting two-way binding conventions such as value/onChange). Quick access to @formily/react, the access cost is extremely low. ## JSON Schema Driver @formily/react provides protocol-driven components such as SchemaField. It is also driven by the standard JSON-Schema, so that form development can become more dynamic and configurable. What's more, we can achieve a protocol that allows multiple terminals Render the form. ## Scene Reuse With the help of protocol-driven capabilities, we can abstract a protocol fragment carrying business logic into a scene component to help users develop efficiently in certain scenes, such as scene components such as FormTab and FormStep. ## Smart tips Because formily is a complete Typescript project, users can develop on VSCode or WebStorm to get the maximum intelligent prompt experience ![img](https://img.alicdn.com/imgextra/i2/O1CN01yiREHk1X95KJPPz1c_!!6000000002880-2-tps-2014-868.png) ## Status observable Install [FormilyDevtools](https://chrome.google.com/webstore/detail/formily-devtools/kkocalmbfnplecdmbadaapgapdioecfm?hl=zh-CN) to observe the model status changes in real time and troubleshoot problems ![img](https://img.alicdn.com/imgextra/i4/O1CN01DSci5h1rAGfRafpXw_!!6000000005590-2-tps-2882-1642.png) ================================================ FILE: packages/react/docs/guide/index.zh-CN.md ================================================ # 介绍 @formily/react 的核心定位是将 ViewModel([@formily/core](//core.formilyjs.org))与组件实现一个状态绑定关系,它不负责管理表单数据,表单校验,它仅仅是一个渲染胶水层,但是这样一层胶水,并不脏,它会把很多脏逻辑优雅的解耦,变得可维护。 ## 超高性能 借助 [@formily/core](//core.formilyjs.org) 的响应式模型,@formily/react 无需做任何优化即可获得超高的性能优势,依赖追踪,精确更新,按需渲染,让我们的表单真正做到了只需关注业务逻辑,无需考虑性能问题。 ## 开箱即用 @formily/react 提供了一系列的 React 组件,比如 Field/ArrayField/ObjectField/VoidField,用户在使用的时候,只需要给 Field 组件传入 component 属性(支持 value/onChange 这样的双向绑定约定)即可快速接入@formily/react,接入成本极低。 ## 协议驱动 @formily/react 提供了 SchemaField 这样的协议驱动组件,同时是基于标准 JSON-Schema 的驱动,让表单开发可以变得更加动态化,可配置化,更甚,我们可以做到一份协议,让多端渲染表单。 ## 场景复用 借助协议驱动的能力,我们可以将一个携带业务逻辑的协议片段抽象成一个场景组件,帮助用户在某些场景上高效开发,比如 FormTab、FormStep 这类场景组件。 ## 智能提示 因为 formily 是完全的 Typescript 项目,所以用户在 VSCode 或 WebStorm 等上开发可以获得最大化的智能提示体验 ![img](https://img.alicdn.com/imgextra/i2/O1CN01yiREHk1X95KJPPz1c_!!6000000002880-2-tps-2014-868.png) ## 状态可观测 安装 [FormilyDevtools](https://chrome.google.com/webstore/detail/formily-devtools/kkocalmbfnplecdmbadaapgapdioecfm?hl=zh-CN) 可以实时观测模型状态变化,排查问题 ![img](https://img.alicdn.com/imgextra/i4/O1CN01DSci5h1rAGfRafpXw_!!6000000005590-2-tps-2882-1642.png) ================================================ FILE: packages/react/docs/index.md ================================================ --- title: Formily-Alibaba unified front-end form solution order: 10 hero: title: React Library desc: Alibaba Unified Form Solution actions: - text: Home Site link: //formilyjs.org - text: Development Guide link: /guide features: - icon: https://img.alicdn.com/imgextra/i1/O1CN01bHdrZJ1rEOESvXEi5_!!6000000005599-55-tps-800-800.svg title: Ultra High Performance desc: Dependency tracking, efficient update, on-demand rendering - icon: https://img.alicdn.com/imgextra/i2/O1CN016i72sH1c5wh1kyy9U_!!6000000003550-55-tps-800-800.svg title: Out Of The Box desc: The component status is automatically bound, and the access cost is extremely low - icon: https://img.alicdn.com/imgextra/i3/O1CN01JHzg8U1FZV5Mvt012_!!6000000000501-55-tps-800-800.svg title: JSON Schema Driver desc: Standard JSON-Schema - icon: https://img.alicdn.com/imgextra/i3/O1CN0194OqFF1ui6mMT4g7O_!!6000000006070-55-tps-800-800.svg title: Scene Reuse desc: Based on protocol-driven, abstract scene components - icon: https://img.alicdn.com/imgextra/i4/O1CN018vDmpl2186xdLu6KI_!!6000000006939-55-tps-800-800.svg title: Debugging Friendly desc: Natural docking with Formily DevTools - icon: https://img.alicdn.com/imgextra/i4/O1CN01u6jHgs1ZMwXpjAYnh_!!6000000003181-55-tps-800-800.svg title: Smart Tips desc: Embrace Typescript footer: Open-source MIT Licensed | Copyright © 2019-present
Powered by self --- ## Installation ```bash $ npm install --save @formily/core @formily/react ``` ## Quick start ```tsx /** * defaultShowCode: true */ import React, { useMemo } from 'react' import { createForm, setValidateLanguage } from '@formily/core' import { FormProvider, FormConsumer, Field, useField, observer, } from '@formily/react' import { Input, Form } from 'antd' // FormItem UI component const FormItem = observer(({ children }) => { const field = useField() return ( {children} ) }) /* * The above logic has been implemented in @formily/antd, and there is no need to rewrite it in actual use */ //Switch the built-in check internationalization copy to English setValidateLanguage('en') export default () => { const form = useMemo(() => createForm({ validateFirst: true })) const createPasswordEqualValidate = (equalName) => (field) => { if ( form.values.confirm_password && field.value && form.values[equalName] !== field.value ) { field.selfErrors = ['Password does not match Confirm Password.'] } else { field.selfErrors = [] } } return (
            
              {(form) => JSON.stringify(form.values, null, 2)}
            
          
) } ``` ================================================ FILE: packages/react/docs/index.zh-CN.md ================================================ --- title: Formily - 阿里巴巴统一前端表单解决方案 order: 10 hero: title: React Library desc: 阿里巴巴统一前端表单解决方案 actions: - text: 主站文档 link: //formilyjs.org - text: 开发指南 link: /zh-CN/guide features: - icon: https://img.alicdn.com/imgextra/i1/O1CN01bHdrZJ1rEOESvXEi5_!!6000000005599-55-tps-800-800.svg title: 超高性能 desc: 依赖追踪,高效更新,按需渲染 - icon: https://img.alicdn.com/imgextra/i2/O1CN016i72sH1c5wh1kyy9U_!!6000000003550-55-tps-800-800.svg title: 开箱即用 desc: 组件状态自动绑定,接入成本极低 - icon: https://img.alicdn.com/imgextra/i3/O1CN01JHzg8U1FZV5Mvt012_!!6000000000501-55-tps-800-800.svg title: 协议驱动 desc: 标准JSON-Schema - icon: https://img.alicdn.com/imgextra/i3/O1CN0194OqFF1ui6mMT4g7O_!!6000000006070-55-tps-800-800.svg title: 场景复用 desc: 基于协议驱动,抽象场景组件 - icon: https://img.alicdn.com/imgextra/i4/O1CN018vDmpl2186xdLu6KI_!!6000000006939-55-tps-800-800.svg title: 调试友好 desc: 天然对接Formily DevTools - icon: https://img.alicdn.com/imgextra/i4/O1CN01u6jHgs1ZMwXpjAYnh_!!6000000003181-55-tps-800-800.svg title: 智能提示 desc: 拥抱Typescript footer: Open-source MIT Licensed | Copyright © 2019-present
Powered by self --- ## 安装 ```bash $ npm install --save @formily/core @formily/react ``` ## 快速开始 ```tsx /** * defaultShowCode: true */ import React, { useMemo } from 'react' import { createForm, setValidateLanguage } from '@formily/core' import { FormProvider, FormConsumer, Field, useField, observer, } from '@formily/react' import { Input, Form } from 'antd' // FormItem UI组件 const FormItem = observer(({ children }) => { const field = useField() return ( {children} ) }) /* * 以上逻辑都已经在 @formily/antd 中实现,实际使用无需重复编写 */ //切换内置校验国际化文案为英文 setValidateLanguage('en') export default () => { const form = useMemo(() => createForm({ validateFirst: true })) const createPasswordEqualValidate = (equalName) => (field) => { if ( form.values.confirm_password && field.value && form.values[equalName] !== field.value ) { field.selfErrors = ['Password does not match Confirm Password.'] } else { field.selfErrors = [] } } return (
            
              {(form) => JSON.stringify(form.values, null, 2)}
            
          
) } ``` ================================================ FILE: packages/react/package.json ================================================ { "name": "@formily/react", "version": "2.3.7", "license": "MIT", "main": "lib", "module": "esm", "umd:main": "dist/formily.react.umd.production.js", "unpkg": "dist/formily.react.umd.production.js", "jsdelivr": "dist/formily.react.umd.production.js", "jsnext:main": "esm", "repository": { "type": "git", "url": "git+https://github.com/alibaba/formily.git" }, "types": "esm/index.d.ts", "bugs": { "url": "https://github.com/alibaba/formily/issues" }, "homepage": "https://github.com/alibaba/formily#readme", "engines": { "npm": ">=3.0.0" }, "scripts": { "start": "dumi dev", "build": "rimraf -rf lib esm dist && npm run build:cjs && npm run build:esm && npm run build:umd", "build:cjs": "tsc --project tsconfig.build.json", "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm", "build:umd": "rollup --config", "build:docs": "dumi build" }, "peerDependencies": { "@types/react": ">=16.8.0", "@types/react-dom": ">=16.8.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", "react-is": ">=16.8.0" }, "peerDependenciesMeta": { "@types/react": { "optional": true }, "@types/react-dom": { "optional": true } }, "devDependencies": { "dumi": "^1.1.0-rc.8" }, "dependencies": { "@formily/core": "2.3.7", "@formily/json-schema": "2.3.7", "@formily/reactive": "2.3.7", "@formily/reactive-react": "2.3.7", "@formily/shared": "2.3.7", "@formily/validator": "2.3.7", "hoist-non-react-statics": "^3.3.2" }, "publishConfig": { "access": "public" }, "gitHead": "ac79c196ae9324889aca5e0501146f9b37b04283" } ================================================ FILE: packages/react/rollup.config.js ================================================ import baseConfig from '../../scripts/rollup.base.js' export default baseConfig('formily.react', 'Formily.React') ================================================ FILE: packages/react/src/__tests__/expression.spec.tsx ================================================ import React from 'react' import { render, waitFor } from '@testing-library/react' import { createForm } from '@formily/core' import { FormProvider, ExpressionScope, createSchemaField, useField, Field, } from '..' test('expression scope', async () => { const Container = (props) => { return ( {props.children} ) } const Input = (props) =>
{props.value}
const SchemaField = createSchemaField({ components: { Container, Input, }, }) const form = createForm() const { getByTestId } = render( ) expect(getByTestId('test-input').textContent).toBe( 'this is inner scope value this is outer scope value' ) }) test('x-compile-omitted', async () => { const form = createForm() const SchemaField = createSchemaField({ components: { Input: (props) => (
{props.aa} {useField().title} {props.extra}
), }, }) const { queryByTestId } = render( ) await waitFor(() => { expect(queryByTestId('input')?.textContent).toBe('{{fake}}123321extra') }) }) test('field hidden & visible', async () => { const form = createForm({ initialValues: { empty: null } }) const { findByTestId } = render(
) await findByTestId('testid') // expect(form.fields.empty.hidden).toBe(false) expect(form.fields.empty.value).toBe(null) form.fields.empty.hidden = true expect(form.fields.empty.hidden).toBe(true) expect(form.fields.empty.value).toBe(null) form.fields.empty.hidden = false expect(form.fields.empty.hidden).toBe(false) expect(form.fields.empty.value).toBe(null) // expect(form.fields.empty.visible).toBe(true) expect(form.fields.empty.value).toBe(null) form.fields.empty.visible = false expect(form.fields.empty.visible).toBe(false) expect(form.fields.empty.value).toBe(undefined) form.fields.empty.visible = true expect(form.fields.empty.visible).toBe(true) expect(form.fields.empty.value).toBe(null) }) ================================================ FILE: packages/react/src/__tests__/field.spec.tsx ================================================ import React from 'react' import { act } from 'react-dom/test-utils' import { render, fireEvent, waitFor } from '@testing-library/react' import { createForm, onFieldUnmount, isArrayField } from '@formily/core' import { isField, Field as FieldType, isVoidField, onFieldChange, } from '@formily/core' import { FormProvider, ArrayField, ObjectField, VoidField, Field, useField, useFormEffects, observer, connect, mapProps, mapReadPretty, } from '..' import { ReactiveField } from '../components/ReactiveField' import { expectThrowError } from './shared' type InputProps = { value?: string onChange?: (...args: any) => void } type CustomProps = { list?: string[] } const Decorator = (props) =>
{props.children}
const Input: React.FC> = (props) => ( ) const Normal = () =>
test('render field', async () => { const form = createForm() const onChange = jest.fn() const { getByTestId, queryByTestId, unmount } = render(
{() => (
)}
) expect(form.mounted).toBeTruthy() expect(form.query('aa').take().mounted).toBeTruthy() expect(form.query('bb').take().mounted).toBeTruthy() expect(form.query('cc').take().mounted).toBeTruthy() expect(form.query('dd').take().mounted).toBeTruthy() fireEvent.change(getByTestId('aa'), { target: { value: '123', }, }) fireEvent.change(getByTestId('kk'), { target: { value: '123', }, }) expect(onChange).toBeCalledTimes(1) expect(getByTestId('bb-children')).not.toBeUndefined() expect(getByTestId('dd-children')).not.toBeUndefined() expect(queryByTestId('ee')).toBeNull() expect(form.query('aa').get('value')).toEqual('123') expect(form.query('kk').get('value')).toEqual('123') unmount() }) test('render field no context', () => { expectThrowError(() => { return ( <> {() =>
}
) }) }) test('ReactiveField', () => { render() render({() =>
}
) }) test('useAttach basic', async () => { const form = createForm() const MyComponent = (props: any) => { return ( ) } const { rerender } = render() expect(form.query('aa').take().mounted).toBeTruthy() rerender() await waitFor(() => { expect(form.query('aa').take().mounted).toBeFalsy() expect(form.query('bb').take().mounted).toBeTruthy() }) }) test('useAttach with array field', async () => { const form = createForm() const MyComponent = () => { return ( {(field) => { return field.value.map((val, index) => { return ( ) }) }} ) } render() await waitFor(() => { expect(form.query('array.0.input').take().mounted).toBeTruthy() expect(form.query('array.1.input').take().mounted).toBeTruthy() }) form.query('array').take((field) => { if (isArrayField(field)) { field.moveDown(0) } }) await waitFor(() => { expect(form.query('array.0.input').take().mounted).toBeTruthy() expect(form.query('array.1.input').take().mounted).toBeTruthy() }) }) test('useFormEffects', async () => { const form = createForm() const CustomField = observer(() => { const field = useField() useFormEffects(() => { onFieldChange('aa', ['value'], (target) => { if (isVoidField(target)) return field.setValue(target.value) }) }) return
{field.value}
}) act(async () => { const { queryByTestId, rerender } = render( ) expect(queryByTestId('custom-value')?.textContent).toEqual('') form.query('aa').take((aa) => { if (isField(aa)) { aa.setValue('123') } }) await waitFor(() => { expect(queryByTestId('custom-value')?.textContent).toEqual('123') }) rerender( ) }) }) test('connect', async () => { const CustomField = connect( (props: CustomProps) => { return
{props.list}
}, mapProps({ value: 'list', loading: true }, (props, field) => { return { ...props, mounted: field.mounted ? 1 : 2, } }), mapReadPretty(() =>
read pretty
) ) const BaseComponent = (props: any) => { return
{props.value}
} BaseComponent.displayName = 'BaseComponent' const CustomField2 = connect( BaseComponent, mapProps({ value: true, loading: true }), mapReadPretty(() =>
read pretty
) ) const form = createForm() const MyComponent = () => { return ( ) } const { queryByText } = render() form.query('aa').take((field) => { field.setState((state) => { state.value = '123' }) }) await waitFor(() => { expect(queryByText('123')).toBeVisible() }) form.query('aa').take((field) => { if (!isField(field)) return field.readPretty = true }) await waitFor(() => { expect(queryByText('123')).toBeNull() expect(queryByText('read pretty')).toBeVisible() }) }) test('fields unmount and validate', async () => { const fn = jest.fn() const form = createForm({ initialValues: { parent: { type: 'mounted', }, }, effects: () => { onFieldUnmount('parent.child', () => { fn() }) }, }) const Parent = observer(() => { const field = useField() if (field.value.type === 'mounted') { return ( ) } return
}) const MyComponent = () => { return ( ) } render() try { await form.validate() } catch {} expect(form.invalid).toBeTruthy() form.query('parent').take((field) => { field.setState((state) => { state.value.type = 'unmounted' }) }) await waitFor(() => { expect(fn.mock.calls.length).toBe(1) }) try { await form.validate() } catch {} expect(form.invalid).toBeTruthy() }) ================================================ FILE: packages/react/src/__tests__/form.spec.tsx ================================================ import React from 'react' import { render } from '@testing-library/react' import { createForm } from '@formily/core' import { FormProvider, ObjectField, VoidField, Field } from '../' import { FormConsumer } from '../components' import { useParentForm } from '../hooks' test('render form', () => { const form = createForm() render( {(form) => `${form.mounted}`} ) expect(form.mounted).toBeTruthy() }) const DisplayParentForm: React.FC< React.PropsWithChildren> > = (props) => { return
{useParentForm()?.displayName}
} test('useParentForm', () => { const form = createForm() const { queryByTestId } = render( ) expect(queryByTestId('111').textContent).toBe('ObjectField') expect(queryByTestId('222').textContent).toBe('Form') expect(queryByTestId('333').textContent).toBe('Form') }) ================================================ FILE: packages/react/src/__tests__/schema.json.spec.tsx ================================================ import React from 'react' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '../index' import { Schema } from '@formily/json-schema' import { render } from '@testing-library/react' const Input = ({ value, onChange }) => { return } import { Button, Rate } from 'antd' import { SearchOutlined, DollarOutlined } from '@ant-design/icons' describe('json schema field', () => { test('string field', () => { const form = createForm() const SchemaField = createSchemaField({ components: { Input, }, }) const { queryByTestId } = render( ) expect(queryByTestId('input')).toBeVisible() expect(queryByTestId('input')?.getAttribute('value')).toEqual('123') }) test('object field', () => { const form = createForm() const SchemaField = createSchemaField({ components: { Input, }, }) const { queryByTestId } = render( ) expect(queryByTestId('input')).toBeVisible() }) test('x-component-props children', () => { const form = createForm() const Text: React.FC = ({ children }) => { return
{children}
} const SchemaField = createSchemaField({ components: { Text, }, }) const { queryByTestId } = render( ) expect(queryByTestId('children-test')).toBeVisible() expect(queryByTestId('children-test')?.innerHTML).toEqual('children') }) test('x-content', async () => { const form = createForm() const Text: React.FC = ({ children }) => { return
{children}
} const SchemaField = createSchemaField({ components: { Text, }, }) const { queryByTestId } = render( ) expect(queryByTestId('content-test')).toBeVisible() expect(queryByTestId('content-test')?.innerHTML).toEqual('content') }) test('x-slot-node', () => { const form = createForm() const SchemaField = createSchemaField({ components: { SearchOutlined, Button, }, }) const { queryByTestId } = render( ) const button = queryByTestId('button') const icon = queryByTestId('icon') expect(button).toContainElement(icon) }) test('x-slot-node render prop', async () => { const form = createForm() const SchemaField = createSchemaField({ components: { Rate, DollarOutlined, }, }) const { queryByRole, queryAllByTestId } = render( ) const rate = queryByRole('radiogroup') expect(rate).toBeVisible() const icons = queryAllByTestId('icon') expect(icons).toHaveLength(10) icons.forEach((icon) => { expect(rate).toContainElement(icon) }) const style = window.getComputedStyle(icons[0]) const fontSize = style.fontSize expect(fontSize).toBe('20px') }) }) ================================================ FILE: packages/react/src/__tests__/schema.markup.spec.tsx ================================================ import React from 'react' import { createForm } from '@formily/core' import { FormProvider, createSchemaField, useFieldSchema, useField, RecursionField, RecordScope, RecordsScope, } from '../index' import { render, fireEvent, waitFor, act } from '@testing-library/react' const Input: React.FC<{ value?: string onChange?: (...args: any) => void [key: string]: any }> = ({ value, onChange, ...others }) => { return ( ) } describe('markup schema field', () => { test('string', () => { const form = createForm() const SchemaField = createSchemaField({ components: { Input, }, }) const { queryByTestId } = render( ) expect(queryByTestId('input')).toBeVisible() }) test('boolean', () => { const form = createForm() const SchemaField = createSchemaField({ components: { Input, }, }) const { queryByTestId } = render( ) expect(queryByTestId('input')).toBeVisible() }) test('number', () => { const form = createForm() const SchemaField = createSchemaField({ components: { Input, }, }) const { queryByTestId } = render( ) expect(queryByTestId('input')).toBeVisible() }) test('date', () => { const form = createForm() const SchemaField = createSchemaField({ components: { Input, }, }) const { queryByTestId } = render( ) expect(queryByTestId('input')).toBeVisible() }) test('datetime', () => { const form = createForm() const SchemaField = createSchemaField({ components: { Input, }, }) const { queryByTestId } = render( ) expect(queryByTestId('input')).toBeVisible() }) test('void', () => { const form = createForm() const VoidComponent = (props) => { return
{props.children}
} const SchemaField = createSchemaField({ components: { VoidComponent, }, }) const { queryByTestId } = render( ) expect(queryByTestId('void-component')).toBeVisible() }) test('array', () => { const form = createForm() const SchemaField = createSchemaField({ components: { Input, }, }) render( ) }) test('other', () => { const form = createForm() const SchemaField = createSchemaField({ components: { Input, }, }) render( ) }) test('no parent', () => { const form = createForm() const SchemaField = createSchemaField({ components: { Input, }, }) render( ) }) test('props children', () => { const form = createForm() const Text = (props) => { return
{props.children}
} const SchemaField = createSchemaField({ components: { Text, }, }) const { queryByTestId } = render( ) expect(queryByTestId('children-test')).toBeVisible() expect(queryByTestId('children-test').innerHTML).toEqual('props') }) test('x-content', () => { const form = createForm() const Text = (props) => { return
{props.children}
} const SchemaField = createSchemaField({ components: { Text, }, }) const { queryByTestId } = render( ) expect(queryByTestId('content-test')).toBeVisible() expect(queryByTestId('content-test').innerHTML).toEqual('content') }) }) describe('recursion field', () => { test('onlyRenderProperties', () => { const form = createForm() const CustomObject: React.FC = () => { const schema = useFieldSchema() return (
) } const CustomObject2: React.FC = () => { const field = useField() const schema = useFieldSchema() return (
) } const SchemaField = createSchemaField({ components: { Input, CustomObject, CustomObject2, }, }) const { queryAllByTestId } = render( ) expect(queryAllByTestId('input').length).toEqual(3) expect(queryAllByTestId('object').length).toEqual(1) expect(queryAllByTestId('only-properties').length).toEqual(2) }) test('mapProperties', () => { const form = createForm() const CustomObject: React.FC = () => { const schema = useFieldSchema() return (
{ schema.default = '123' return schema }} />
) } const CustomObject2: React.FC = () => { const schema = useFieldSchema() return (
{ return null }} />
) } const SchemaField = createSchemaField({ components: { Input, CustomObject, CustomObject2, }, }) const { queryAllByTestId } = render( ) expect(queryAllByTestId('input').length).toEqual(2) expect(queryAllByTestId('input')[0].getAttribute('value')).toEqual('123') expect(queryAllByTestId('input')[1].getAttribute('value')).toEqual('') }) test('filterProperties', () => { const form = createForm() const CustomObject: React.FC = () => { const schema = useFieldSchema() return (
{ if (schema['x-component'] === 'Input') return false return true }} />
) } const CustomObject2: React.FC = () => { const schema = useFieldSchema() return (
{ if (schema['x-component'] === 'Input') return true return false }} />
) } const SchemaField = createSchemaField({ components: { Input, CustomObject, CustomObject2, }, }) const { queryAllByTestId } = render( ) expect(queryAllByTestId('input').length).toEqual(1) expect(queryAllByTestId('object').length).toEqual(2) }) test('onlyRenderSelf', () => { const form = createForm() const CustomObject: React.FC = () => { const schema = useFieldSchema() return (
) } const SchemaField = createSchemaField({ components: { Input, CustomObject, }, }) const { queryAllByTestId } = render( ) expect(queryAllByTestId('input').length).toEqual(0) expect(queryAllByTestId('object').length).toEqual(1) }) test('illegal schema', () => { const form = createForm() const CustomObject: React.FC = () => { return (
) } const CustomObject2: React.FC = () => { return (
) } const SchemaField = createSchemaField({ components: { Input, CustomObject, CustomObject2, }, }) const { queryByTestId } = render( ) expect(queryByTestId('input')).toBeNull() }) }) test('schema reactions', async () => { const form = createForm() const SchemaField = createSchemaField({ components: { Input, }, }) const { queryByTestId } = render( ) expect(queryByTestId('bbb')).toBeNull() fireEvent.change(queryByTestId('aaa'), { target: { value: '123', }, }) await waitFor(() => { expect(queryByTestId('bbb')).toBeVisible() }) expect(queryByTestId('ccc')).toBeNull() fireEvent.change(queryByTestId('bbb'), { target: { value: '123', }, }) await waitFor(() => { expect(queryByTestId('ccc')).toBeVisible() }) }) test('expression scope', async () => { let aa = false let bb = false let cc = false const form = createForm() const SchemaField = createSchemaField({ components: { Input, }, scope: { aa() { aa = true }, }, }) const scope = { bb() { bb = true }, cc() { cc = true }, } const schema = { type: 'object', properties: { aa: { type: 'string', 'x-component': 'Input', 'x-reactions': '{{ aa }}', }, bb: { type: 'string', 'x-component': 'Input', 'x-reactions': '{{ bb }}', }, cc: { type: 'string', 'x-component': 'Input', 'x-reactions': { dependencies: ['aa'], fulfill: { run: 'cc()', }, }, }, }, } const { queryByTestId } = render( ) await waitFor(() => queryByTestId('aa')) expect(aa).toBeTruthy() await waitFor(() => queryByTestId('bb')) expect(bb).toBeTruthy() await waitFor(() => queryByTestId('cc')) expect(cc).toBeTruthy() }) test('expression x-content', async () => { const form = createForm() const SchemaField = createSchemaField({ components: { Wrapper: (props) => props.children, }, scope: { child:
, }, }) const { queryByTestId } = render( ) await waitFor(() => { expect(queryByTestId('child')).not.toBeUndefined() }) }) test('expression x-visible', async () => { const form = createForm() const SchemaField = createSchemaField({ components: { AAA: () =>
AAA
, BBB: () =>
BBB
, }, }) const { queryByText } = render( ) await waitFor(() => { expect(queryByText('BBB')).toBeNull() }) act(() => { form.values.aaa = 123 }) await waitFor(() => { expect(queryByText('BBB')).not.toBeNull() }) }) test('expression x-value', async () => { const form = createForm({ values: { aaa: 1, }, }) const SchemaField = createSchemaField({ components: { Text: (props) =>
{props.value}
, }, }) const { queryByText } = render( ) await waitFor(() => { expect(queryByText('10')).not.toBeNull() }) act(() => { form.values.aaa = 10 }) await waitFor(() => { expect(queryByText('100')).not.toBeNull() }) }) test('nested update component props with expression', async () => { const form = createForm({ values: { aaa: 'xxx', }, }) const SchemaField = createSchemaField({ components: { Text: (props) =>
{props.aa?.bb?.cc}
, }, }) const { queryByText } = render( ) await waitFor(() => { expect(queryByText('xxx')).not.toBeNull() }) act(() => { form.values.aaa = '10' }) await waitFor(() => { expect(queryByText('10')).not.toBeNull() }) }) test('nested update component props with x-reactions', async () => { const form = createForm({ values: { aaa: 'xxx', }, }) const SchemaField = createSchemaField({ components: { Text: (props) =>
{props.aa?.bb?.cc}
, }, }) const { queryByText } = render( ) await waitFor(() => { expect(queryByText('xxx')).not.toBeNull() }) act(() => { form.values.aaa = '10' }) await waitFor(() => { expect(queryByText('10')).not.toBeNull() }) }) test('schema x-validator/required', async () => { const form = createForm({ values: { aaa: 'xxx', }, }) const SchemaField = createSchemaField({ components: { Input: () =>
, }, }) render( ) await waitFor(() => { expect(form.query('input').get('required')).toBeTruthy() expect(form.query('input').get('validator')).toEqual([ { required: true }, { format: 'email' }, ]) }) }) test('schema x-reactions when undefined', async () => { const form = createForm({ values: { aaa: 'xxx', }, }) const SchemaField = createSchemaField({ components: { Input: () =>
, Select: () =>
, }, }) const { queryByTestId } = render( ) await waitFor(() => { expect(queryByTestId('input')).not.toBeNull() expect(queryByTestId('select')).toBeNull() }) }) test('void field children', async () => { const form = createForm() const SchemaField = createSchemaField({ components: { Button: (props) => (
{props.children || 'placeholder'}
), }, }) const { queryByTestId } = render( ) await waitFor(() => { expect(queryByTestId('btn')?.textContent).toBe('placeholder') }) }) test('x-reactions runner for target', async () => { const form = createForm() const getTarget = jest.fn() const SchemaField = createSchemaField({ components: { Input: () =>
, Button: (props) => ( ), }, scope: { getTarget, }, }) const { getByTestId } = render( ) fireEvent.click(getByTestId('btn')) await waitFor(() => { expect(getByTestId('btn').textContent).toBe('Click 123') expect(getTarget).toBeCalledWith('333') expect(getTarget).toBeCalledTimes(1) }) }) test('multi x-reactions isolate effect', async () => { const form = createForm() const otherEffect = jest.fn() const SchemaField = createSchemaField({ components: { Input: () =>
, Button: (props) => ( ), }, }) const { getByTestId, queryByTestId } = render( ) await waitFor(() => { expect(queryByTestId('input')).toBeNull() }) fireEvent.click(getByTestId('btn')) await waitFor(() => { expect(getByTestId('btn').textContent).toBe('Click 123') expect(getByTestId('input')).not.toBeNull() expect(otherEffect).toBeCalledTimes(1) }) }) test('nested record scope', async () => { const form = createForm() const SchemaField = createSchemaField({ components: { Text: (props) =>
{props.text}
, }, }) const { queryByTestId } = render( ({ bb: '321' })} getIndex={() => 1}> ({ aa: '123' })} getIndex={() => 2}> ) await waitFor(() => { expect(queryByTestId('text')?.textContent).toBe('12332121') }) }) test('literal record scope', async () => { const form = createForm() const SchemaField = createSchemaField({ components: { Text: (props) =>
{props.text}
, }, }) const { queryByTestId } = render( '123'} getIndex={() => 2}> ) await waitFor(() => { expect(queryByTestId('text')?.textContent).toBe('1232') }) }) test('records scope', async () => { const form = createForm() const SchemaField = createSchemaField({ components: { Text: (props) =>
{props.text}
, }, }) const { queryByTestId } = render( [1, 2, 3]}> ) await waitFor(() => { expect(queryByTestId('text')?.textContent).toBe('3') }) }) test('propsRecursion as true', () => { const form = createForm() const CustomObject: React.FC = () => { const schema = useFieldSchema() return (
{ if (schema['x-component'] === 'Input') { return false } return true }} />
) } const SchemaField = createSchemaField({ components: { Input, CustomObject, }, }) const { queryAllByTestId } = render( ) expect(queryAllByTestId('input').length).toEqual(0) expect(queryAllByTestId('object').length).toEqual(1) }) test('propsRecursion as empty', () => { const form = createForm() const CustomObject: React.FC = () => { const schema = useFieldSchema() return (
{ if (schema['x-component'] === 'Input') { return false } return true }} />
) } const SchemaField = createSchemaField({ components: { Input, CustomObject, }, }) const { queryAllByTestId } = render( ) expect(queryAllByTestId('input').length).toEqual(1) expect(queryAllByTestId('object').length).toEqual(1) }) ================================================ FILE: packages/react/src/__tests__/shared.tsx ================================================ import React, { Component, Fragment } from 'react' import { render } from '@testing-library/react' export class ErrorBoundary extends Component { state = { error: null, } componentDidCatch(error: Error) { this.setState({ error, }) } render() { if (this.state.error) { return (
{this.state.error.message}
) } return {this.props.children} } } export const expectThrowError = (callback: () => React.ReactElement) => { const { queryByTestId } = render({callback()}) expect(queryByTestId('error-boundary-message')).toBeVisible() } ================================================ FILE: packages/react/src/components/ArrayField.tsx ================================================ import React from 'react' import { ArrayField as ArrayFieldType } from '@formily/core' import { useForm, useField } from '../hooks' import { useAttach } from '../hooks/useAttach' import { FieldContext } from '../shared' import { JSXComponent, IFieldProps } from '../types' import { ReactiveField } from './ReactiveField' export const ArrayField = ( props: IFieldProps ) => { const form = useForm() const parent = useField() const field = useAttach( form.createArrayField({ basePath: parent?.address, ...props, }) ) return ( {props.children} ) } ArrayField.displayName = 'ArrayField' ================================================ FILE: packages/react/src/components/ExpressionScope.tsx ================================================ import React, { useContext } from 'react' import { lazyMerge } from '@formily/shared' import { SchemaExpressionScopeContext } from '../shared' import { IExpressionScopeProps, ReactFC } from '../types' export const ExpressionScope: ReactFC = (props) => { const scope = useContext(SchemaExpressionScopeContext) return ( {props.children} ) } ================================================ FILE: packages/react/src/components/Field.tsx ================================================ import React, { useEffect } from 'react' import { useField, useForm } from '../hooks' import { ReactiveField } from './ReactiveField' import { FieldContext } from '../shared' import { JSXComponent, IFieldProps } from '../types' export const Field = ( props: IFieldProps ) => { const form = useForm() const parent = useField() const field = form.createField({ basePath: parent?.address, ...props }) useEffect(() => { field?.onMount() return () => { field?.onUnmount() } }, [field]) return ( {props.children} ) } Field.displayName = 'Field' ================================================ FILE: packages/react/src/components/FormConsumer.tsx ================================================ import React, { Fragment } from 'react' import { isFn } from '@formily/shared' import { observer } from '@formily/reactive-react' import { useForm } from '../hooks' import { IFormSpyProps, ReactFC } from '../types' export const FormConsumer: ReactFC = observer((props) => { const children = isFn(props.children) ? props.children(useForm()) : null return {children} }) FormConsumer.displayName = 'FormConsumer' ================================================ FILE: packages/react/src/components/FormProvider.tsx ================================================ import React from 'react' import { useAttach } from '../hooks/useAttach' import { FormContext, ContextCleaner } from '../shared' import { IProviderProps, ReactFC } from '../types' export const FormProvider: ReactFC = (props) => { const form = useAttach(props.form) return ( {props.children} ) } FormProvider.displayName = 'FormProvider' ================================================ FILE: packages/react/src/components/ObjectField.tsx ================================================ import React from 'react' import { ObjectField as ObjectFieldType } from '@formily/core' import { useForm, useField } from '../hooks' import { useAttach } from '../hooks/useAttach' import { ReactiveField } from './ReactiveField' import { FieldContext } from '../shared' import { JSXComponent, IFieldProps } from '../types' export const ObjectField = ( props: IFieldProps ) => { const form = useForm() const parent = useField() const field = useAttach( form.createObjectField({ basePath: parent?.address, ...props }) ) return ( {props.children} ) } ObjectField.displayName = 'ObjectField' ================================================ FILE: packages/react/src/components/ReactiveField.tsx ================================================ import React, { Fragment, useContext } from 'react' import { toJS } from '@formily/reactive' import { observer } from '@formily/reactive-react' import { FormPath, isFn } from '@formily/shared' import { isVoidField, GeneralField, Form } from '@formily/core' import { SchemaComponentsContext } from '../shared' import { RenderPropsChildren } from '../types' interface IReactiveFieldProps { field: GeneralField children?: RenderPropsChildren } const mergeChildren = ( children: RenderPropsChildren, content: React.ReactNode ) => { if (!children && !content) return if (isFn(children)) return return ( {children} {content} ) } const isValidComponent = (target: any) => target && (typeof target === 'object' || typeof target === 'function') const renderChildren = ( children: RenderPropsChildren, field?: GeneralField, form?: Form ) => (isFn(children) ? children(field, form) : children) const ReactiveInternal: React.FC = (props) => { const components = useContext(SchemaComponentsContext) if (!props.field) { return {renderChildren(props.children)} } const field = props.field const content = mergeChildren( renderChildren(props.children, field, field.form), field.content ?? field.componentProps.children ) if (field.display !== 'visible') return null const getComponent = (target: any) => { return isValidComponent(target) ? target : FormPath.getIn(components, target) ?? target } const renderDecorator = (children: React.ReactNode) => { if (!field.decoratorType) { return {children} } return React.createElement( getComponent(field.decoratorType), toJS(field.decoratorProps), children ) } const renderComponent = () => { if (!field.componentType) return content const value = !isVoidField(field) ? field.value : undefined const onChange = !isVoidField(field) ? (...args: any[]) => { field.onInput(...args) field.componentProps?.onChange?.(...args) } : field.componentProps?.onChange const onFocus = !isVoidField(field) ? (...args: any[]) => { field.onFocus(...args) field.componentProps?.onFocus?.(...args) } : field.componentProps?.onFocus const onBlur = !isVoidField(field) ? (...args: any[]) => { field.onBlur(...args) field.componentProps?.onBlur?.(...args) } : field.componentProps?.onBlur const disabled = !isVoidField(field) ? field.pattern === 'disabled' || field.pattern === 'readPretty' : undefined const readOnly = !isVoidField(field) ? field.pattern === 'readOnly' : undefined return React.createElement( getComponent(field.componentType), { disabled, readOnly, ...toJS(field.componentProps), value, onChange, onFocus, onBlur, }, content ) } return renderDecorator(renderComponent()) } ReactiveInternal.displayName = 'ReactiveField' export const ReactiveField = observer(ReactiveInternal, { forwardRef: true, }) ================================================ FILE: packages/react/src/components/RecordScope.tsx ================================================ import React from 'react' import { lazyMerge } from '@formily/shared' import { ExpressionScope } from './ExpressionScope' import { ReactFC, IRecordScopeProps } from '../types' import { useExpressionScope } from '../hooks' export const RecordScope: ReactFC = (props) => { const scope = useExpressionScope() return ( {props.children} ) } ================================================ FILE: packages/react/src/components/RecordsScope.tsx ================================================ import React from 'react' import { ExpressionScope } from './ExpressionScope' import { ReactFC, IRecordsScopeProps } from '../types' export const RecordsScope: ReactFC = (props) => { return ( {props.children} ) } ================================================ FILE: packages/react/src/components/RecursionField.tsx ================================================ import React, { Fragment, useMemo } from 'react' import { FormPath, isBool, isFn, isValid } from '@formily/shared' import { GeneralField } from '@formily/core' import { Schema } from '@formily/json-schema' import { SchemaContext } from '../shared' import { IRecursionFieldProps, ReactFC } from '../types' import { useField, useExpressionScope } from '../hooks' import { ObjectField } from './ObjectField' import { ArrayField } from './ArrayField' import { Field } from './Field' import { VoidField } from './VoidField' import { ExpressionScope } from './ExpressionScope' import { observable } from '@formily/reactive' const useFieldProps = (schema: Schema) => { const scope = useExpressionScope() return schema.toFieldProps({ scope, }) as any } const useBasePath = (props: IRecursionFieldProps) => { const parent = useField() if (props.onlyRenderProperties) { return props.basePath || parent?.address.concat(props.name) } return props.basePath || parent?.address } export const RecursionField: ReactFC = (props) => { const basePath = useBasePath(props) const fieldSchema = useMemo(() => new Schema(props.schema), [props.schema]) const fieldProps = useFieldProps(fieldSchema) const renderSlots = (innerSchema, key) => { const slot = innerSchema['x-slot-node'] const { target, isRenderProp } = slot if (isRenderProp) { const args = observable({ $slotArgs: [] }) FormPath.setIn(fieldSchema.properties, target, (..._args: any) => { args.$slotArgs = _args return ( ) }) } else { FormPath.setIn( fieldSchema.properties, target, ) } } const renderProperties = (field?: GeneralField) => { if (props.onlyRenderSelf) return const properties = Schema.getOrderProperties(fieldSchema) if (!properties.length) return return ( {properties.map(({ schema: item, key: name }, index) => { const base = field?.address || basePath let schema: Schema = item if (schema['x-slot-node']) { renderSlots(schema, name) return null } if (isFn(props.mapProperties)) { const mapped = props.mapProperties(item, name) if (mapped) { schema = mapped } } if (isFn(props.filterProperties)) { if (props.filterProperties(schema, name) === false) { return null } } if (isBool(props.propsRecursion) && props.propsRecursion) { return ( ) } return ( ) })} ) } const render = () => { if (!isValid(props.name)) return renderProperties() if (fieldSchema.type === 'object') { if (props.onlyRenderProperties) return renderProperties() return ( {renderProperties} ) } else if (fieldSchema.type === 'array') { return ( ) } else if (fieldSchema.type === 'void') { if (props.onlyRenderProperties) return renderProperties() return ( {renderProperties} ) } return } if (!fieldSchema) return return ( {render()} ) } ================================================ FILE: packages/react/src/components/SchemaField.tsx ================================================ import React, { useContext, Fragment } from 'react' import { ISchema, Schema } from '@formily/json-schema' import { RecursionField } from './RecursionField' import { render } from '../shared/render' import { SchemaMarkupContext, SchemaOptionsContext, SchemaComponentsContext, } from '../shared' import { ReactComponentPath, JSXComponent, ISchemaFieldReactFactoryOptions, SchemaReactComponents, ISchemaFieldProps, ISchemaMarkupFieldProps, ISchemaTypeFieldProps, } from '../types' import { lazyMerge } from '@formily/shared' import { ExpressionScope } from './ExpressionScope' const env = { nonameId: 0, } const getRandomName = () => { return `NO_NAME_FIELD_$${env.nonameId++}` } export function createSchemaField( options: ISchemaFieldReactFactoryOptions = {} ) { function SchemaField< Decorator extends JSXComponent, Component extends JSXComponent >(props: ISchemaFieldProps) { const schema = Schema.isSchemaInstance(props.schema) ? props.schema : new Schema({ type: 'object', ...props.schema, }) const renderMarkup = () => { env.nonameId = 0 if (props.schema) return null return render( {props.children} ) } const renderChildren = () => { return } return ( {renderMarkup()} {renderChildren()} ) } SchemaField.displayName = 'SchemaField' function MarkupRender(props: any) { const parent = useContext(SchemaMarkupContext) if (!parent) return const renderChildren = () => { return {props.children} } const appendArraySchema = (schema: ISchema) => { const items = parent.items as Schema if (items && items.name !== props.name) { return parent.addProperty(props.name, schema) } else { return parent.setItems(schema) } } if (parent.type === 'object' || parent.type === 'void') { const schema = parent.addProperty(props.name, props) return ( {renderChildren()} ) } else if (parent.type === 'array') { const schema = appendArraySchema(props) return ( {props.children} ) } else { return renderChildren() } } function MarkupField< Decorator extends ReactComponentPath, Component extends ReactComponentPath >(props: ISchemaMarkupFieldProps) { return } MarkupField.displayName = 'MarkupField' function StringField< Decorator extends ReactComponentPath, Component extends ReactComponentPath >(props: ISchemaTypeFieldProps) { return } StringField.displayName = 'StringField' function ObjectField< Decorator extends ReactComponentPath, Component extends ReactComponentPath >(props: ISchemaTypeFieldProps) { return } ObjectField.displayName = 'ObjectField' function ArrayField< Decorator extends ReactComponentPath, Component extends ReactComponentPath >(props: ISchemaTypeFieldProps) { return } ArrayField.displayName = 'ArrayField' function BooleanField< Decorator extends ReactComponentPath, Component extends ReactComponentPath >(props: ISchemaTypeFieldProps) { return } BooleanField.displayName = 'BooleanField' function NumberField< Decorator extends ReactComponentPath, Component extends ReactComponentPath >(props: ISchemaTypeFieldProps) { return } NumberField.displayName = 'NumberField' function DateField< Decorator extends ReactComponentPath, Component extends ReactComponentPath >(props: ISchemaTypeFieldProps) { return } DateField.displayName = 'DateField' function DateTimeField< Decorator extends ReactComponentPath, Component extends ReactComponentPath >(props: ISchemaTypeFieldProps) { return } DateTimeField.displayName = 'DateTimeField' function VoidField< Decorator extends ReactComponentPath, Component extends ReactComponentPath >(props: ISchemaTypeFieldProps) { return } VoidField.displayName = 'VoidField' SchemaField.Markup = MarkupField SchemaField.String = StringField SchemaField.Object = ObjectField SchemaField.Array = ArrayField SchemaField.Boolean = BooleanField SchemaField.Date = DateField SchemaField.DateTime = DateTimeField SchemaField.Void = VoidField SchemaField.Number = NumberField return SchemaField } ================================================ FILE: packages/react/src/components/VoidField.tsx ================================================ import React from 'react' import { useForm, useField } from '../hooks' import { useAttach } from '../hooks/useAttach' import { ReactiveField } from './ReactiveField' import { FieldContext } from '../shared' import { JSXComponent, IVoidFieldProps } from '../types' export const VoidField = ( props: IVoidFieldProps ) => { const form = useForm() const parent = useField() const field = useAttach( form.createVoidField({ basePath: parent?.address, ...props }) ) return ( {props.children} ) } VoidField.displayName = 'VoidField' ================================================ FILE: packages/react/src/components/index.ts ================================================ export * from './FormProvider' export * from './FormConsumer' export * from './ArrayField' export * from './ObjectField' export * from './VoidField' export * from './RecursionField' export * from './ExpressionScope' export * from './RecordsScope' export * from './RecordScope' export * from './SchemaField' export * from './Field' ================================================ FILE: packages/react/src/global.d.ts ================================================ /// /// import * as Types from './types' declare global { namespace Formily.React { export { Types } } } ================================================ FILE: packages/react/src/hooks/index.ts ================================================ export * from './useForm' export * from './useField' export * from './useParentForm' export * from './useFieldSchema' export * from './useFormEffects' export * from './useExpressionScope' ================================================ FILE: packages/react/src/hooks/useAttach.ts ================================================ import { unstable_useCompatEffect } from '@formily/reactive-react' interface IRecycleTarget { onMount: () => void onUnmount: () => void } export const useAttach = (target: T): T => { unstable_useCompatEffect(() => { target.onMount() return () => target.onUnmount() }, [target]) return target } ================================================ FILE: packages/react/src/hooks/useExpressionScope.ts ================================================ import { useContext } from 'react' import { SchemaExpressionScopeContext } from '../shared/context' export const useExpressionScope = () => useContext(SchemaExpressionScopeContext) ================================================ FILE: packages/react/src/hooks/useField.ts ================================================ import { useContext } from 'react' import { GeneralField } from '@formily/core' import { FieldContext } from '../shared' export const useField = (): T => { return useContext(FieldContext) as any } ================================================ FILE: packages/react/src/hooks/useFieldSchema.ts ================================================ import { useContext } from 'react' import { SchemaContext } from '../shared' import { Schema } from '@formily/json-schema' export const useFieldSchema = (): Schema => { return useContext(SchemaContext) } ================================================ FILE: packages/react/src/hooks/useForm.ts ================================================ import { useContext } from 'react' import { Form } from '@formily/core' import { FormContext } from '../shared' export const useForm = (): Form => { return useContext(FormContext) } ================================================ FILE: packages/react/src/hooks/useFormEffects.ts ================================================ import { unstable_useCompatFactory } from '@formily/reactive-react' import { Form } from '@formily/core' import { uid } from '@formily/shared' import { useForm } from './useForm' export const useFormEffects = (effects?: (form: Form) => void) => { const form = useForm() unstable_useCompatFactory(() => { const id = uid() form.addEffects(id, effects) return { dispose() { form.removeEffects(id) }, } }) } ================================================ FILE: packages/react/src/hooks/useParentForm.ts ================================================ import { isObjectField, GeneralField, Form, ObjectField } from '@formily/core' import { useField } from './useField' import { useForm } from './useForm' export const useParentForm = (): Form | ObjectField => { const field = useField() const form = useForm() const findObjectParent = (field: GeneralField) => { if (!field) return form if (isObjectField(field)) return field return findObjectParent(field?.parent) } return findObjectParent(field) } ================================================ FILE: packages/react/src/index.ts ================================================ export * from '@formily/json-schema' export * from './components' export * from './shared' export * from './hooks' export * from './types' ================================================ FILE: packages/react/src/shared/connect.ts ================================================ import React from 'react' import { isFn, isStr, FormPath, each, isValid } from '@formily/shared' import { isVoidField } from '@formily/core' import { observer, Observer } from '@formily/reactive-react' import { JSXComponent, IComponentMapper, IStateMapper } from '../types' import { useField } from '../hooks' import hoistNonReactStatics from 'hoist-non-react-statics' export function mapProps( ...args: IStateMapper>[] ) { return (target: T) => { return observer( (props: any) => { const field = useField() const results = args.reduce( (props, mapper) => { if (isFn(mapper)) { props = Object.assign(props, mapper(props, field)) } else { each(mapper, (to, extract) => { const extractValue = FormPath.getIn(field, extract) const targetValue = isStr(to) ? to : (extract as any) const originalValue = FormPath.getIn(props, targetValue) if (extract === 'value') { if (to !== extract) { delete props.value } } if (isValid(originalValue) && !isValid(extractValue)) return FormPath.setIn(props, targetValue, extractValue) }) } return props }, { ...props } ) return React.createElement(target, results) }, { forwardRef: true, } ) } } export function mapReadPretty( component: C, readPrettyProps?: React.ComponentProps ) { return (target: T) => { return observer( (props) => { const field = useField() if (!isVoidField(field) && field?.pattern === 'readPretty') { return React.createElement(component, { ...readPrettyProps, ...props, }) } return React.createElement(target, props) }, { forwardRef: true, } ) } } export function connect( target: T, ...args: IComponentMapper[] ) { const Target = args.reduce((target, mapper) => { return mapper(target) }, target) const Destination = React.forwardRef( (props: Partial>, ref) => { return React.createElement(Target, { ...props, ref }) } ) if (target) hoistNonReactStatics(Destination, target as any) return Destination } export { observer, Observer } ================================================ FILE: packages/react/src/shared/context.ts ================================================ import React, { createContext } from 'react' import { Form, GeneralField } from '@formily/core' import { Schema } from '@formily/json-schema' import { ISchemaFieldReactFactoryOptions, SchemaReactComponents, } from '../types' const createContextCleaner = (...contexts: React.Context[]) => { return ({ children }) => { return contexts.reduce((buf, ctx) => { return React.createElement(ctx.Provider, { value: undefined }, buf) }, children) } } export const FormContext = createContext
(null) export const FieldContext = createContext(null) export const SchemaMarkupContext = createContext(null) export const SchemaContext = createContext(null) export const SchemaExpressionScopeContext = createContext(null) export const SchemaComponentsContext = createContext(null) export const SchemaOptionsContext = createContext(null) export const ContextCleaner = createContextCleaner( FieldContext, SchemaMarkupContext, SchemaContext, SchemaExpressionScopeContext, SchemaComponentsContext, SchemaOptionsContext ) ================================================ FILE: packages/react/src/shared/index.ts ================================================ export * from './context' export * from './connect' ================================================ FILE: packages/react/src/shared/render.ts ================================================ import React, { ReactNode, ReactPortal } from 'react' import { globalThisPolyfill } from '@formily/shared' interface Env { portalDOM?: HTMLDivElement createPortal?: (children: ReactNode, container: Element) => ReactPortal } const env: Env = { portalDOM: globalThisPolyfill?.document?.createElement?.('div'), createPortal: globalThisPolyfill?.['ReactDOM']?.createPortal, } /* istanbul ignore next */ const loadCreatePortal = () => { if (!env.createPortal) { try { // eslint-disable-next-line @typescript-eslint/no-var-requires env.createPortal ??= require('react-dom')?.createPortal } catch {} } if (!env.createPortal) { try { // @ts-ignore import('react-dom') .then((module) => (env.createPortal ??= module?.createPortal)) .catch() } catch {} } } export const render = (element: React.ReactElement) => { if (globalThisPolyfill.navigator?.product === 'ReactNative') return null if (env.portalDOM && env.createPortal) { return env.createPortal(element, env.portalDOM) } else { return React.createElement('template', {}, element) } } loadCreatePortal() ================================================ FILE: packages/react/src/types.ts ================================================ import React from 'react' import { Form, Field as FieldType, VoidField, ObjectField, GeneralField, IFieldFactoryProps, IVoidFieldFactoryProps, FormPatternTypes, FieldDisplayTypes, FieldValidator, } from '@formily/core' import { ReactFC } from '@formily/reactive-react' import { ISchema, Schema, SchemaKey } from '@formily/json-schema' import { FormPathPattern } from '@formily/shared' export type JSXComponent = | keyof JSX.IntrinsicElements | React.JSXElementConstructor export type IProviderProps = { form: Form } export interface IFormSpyProps { children?: (form: Form) => ReactChild } export type RenderPropsChildren = | ((field: Payload, form: Form) => React.ReactNode) | React.ReactNode export interface IFieldProps< D extends JSXComponent, C extends JSXComponent, Field = FieldType > extends IFieldFactoryProps { children?: RenderPropsChildren decorator?: [] | [D] | [D, React.ComponentProps] | any[] component?: [] | [C] | [C, React.ComponentProps] | any[] } export interface IVoidFieldProps< D extends JSXComponent, C extends JSXComponent, Field = VoidField > extends IVoidFieldFactoryProps { children?: RenderPropsChildren decorator?: [] | [D] | [D, React.ComponentProps] | any[] component?: [] | [C] | [C, React.ComponentProps] | any[] } export interface IComponentMapper { (target: T): JSXComponent } export type IStateMapper = | { [key in keyof FieldType]?: keyof Props | boolean } | ((props: Props, field: GeneralField) => Props) export type SchemaReactComponents = Record export interface ISchemaFieldReactFactoryOptions< Components extends SchemaReactComponents = any > { components?: Components scope?: any } export interface ISchemaFieldOptionContext { components: SchemaReactComponents } export interface ISchemaFieldProps< Decorator extends JSXComponent = any, Component extends JSXComponent = any, InnerField = ObjectField > extends Omit, 'name'> { schema?: ISchema components?: { [key: string]: JSXComponent } scope?: any name?: SchemaKey children?: React.ReactNode } export interface ISchemaMapper { (schema: Schema, name: SchemaKey): Schema } export interface ISchemaFilter { (schema: Schema, name: SchemaKey): boolean } export interface IRecursionFieldProps { schema: ISchema name?: SchemaKey basePath?: FormPathPattern propsRecursion?: boolean onlyRenderProperties?: boolean onlyRenderSelf?: boolean mapProperties?: ISchemaMapper filterProperties?: ISchemaFilter } export type ObjectKey = string | number | boolean | symbol export type Path = Key extends string ? T[Key] extends Record ? | `${Key}.${Path>> & string}` | `${Key}.${Exclude> & string}` | Key : Key : never export type PathValue< T, P extends Path > = P extends `${infer Key}.${infer Rest}` ? Key extends keyof T ? Rest extends Path ? PathValue : never : never : P extends keyof T ? T[P] : never export type KeyOfReactComponent = Exclude< keyof T, 'contextTypes' | 'displayName' | 'propTypes' | 'defaultProps' > export type ReactComponentPath< T, Key extends KeyOfReactComponent = KeyOfReactComponent > = Key extends string ? T[Key] extends Record ? | `${Key}.${Exclude, keyof Array> & string}` | Key : Key : never export type ReactComponentPropsByPathValue< T extends Record, P extends ReactComponentPath > = P extends `${infer Key}.${infer Rest}` ? Key extends keyof T ? Rest extends ReactComponentPath ? ReactComponentPropsByPathValue : never : React.ComponentProps : P extends keyof T ? React.ComponentProps : never export interface ISchemaMarkupFieldProps< Components extends SchemaReactComponents, Decorator extends ReactComponentPath, Component extends ReactComponentPath > extends ISchema< Decorator, Component, ReactComponentPropsByPathValue, ReactComponentPropsByPathValue, FormPatternTypes, FieldDisplayTypes, FieldValidator, React.ReactNode, GeneralField > { children?: React.ReactNode } export type ISchemaTypeFieldProps< Components extends SchemaReactComponents, Decorator extends ReactComponentPath, Component extends ReactComponentPath > = ISchemaMarkupFieldProps export interface IExpressionScopeProps { value?: any } export interface IRecordScopeProps { getIndex?(): number getRecord(): any } export interface IRecordsScopeProps { getRecords(): any[] } export type ReactChild = React.ReactElement | string | number export { ReactFC } ================================================ FILE: packages/react/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./lib", "paths": { "@formily/*": ["packages/*", "devtools/*"] }, "declaration": true } } ================================================ FILE: packages/react/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["./src/**/*.ts", "./src/**/*.tsx"], "exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"], "compilerOptions": { "lib": ["ESNext", "DOM"] } } ================================================ FILE: packages/reactive/.npmignore ================================================ node_modules *.log build docs doc-site __tests__ .eslintrc jest.config.js tsconfig.json .umi src ================================================ FILE: packages/reactive/.umirc.js ================================================ import { resolve } from 'path' export default { mode: 'site', logo: '//img.alicdn.com/imgextra/i2/O1CN01Kq3OHU1fph6LGqjIz_!!6000000004056-55-tps-1141-150.svg', title: 'Reactive', hash: true, favicon: '//img.alicdn.com/imgextra/i3/O1CN01XtT3Tv1Wd1b5hNVKy_!!6000000002810-55-tps-360-360.svg', outputPath: './doc-site', navs: { 'en-US': [ { title: 'Guide', path: '/guide', }, { title: 'API', path: '/api', }, { title: 'Home Site', path: 'https://formilyjs.org', }, { title: 'GITHUB', path: 'https://github.com/alibaba/formily', }, ], 'zh-CN': [ { title: '指南', path: '/zh-CN/guide', }, { title: 'API', path: '/zh-CN/api', }, { title: '主站', path: 'https://formilyjs.org', }, { title: 'GITHUB', path: 'https://github.com/alibaba/formily', }, ], }, headScripts: [ ` function loadAd(){ var header = document.querySelector('.__dumi-default-layout-content .markdown h1') if(header && !header.querySelector('#_carbonads_js')){ var script = document.createElement('script') script.src = '//cdn.carbonads.com/carbon.js?serve=CEAICK3M&placement=formilyjsorg' script.id = '_carbonads_js' script.classList.add('head-ad') header.appendChild(script) } } var request = null var observer = new MutationObserver(function(){ cancelIdleCallback(request) request = requestIdleCallback(loadAd) }) document.addEventListener('DOMContentLoaded',function(){ loadAd() observer.observe( document.body, { childList:true, subtree:true } ) }) `, ], styles: [ `.__dumi-default-navbar-logo{ background-size: 140px!important; background-position: center left!important; background-repeat: no-repeat!important; padding-left: 150px!important;/*可根据title的宽度调整*/ font-size: 22px!important; color: #000!important; font-weight: lighter!important; } .__dumi-default-navbar{ padding: 0 28px !important; } .__dumi-default-layout-hero{ background-image: url(//img.alicdn.com/imgextra/i4/O1CN01ZcvS4e26XMsdsCkf9_!!6000000007671-2-tps-6001-4001.png); background-size: cover; background-repeat: no-repeat; padding: 120px 0 !important; } .__dumi-default-layout-hero h1{ color:#45124e !important; font-size:80px !important; padding-bottom: 30px !important; } .__dumi-default-dark-switch { display:none } nav a{ text-decoration: none !important; } #carbonads * { margin: initial; padding: initial; } #carbonads { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Helvetica, Arial, sans-serif; } #carbonads { display: flex; max-width: 330px; background-color: hsl(0, 0%, 98%); box-shadow: 0 1px 4px 1px hsla(0, 0%, 0%, 0.1); z-index: 100; float:right; } #carbonads a { color: inherit; text-decoration: none; } #carbonads a:hover { color: inherit; } #carbonads span { position: relative; display: block; overflow: hidden; } #carbonads .carbon-wrap { display: flex; } #carbonads .carbon-img { display: block; margin: 0; line-height: 1; } #carbonads .carbon-img img { display: block; } #carbonads .carbon-text { font-size: 13px; padding: 10px; margin-bottom: 16px; line-height: 1.5; text-align: left; } #carbonads .carbon-poweredby { display: block; padding: 6px 8px; background: #f1f1f2; text-align: center; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; font-size: 8px; line-height: 1; border-top-left-radius: 3px; position: absolute; bottom: 0; right: 0; } `, ], menus: { '/guide': [ { title: 'Introduction', path: '/guide', }, { title: 'Concept', path: '/guide/concept' }, { title: 'Best Practice', path: '/guide/best-practice', }, ], '/api': [ { title: '@formily/reactive', children: [ { title: 'observable', path: '/api/observable', }, { title: 'autorun', path: '/api/autorun', }, { title: 'reaction', path: '/api/reaction', }, { title: 'batch', path: '/api/batch', }, { title: 'action', path: '/api/action', }, { title: 'define', path: '/api/define', }, { title: 'model', path: '/api/model', }, { title: 'observe', path: '/api/observe', }, { title: 'markRaw', path: '/api/mark-raw', }, { title: 'markObservable', path: '/api/mark-observable', }, { title: 'raw', path: '/api/raw', }, { title: 'toJS', path: '/api/to-js', }, { title: 'untracked', path: '/api/untracked', }, { title: 'hasCollected', path: '/api/has-collected', }, { title: 'Tracker', path: '/api/tracker', }, { title: 'Type Chekcer', path: '/api/type-checker', }, ], }, { title: '@formily/reactive-react', children: [ { title: 'observer', path: '/api/react/observer', }, ], }, { title: '@formily/reactive-vue', children: [ { title: 'observer', path: '/api/vue/observer', }, ], }, ], '/zh-CN/guide': [ { title: '介绍', path: '/zh-CN/guide', }, { title: '核心概念', path: '/zh-CN/guide/concept' }, { title: '最佳实践', path: '/zh-CN/guide/best-practice', }, ], '/zh-CN/api': [ { title: '@formily/reactive', children: [ { title: 'observable', path: '/zh-CN/api/observable', }, { title: 'autorun', path: '/zh-CN/api/autorun', }, { title: 'reaction', path: '/zh-CN/api/reaction', }, { title: 'batch', path: '/zh-CN/api/batch', }, { title: 'action', path: '/zh-CN/api/action', }, { title: 'define', path: '/zh-CN/api/define', }, { title: 'model', path: '/zh-CN/api/model', }, { title: 'observe', path: '/zh-CN/api/observe', }, { title: 'markRaw', path: '/zh-CN/api/mark-raw', }, { title: 'markObservable', path: '/zh-CN/api/mark-observable', }, { title: 'raw', path: '/zh-CN/api/raw', }, { title: 'toJS', path: '/zh-CN/api/to-js', }, { title: 'untracked', path: '/zh-CN/api/untracked', }, { title: 'hasCollected', path: '/zh-CN/api/has-collected', }, { title: 'Tracker', path: '/zh-CN/api/tracker', }, { title: 'Type Chekcer', path: '/zh-CN/api/type-checker', }, ], }, { title: '@formily/reactive-react', children: [ { title: 'observer', path: '/zh-CN/api/react/observer', }, ], }, { title: '@formily/reactive-vue', children: [ { title: 'observer', path: '/zh-CN/api/vue/observer', }, ], }, ], }, } ================================================ FILE: packages/reactive/LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2015-present, Alibaba Group Holding Limited. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/reactive/README.md ================================================ # @formily/reactive > Web Reactive Library Like Mobx ## QuikStart ================================================ FILE: packages/reactive/benchmark.ts ================================================ import b from 'benny' import _ from 'lodash' import * as mobx from 'mobx' import * as vueReactivity from '@vue/reactivity' import * as formilyReactive from './src' function func(obs, times) { obs.arr = [] obs.obj = {} _.times(times, (v) => { obs.num = v obs.str = `${v}` obs.arr.push(v) obs.obj[`${v}`] = v }) } b.suite( 'Reactive Observable', b.add('Case MobX', () => { const obs = mobx.observable({}) func(obs, 1e3) }), b.add('Case @vue/reactivity', () => { const obs = vueReactivity.reactive({}) func(obs, 1e3) }), b.add('Case @formily/reactive', () => { const obs = formilyReactive.observable({}) func(obs, 1e3) }), b.cycle(), b.complete() ) ================================================ FILE: packages/reactive/docs/api/action.md ================================================ # action ## Description Define a batch action. The only difference with batch is that dependencies cannot be collected inside an action ## Signature ```ts interface action { (callback?: () => T): T //In-situ action scope(callback?: () => T): T //In-situ local action bound any>(callback: T, context?: any): T //High-level binding } ``` ## Example ```ts import { observable, action } from '@formily/reactive' const obs = observable({}) const method = action.bound(() => { obs.aa = 123 obs.bb = 321 }) method() ``` ================================================ FILE: packages/reactive/docs/api/action.zh-CN.md ================================================ # action ## 描述 定义一个批量动作。与 batch 的唯一差别就是 action 内部是无法收集依赖的 ## 签名 ```ts interface action { (callback?: () => T): T //原地action scope(callback?: () => T): T //原地局部action bound any>(callback: T, context?: any): T //高阶绑定 } ``` ## 用例 ```ts import { observable, action } from '@formily/reactive' const obs = observable({}) const method = action.bound(() => { obs.aa = 123 obs.bb = 321 }) method() ``` ================================================ FILE: packages/reactive/docs/api/autorun.md ================================================ # autorun ## Description Receive a tracker function, if there is observable data in the function, the tracker function will be executed repeatedly when the data changes ## Signature ```ts interface autorun { (tracker: () => void, name?: string): void } ``` ## Example ```ts import { observable, autorun } from '@formily/reactive' const obs = observable({}) const dispose = autorun(() => { console.log(obs.aa) }) obs.aa = 123 dispose() ``` ## autorun.memo ### Description Used in autorun to create persistent reference data, only re-execute memo internal functions due to dependency changes Note: Please do not use it in If/For statements, because it depends on the execution order to bind the current autorun ### Signature ```ts interface memo { (callback: () => T, dependencies: any[] = []): T } ``` Note: The default dependency is `[]`, that is, if the dependency is not passed, it means that the second time will never be executed ### Example ```ts import { observable, autorun } from '@formily/reactive' const obs1 = observable({ aa: 0, }) const dispose = autorun(() => { const obs2 = autorun.memo(() => observable({ bb: 0, }) ) console.log(obs1.aa, obs2.bb++) }) obs1.aa++ obs1.aa++ obs1.aa++ //Execute four times, the output result is /** * 0 0 * 1 1 * twenty two * 3 3 */ dispose() ``` ## autorun.effect ### Description In autorun, it is used to respond to the next micro task timing of autorun's first execution and the dispose of responding to autorun Note: Please do not use it in If/For statements, because it depends on the execution order to bind the current autorun ### Signature ```ts interface effect { (callback: () => void | (() => void), dependencies: any[] = [{}]): void } ``` Note: The default dependency is `[{}]`, that is, if the dependency is not passed, the representative will continue to execute, because the internal dirty check is a shallow comparison ### Example ```ts import { observable, autorun } from '@formily/reactive' const obs1 = observable({ aa: 0, }) const dispose = autorun(() => { const obs2 = autorun.memo(() => observable({ bb: 0, }) ) console.log(obs1.aa, obs2.bb++) autorun.effect(() => { obs2.bb++ }, []) }) obs1.aa++ obs1.aa++ obs1.aa++ //Execute five times, the output result is /** * 0 0 * 1 1 * twenty two * 3 3 * 3 5 */ dispose() ``` ================================================ FILE: packages/reactive/docs/api/autorun.zh-CN.md ================================================ # autorun ## 描述 接收一个 tracker 函数,如果函数内部有消费 observable 数据,数据发生变化时,tracker 函数会重复执行 ## 签名 ```ts interface autorun { (tracker: () => void, name?: string): void } ``` ## 用例 ```ts import { observable, autorun } from '@formily/reactive' const obs = observable({}) const dispose = autorun(() => { console.log(obs.aa) }) obs.aa = 123 dispose() ``` ## autorun.memo ### 描述 在 autorun 中用于创建持久引用数据,仅仅只会受依赖变化而重新执行 memo 内部函数 注意:请不要在 If/For 这类语句中使用,因为它内部是依赖执行顺序来绑定当前 autorun 的 ### 签名 ```ts interface memo { (callback: () => T, dependencies: any[] = []): T } ``` 注意:依赖默认为`[]`,也就是如果不传依赖,代表永远不会执行第二次 ### 用例 ```ts import { observable, autorun } from '@formily/reactive' const obs1 = observable({ aa: 0, }) const dispose = autorun(() => { const obs2 = autorun.memo(() => observable({ bb: 0, }) ) console.log(obs1.aa, obs2.bb++) }) obs1.aa++ obs1.aa++ obs1.aa++ //执行四次,输出结果为 /** * 0 0 * 1 1 * 2 2 * 3 3 */ dispose() ``` ## autorun.effect ### 描述 在 autorun 中用于响应 autorun 第一次执行的下一个微任务时机与响应 autorun 的 dispose 注意:请不要在 If/For 这类语句中使用,因为它内部是依赖执行顺序来绑定当前 autorun 的 ### 签名 ```ts interface effect { (callback: () => void | (() => void), dependencies: any[] = [{}]): void } ``` 注意:依赖默认为`[{}]`,也就是如果不传依赖,代表会持续执行,因为内部脏检查是浅比较 ### 用例 ```ts import { observable, autorun } from '@formily/reactive' const obs1 = observable({ aa: 0, }) const dispose = autorun(() => { const obs2 = autorun.memo(() => observable({ bb: 0, }) ) console.log(obs1.aa, obs2.bb++) autorun.effect(() => { obs2.bb++ }, []) }) obs1.aa++ obs1.aa++ obs1.aa++ //执行五次,输出结果为 /** * 0 0 * 1 1 * 2 2 * 3 3 * 3 5 */ dispose() ``` ================================================ FILE: packages/reactive/docs/api/batch.md ================================================ # batch ## Description Define batch operations, internal dependencies can be collected ## Signature ```ts interface batch { (callback?: () => T): T //In-place batch scope(callback?: () => T): T //In-situ local batch bound any>(callback: T, context?: any): T //High-level binding endpoint(callback?: () => void): void //Register batch endpoint callback } ``` ## Example ```ts import { observable, autorun, batch } from '@formily/reactive' const obs = observable({}) autorun(() => { console.log(obs.aa, obs.bb, obs.cc, obs.dd) }) batch(() => { batch.scope(() => { obs.aa = 123 }) batch.scope(() => { obs.cc = 'ccccc' }) obs.bb = 321 obs.dd = 'dddd' }) ``` ================================================ FILE: packages/reactive/docs/api/batch.zh-CN.md ================================================ # batch ## 描述 定义批量操作,内部可以收集依赖 ## 签名 ```ts interface batch { (callback?: () => T): T //原地batch scope(callback?: () => T): T //原地局部batch bound any>(callback: T, context?: any): T //高阶绑定 endpoint(callback?: () => void): void //注册批量执行结束回调 } ``` ## 用例 ```ts import { observable, autorun, batch } from '@formily/reactive' const obs = observable({}) autorun(() => { console.log(obs.aa, obs.bb, obs.cc, obs.dd) }) batch(() => { batch.scope(() => { obs.aa = 123 }) batch.scope(() => { obs.cc = 'ccccc' }) obs.bb = 321 obs.dd = 'dddd' }) ``` ================================================ FILE: packages/reactive/docs/api/define.md ================================================ # define ## Description Manually define the domain model, you can specify the responsive behavior of specific attributes, or you can specify a method as batch mode ## Signature ```ts interface define { ( target: Target, annotations?: { [key: string]: (...args: any[]) => any } ): Target } ``` ## Annotations All Annotations currently supported are: - observable/observable.deep defines deep hijacking responsive properties - observable.box defines get/set container - observable.computed defines calculated properties - observable.ref defines reference hijacking responsive attributes - observable.shallow defines shallow hijacking responsive properties - action/batch defines the batch processing method ## Example ```ts import { define, observable, action, autorun } from '@formily/reactive' class DomainModel { deep = { aa: 1 } shallow = {} box = 0 ref = '' constructor() { define(this, { deep: observable, shallow: observable.shallow, box: observable.box, ref: observable.ref, computed: observable.computed, action, }) } get computed() { return this.deep.aa + this.box.get() } action(aa, box) { this.deep.aa = aa this.box.set(box) } } const model = new DomainModel() autorun(() => { console.log(model.computed) }) model.action(1, 2) model.action(1, 2) //Repeat calls will not respond repeatedly model.action(3, 4) ``` ================================================ FILE: packages/reactive/docs/api/define.zh-CN.md ================================================ # define ## 描述 手动定义领域模型,可以指定具体属性的响应式行为,也可以指定某个方法为 batch 模式 ## 签名 ```ts interface define { ( target: Target, annotations?: { [key: string]: (...args: any[]) => any } ): Target } ``` ## Annotations 目前支持的所有 Annotation 有: - observable/observable.deep 定义深度劫持响应式属性 - observable.box 定义 get/set 容器 - observable.computed 定义计算属性 - observable.ref 定义引用劫持响应式属性 - observable.shallow 定义浅劫持响应式属性 - action/batch 定义批处理方法 ## 用例 ```ts import { define, observable, action, autorun } from '@formily/reactive' class DomainModel { deep = { aa: 1 } shallow = {} box = 0 ref = '' constructor() { define(this, { deep: observable, shallow: observable.shallow, box: observable.box, ref: observable.ref, computed: observable.computed, action, }) } get computed() { return this.deep.aa + this.box.get() } action(aa, box) { this.deep.aa = aa this.box.set(box) } } const model = new DomainModel() autorun(() => { console.log(model.computed) }) model.action(1, 2) model.action(1, 2) //重复调用不会重复响应 model.action(3, 4) ``` ================================================ FILE: packages/reactive/docs/api/hasCollected.md ================================================ # hasCollected ## describe Used to detect whether a certain piece of execution logic has dependent collection ## Signature ```ts interface hasCollected { (callback?: () => void): boolean } ``` ## Example ```ts import { observable, autorun } from '@formily/reactive' const obs = observable({ aa: 11, }) autorun(() => { console.log( hasCollected(() => { obs.aa }) ) //return true console.log( hasCollected(() => { 11 + 22 }) ) //return false }) obs.aa = 22 ``` ================================================ FILE: packages/reactive/docs/api/hasCollected.zh-CN.md ================================================ # hasCollected ## 描述 用于检测某段执行逻辑是否存在依赖收集 ## 签名 ```ts interface hasCollected { (callback?: () => void): boolean } ``` ## 用例 ```ts import { observable, autorun } from '@formily/reactive' const obs = observable({ aa: 11, }) autorun(() => { console.log( hasCollected(() => { obs.aa }) ) //return true console.log( hasCollected(() => { 11 + 22 }) ) //return false }) obs.aa = 22 ``` ================================================ FILE: packages/reactive/docs/api/markObservable.md ================================================ # markObservable ## Description Mark any object or class prototype as being hijacked by observable. React Node and objects with toJSON/toJS methods will be automatically bypassed in @formily/reactive. In special scenarios, we may hope that the object should be hijacked, so you can use it markObservable mark ## Signature ```ts interface markObservable { (target: T): T } ``` ## Example ```ts import { observable, autorun, markObservable } from '@formily/reactive' class A { property = '' toJSON() {} } const a = observable(new A()) autorun(() => { console.log(a.property) //will not be triggered when the property changes, because there is a toJSON method in the A instance }) a.property = 123 //-------------------------------------------- const b = observable(markObservable(new A())) //instance-level mark, only valid for the current instance autorun(() => { console.log(b.property) //Can be triggered when the property changes, because it has been marked as observable }) b.property = 123 //-------------------------------------------- markObservable(A) //Class-level mark, then all instances will take effect const c = observable(new A()) autorun(() => { console.log(c.property) //Can be triggered when the property changes, because it has been marked as observable }) c.property = 123 ``` ================================================ FILE: packages/reactive/docs/api/markObservable.zh-CN.md ================================================ # markObservable ## 描述 标记任意一个对象或者类原型为可被 observable 劫持,在@formily/reactive 中会自动绕过 React Node 与带有 toJSON/toJS 方法的对象,特殊场景,我们可能希望该对象应该被劫持,所以可以使用 markObservable 标记 ## 签名 ```ts interface markObservable { (target: T): T } ``` ## 用例 ```ts import { observable, autorun, markObservable } from '@formily/reactive' class A { property = '' toJSON() {} } const a = observable(new A()) autorun(() => { console.log(a.property) //property变化时不会被触发,因为A实例中有toJSON方法 }) a.property = 123 //-------------------------------------------- const b = observable(markObservable(new A())) //实例级标记,只对当前实例生效 autorun(() => { console.log(b.property) //property变化时可以被触发,因为已被标记observable }) b.property = 123 //-------------------------------------------- markObservable(A) //类级标记,那么所有实例都会生效 const c = observable(new A()) autorun(() => { console.log(c.property) //property变化时可以被触发,因为已被标记observable }) c.property = 123 ``` ================================================ FILE: packages/reactive/docs/api/markRaw.md ================================================ # markRaw ## Description Mark any object or class prototype as never being hijacked by observable, priority is higher than markObservable Note: If you mark an object that is already observable with markRaw, then toJS will not convert it into a normal object ## Signature ```ts interface markRaw { (target: T): T } ``` ## Example ```ts import { observable, autorun, markRaw } from '@formily/reactive' class A { property = '' } const a = observable(new A()) autorun(() => { console.log(a.property) //It will be triggered when the property changes, because the A instance is a normal object }) a.property = 123 //-------------------------------------------- const b = observable(markRaw(new A())) //instance-level mark, only valid for the current instance autorun(() => { console.log(b.property) //will not be triggered when the property changes, because it has been marked raw }) b.property = 123 //-------------------------------------------- markRaw(A) //Class-level mark, then all instances will take effect const c = observable(new A()) autorun(() => { console.log(c.property) //will not be triggered when the property changes, because it has been marked raw }) c.property = 123 ``` ================================================ FILE: packages/reactive/docs/api/markRaw.zh-CN.md ================================================ # markRaw ## 描述 标记任意一个对象或者类原型为永远不可被 observable 劫持,优先级比 markObservable 高 注意:如果对一个已经是 observable 的对象标记 markRaw,那么 toJS,是不会将它转换成普通对象的 ## 签名 ```ts interface markRaw { (target: T): T } ``` ## 用例 ```ts import { observable, autorun, markRaw } from '@formily/reactive' class A { property = '' } const a = observable(new A()) autorun(() => { console.log(a.property) //property变化时会被触发,因为A实例是普通对象 }) a.property = 123 //-------------------------------------------- const b = observable(markRaw(new A())) //实例级标记,只对当前实例生效 autorun(() => { console.log(b.property) //property变化时不会被触发,因为已被标记raw }) b.property = 123 //-------------------------------------------- markRaw(A) //类级标记,那么所有实例都会生效 const c = observable(new A()) autorun(() => { console.log(c.property) //property变化时不会被触发,因为已被标记raw }) c.property = 123 ``` ================================================ FILE: packages/reactive/docs/api/model.md ================================================ # model ## Description Quickly define the domain model, and automatically declare the model attributes: - Automatic declaration of getter/setter properties computed - Function automatically declare action - Common attributes are automatically declared observable ## Signature ```ts interface model { (target: Target): Target } ``` ## Example ```ts import { model, autorun } from '@formily/reactive' const obs = model({ aa: 1, bb: 2, get cc() { return this.aa + this.bb }, update(aa, bb) { this.aa=aa this.bb=bb }, }) autorun(() => { console.log(obs.cc) }) obs.aa = 3 obs.update(4, 6) ``` ================================================ FILE: packages/reactive/docs/api/model.zh-CN.md ================================================ # model ## 描述 快速定义领域模型,会对模型属性做自动声明: - getter/setter 属性自动声明 computed - 函数自动声明 action - 普通属性自动声明 observable ## 签名 ```ts interface model { (target: Target): Target } ``` ## 用例 ```ts import { model, autorun } from '@formily/reactive' const obs = model({ aa: 1, bb: 2, get cc() { return this.aa + this.bb }, update(aa, bb) { this.aa=aa this.bb=bb }, }) autorun(() => { console.log(obs.cc) }) obs.aa = 3 obs.update(4, 6) ``` ================================================ FILE: packages/reactive/docs/api/observable.md ================================================ # observable > Mainly used to create observable objects with different responsive behaviors, and can be used as an annotation to define to mark responsive attributes ## observable/observable.deep ### Description Create deep hijacking responsive objects ### Signature ```ts interface observable { (target: T): T } interface deep { (target: T): T } ``` ### Example ```ts import { observable, autorun } from '@formily/reactive' const obs = observable({ aa: { bb: 123, }, }) autorun(() => { console.log(obs.aa.bb) }) obs.aa.bb = 321 ``` ## observable.shallow ### Description Create shallow hijacking responsive objects, that is, only respond to the first-level attribute operations of the target object ### Signature ```ts interface shallow { (target: T): T } ``` ### Example ```ts import { observable, autorun } from '@formily/reactive' const obs = observable.shallow({ aa: { bb: 111, }, }) autorun(() => { console.log(obs.aa.bb) }) obs.aa.bb = 222 // will not respond obs.aa = { bb: 333 } // can respond ``` ## observable.computed ### Description Create a calculation buffer ### Signature ```ts interface computed { any>(target: T): { value: ReturnType } any; set?: (value: any) => void }>(target: T): { value: ReturnType } } ``` ### Example ```ts import { observable, autorun } from '@formily/reactive' const obs = observable({ aa: 11, bb: 22, }) const computed = observable.computed(() => obs.aa + obs.bb) autorun(() => { console.log(computed.value) }) obs.aa = 33 ``` ## observable.ref ### Description Create reference hijacking responsive objects ### Signature ```ts interface ref { (target: T): { value: T } } ``` ### Example ```ts import { observable, autorun } from '@formily/reactive' const ref = observable.ref(1) autorun(() => { console.log(ref.value) }) ref.value = 2 ``` ## observable.box ### Description Similar to ref, except that the data is read and written through the get/set method ### Signature ```ts interface box { (target: T): { get: () => T; set: (value: T) => void } } ``` ### Example ```ts import { observable, autorun } from '@formily/reactive' const box = observable.box(1) autorun(() => { console.log(box.get()) }) box.set(2) ``` ================================================ FILE: packages/reactive/docs/api/observable.zh-CN.md ================================================ # observable > 主要用于创建不同响应式行为的 observable 对象,同时可以作为 annotation 给 define 用于标记响应式属性 ## observable/observable.deep ### 描述 创建深度劫持响应式对象 ### 签名 ```ts interface observable { (target: T): T } interface deep { (target: T): T } ``` ### 用例 ```ts import { observable, autorun } from '@formily/reactive' const obs = observable({ aa: { bb: 123, }, }) autorun(() => { console.log(obs.aa.bb) }) obs.aa.bb = 321 ``` ## observable.shallow ### 描述 创建浅劫持响应式对象,也就是只会对目标对象的第一级属性操作响应 ### 签名 ```ts interface shallow { (target: T): T } ``` ### 用例 ```ts import { observable, autorun } from '@formily/reactive' const obs = observable.shallow({ aa: { bb: 111, }, }) autorun(() => { console.log(obs.aa.bb) }) obs.aa.bb = 222 // 不会响应 obs.aa = { bb: 333 } // 可以响应 ``` ## observable.computed ### 描述 创建一个计算缓存器 ### 签名 ```ts interface computed { any>(target: T): { value: ReturnType } any; set?: (value: any) => void }>(target: T): { value: ReturnType } } ``` ### 用例 ```ts import { observable, autorun } from '@formily/reactive' const obs = observable({ aa: 11, bb: 22, }) const computed = observable.computed(() => obs.aa + obs.bb) autorun(() => { console.log(computed.value) }) obs.aa = 33 ``` ## observable.ref ### 描述 创建引用劫持响应式对象 ### 签名 ```ts interface ref { (target: T): { value: T } } ``` ### 用例 ```ts import { observable, autorun } from '@formily/reactive' const ref = observable.ref(1) autorun(() => { console.log(ref.value) }) ref.value = 2 ``` ## observable.box ### 描述 与 ref 相似,只是读写数据是通过 get/set 方法 ### 签名 ```ts interface box { (target: T): { get: () => T; set: (value: T) => void } } ``` ### 用例 ```ts import { observable, autorun } from '@formily/reactive' const box = observable.box(1) autorun(() => { console.log(box.get()) }) box.set(2) ``` ================================================ FILE: packages/reactive/docs/api/observe.md ================================================ # observe ## Description Very different from autorun/reaction/Tracker, using observe will monitor all operations of observable objects, support deep monitoring and shallow monitoring Note: The read operation will not be monitored ## Signature ```ts type PropertyKey = string | number | symbol type ObservablePath = Array type OperationType = | 'add' | 'delete' | 'clear' | 'set' | 'get' | 'iterate' | 'has' interface IChange { key?: PropertyKey path?: ObservablePath object?: object value?: any oldValue?: any type?: OperationType } interface IDispose { (): void } interface observe { ( target: object, observer?: (change: IChange) => void, deep?: boolean //default is true ): IDispose //Release the monitor } ``` ## Example ```ts import { observable, observe } from '@formily/reactive' const obs = observable({ aa: 11, }) const dispose = observe(obs, (change) => { console.log(change) }) obs.aa = 22 dispose() ``` ================================================ FILE: packages/reactive/docs/api/observe.zh-CN.md ================================================ # observe ## 描述 与 autorun/reaction/Tracker 非常不一样,使用 observe 会监听 observable 对象的所有操作,支持深度监听也支持浅监听 注意:读取操作是不会被监听到的 ## 签名 ```ts type PropertyKey = string | number | symbol type ObservablePath = Array type OperationType = | 'add' | 'delete' | 'clear' | 'set' | 'get' | 'iterate' | 'has' interface IChange { key?: PropertyKey path?: ObservablePath object?: object value?: any oldValue?: any type?: OperationType } interface IDispose { (): void } interface observe { ( target: object, observer?: (change: IChange) => void, deep?: boolean //默认为true ): IDispose //释放监听 } ``` ## 用例 ```ts import { observable, observe } from '@formily/reactive' const obs = observable({ aa: 11, }) const dispose = observe(obs, (change) => { console.log(change) }) obs.aa = 22 dispose() ``` ================================================ FILE: packages/reactive/docs/api/raw.md ================================================ # raw ## Description Obtain the source data from the observable object. Generally, this API is not recommended Note: Only the source data of the current object can be obtained, excluding deep object properties ## Signature ```ts interface raw { (target: T): T } ``` ## Example ```ts import { raw, observable } from '@formily/reactive' const obs = observable({}) obs.aa = { bb: 123 } console.log(raw(obs)) console.log(raw(obs.aa)) ``` ================================================ FILE: packages/reactive/docs/api/raw.zh-CN.md ================================================ # raw ## 描述 从 observable 对象中获取源数据,通常情况下并不推荐使用该 API 注意:只能获取当前对象的源数据,不包括深层对象属性 ## 签名 ```ts interface raw { (target: T): T } ``` ## 用例 ```ts import { raw, observable } from '@formily/reactive' const obs = observable({}) obs.aa = { bb: 123 } console.log(raw(obs)) console.log(raw(obs.aa)) ``` ================================================ FILE: packages/reactive/docs/api/react/observer.md ================================================ # observer ## observer ### Description In React, turn Function Component into Reaction, and dependencies will be collected every time the view is re-rendered, and dependency updates will be automatically re-rendered Note: Only Function Component is supported ### Signature ```ts interface IObserverOptions { forwardRef?: boolean //Whether to pass the reference transparently scheduler?: (updater: () => void) => void //The scheduler, you can manually control the timing of the update displayName?: string //displayName of the packaged component } interface observer { (component: T, options?: IObserverOptions): T } ``` ### Example ```tsx /** * defaultShowCode: true */ import React from 'react' import { observable } from '@formily/reactive' import { observer } from '@formily/reactive-react' const obs = observable({ value: 'Hello world', }) export default observer(() => { return (
{ obs.value = e.target.value }} />
{obs.value}
) }) ``` ## Observer ### Description Similar to Vue's responsive slot, it receives a Function RenderProps, as long as any responsive data consumed inside the Function, it will be automatically re-rendered as the data changes, and it is easier to achieve local accurate rendering ### Signature ```ts interface IObserverProps { children?: () => React.ReactElement } type Observer = React.FC> ``` ### Example ```tsx /** * defaultShowCode: true */ import React from 'react' import { observable } from '@formily/reactive' import { Observer } from '@formily/reactive-react' const obs = observable({ value: 'Hello world', }) export default () => { return (
{() => ( { obs.value = e.target.value }} /> )}
{() =>
{obs.value}
}
) } ``` ================================================ FILE: packages/reactive/docs/api/react/observer.zh-CN.md ================================================ # observer ## observer ### 描述 在 React 中,将 Function Component 变成 Reaction,每次视图重新渲染就会收集依赖,依赖更新会自动重渲染 注意:只支持Function Component ### 签名 ```ts interface IObserverOptions { forwardRef?: boolean //是否透传引用 scheduler?: (updater: () => void) => void //调度器,可以手动控制更新时机 displayName?: string //包装后的组件的displayName } interface observer { (component: T, options?: IObserverOptions): T } ``` ### 用例 ```tsx /** * defaultShowCode: true */ import React from 'react' import { observable } from '@formily/reactive' import { observer } from '@formily/reactive-react' const obs = observable({ value: 'Hello world', }) export default observer(() => { return (
{ obs.value = e.target.value }} />
{obs.value}
) }) ``` ## Observer ### 描述 类似于 Vue 的响应式 Slot,它接收一个 Function RenderProps,只要在 Function 内部消费到的任何响应式数据,都会随数据变化而自动重新渲染,也更容易实现局部精确渲染 ### 签名 ```ts interface IObserverProps { children?: () => React.ReactElement } type Observer = React.FC> ``` ### 用例 ```tsx /** * defaultShowCode: true */ import React from 'react' import { observable } from '@formily/reactive' import { Observer } from '@formily/reactive-react' const obs = observable({ value: 'Hello world', }) export default () => { return (
{() => ( { obs.value = e.target.value }} /> )}
{() =>
{obs.value}
}
) } ``` ================================================ FILE: packages/reactive/docs/api/reaction.md ================================================ # reaction ## Description Receive a tracker function and a callback response function. If there is observable data in the tracker, the tracker function will be executed repeatedly when the data changes, but the callback execution must be executed when the tracker function return value changes. ## Signature ```ts interface IReactionOptions { name?: string equals?: (oldValue: T, newValue: T) => boolean //Dirty check fireImmediately?: boolean //Is it triggered by default for the first time, bypassing the dirty check } interface reaction { ( tracker: () => T, subscriber?: (newValue: T, oldValue: T) => void, options?: IReactionOptions ): void } ``` ## Example ```ts import { observable, reaction, batch } from '@formily/reactive' const obs = observable({ aa: 1, bb: 2, }) const dispose = reaction(() => { return obs.aa + obs.bb }, console.log) batch(() => { //Won't trigger because the value of obs.aa + obs.bb has not changed obs.aa = 2 obs.bb = 1 }) obs.aa = 4 dispose() ``` ================================================ FILE: packages/reactive/docs/api/reaction.zh-CN.md ================================================ # reaction ## 描述 接收一个 tracker 函数,与 callback 响应函数,如果 tracker 内部有消费 observable 数据,数据发生变化时,tracker 函数会重复执行,但是 callback 执行必须要求 tracker 函数返回值发生变化时才执行 ## 签名 ```ts interface IReactionOptions { name?: string equals?: (oldValue: T, newValue: T) => boolean //脏检查 fireImmediately?: boolean //是否第一次默认触发,绕过脏检查 } interface reaction { ( tracker: () => T, subscriber?: (newValue: T, oldValue: T) => void, options?: IReactionOptions ): void } ``` ## 用例 ```ts import { observable, reaction, batch } from '@formily/reactive' const obs = observable({ aa: 1, bb: 2, }) const dispose = reaction(() => { return obs.aa + obs.bb }, console.log) batch(() => { //不会触发,因为obs.aa + obs.bb值没变 obs.aa = 2 obs.bb = 1 }) obs.aa = 4 dispose() ``` ================================================ FILE: packages/reactive/docs/api/toJS.md ================================================ # toJS ## Description Deep recursion converts observable objects into ordinary JS objects Note: If you mark an object that is already observable with markRaw, then toJS will not convert it into a normal object ## Signature ```ts interface toJS { (target: T): T } ``` ## Example ```ts import { observable, autorun, toJS } from '@formily/reactive' const obs = observable({ aa: { bb: { cc: 123, }, }, }) const js = toJS(obs) autorun(() => { console.log(js.aa.bb.cc) // will not trigger when changes }) js.aa.bb.cc = 321 ``` ================================================ FILE: packages/reactive/docs/api/toJS.zh-CN.md ================================================ # toJS ## 描述 深度递归将 observable 对象转换成普通 JS 对象 注意:如果对一个已经是 observable 的对象标记 markRaw,那么 toJS,是不会将它转换成普通对象的 ## 签名 ```ts interface toJS { (target: T): T } ``` ## 用例 ```ts import { observable, autorun, toJS } from '@formily/reactive' const obs = observable({ aa: { bb: { cc: 123, }, }, }) const js = toJS(obs) autorun(() => { console.log(js.aa.bb.cc) //变化时不会触发 }) js.aa.bb.cc = 321 ``` ================================================ FILE: packages/reactive/docs/api/tracker.md ================================================ # tracker ## Description Mainly used to access the manual tracking dependency tool of React/Vue. The tracker function will not be executed repeatedly when the dependency changes. It requires the user to manually execute it repeatedly, which will only trigger the scheduler ## Signature ```ts class Tracker { constructor(scheduler?: (reaction: this['track']) => void, name?: string) track: (tracker?: () => T) => T dispose: () => void } ``` ## Example ```ts import { observable, Tracker } from '@formily/reactive' const obs = observable({ aa: 11, }) const view = () => { console.log(obs.aa) } const tracker = new Tracker(() => { tracker.track(view) }) tracker.track(view) obs.aa = 22 tracker.dispose() ``` ================================================ FILE: packages/reactive/docs/api/tracker.zh-CN.md ================================================ # tracker ## 描述 主要用于接入 React/Vue 的手动追踪依赖工具,在依赖发生变化时不会重复执行 tracker 函数,需要用户手动重复执行,只会触发 scheduler ## 签名 ```ts class Tracker { constructor(scheduler?: (reaction: this['track']) => void, name?: string) track: (tracker?: () => T) => T dispose: () => void } ``` ## 用例 ```ts import { observable, Tracker } from '@formily/reactive' const obs = observable({ aa: 11, }) const view = () => { console.log(obs.aa) } const tracker = new Tracker(() => { tracker.track(view) }) tracker.track(view) obs.aa = 22 tracker.dispose() ``` ================================================ FILE: packages/reactive/docs/api/typeChecker.md ================================================ # Type Checker ## isObservable #### Description Determine whether an object is observable #### Signature ```ts interface isObservable { (target: any): boolean } ``` ## isAnnotation #### Description Determine whether an object is Annotation #### Signature ```ts interface isAnnotation { (target: any): boolean } ``` ## isSupportObservable #### Description Determine whether an object can be observable #### Signature ```ts interface isSupportObservable { (target: any): boolean } ``` ================================================ FILE: packages/reactive/docs/api/typeChecker.zh-CN.md ================================================ # Type Checker ## isObservable #### 描述 判断某个对象是否是 observable 对象 #### 签名 ```ts interface isObservable { (target: any): boolean } ``` ## isAnnotation #### 描述 判断某个对象是否是 Annotation #### 签名 ```ts interface isAnnotation { (target: any): boolean } ``` ## isSupportObservable #### 描述 判断某个对象是否可以被 observable #### 签名 ```ts interface isSupportObservable { (target: any): boolean } ``` ================================================ FILE: packages/reactive/docs/api/untracked.md ================================================ # untracked ## Description Usage is similar to batch, and will never be collected by dependencies within a given untracker function ## Signature ```ts interface untracked any> { (untracker?: T): ReturnType } ``` ## Example ```ts import { observable, autorun, untracked } from '@formily/reactive' const obs = observable({ aa: 11, }) autorun(() => { console.log(untracked(() => obs.aa)) // will not trigger when changes }) obs.aa = 22 ``` ================================================ FILE: packages/reactive/docs/api/untracked.zh-CN.md ================================================ # untracked ## 描述 用法与 batch 相似,在给定的 untracker 函数内部永远不会被依赖收集 ## 签名 ```ts interface untracked any> { (untracker?: T): ReturnType } ``` ## 用例 ```ts import { observable, autorun, untracked } from '@formily/reactive' const obs = observable({ aa: 11, }) autorun(() => { console.log(untracked(() => obs.aa)) //变化时不会触发 }) obs.aa = 22 ``` ================================================ FILE: packages/reactive/docs/api/vue/observer.md ================================================ # observer ## describe In Vue, the component rendering method is changed to Reaction, and dependencies are collected every time the view is re-rendered, and dependencies are updated automatically to re-render. ### Signature ```ts interface IObserverOptions { scheduler?: (updater: () => void) => void //The scheduler, you can manually control the timing of the update name?: string //name of the packaged component } interface observer { (component: T, options?: IObserverOptions): T } ``` ## Example ```html ``` ================================================ FILE: packages/reactive/docs/api/vue/observer.zh-CN.md ================================================ # observer ## 描述 在 Vue 中,将组件渲染方法变成 Reaction,每次视图重新渲染就会收集依赖,依赖更新会自动重渲染。 ### 签名 ```ts interface IObserverOptions { scheduler?: (updater: () => void) => void //调度器,可以手动控制更新时机 name?: string //包装后的组件的name } interface observer { (component: T, options?: IObserverOptions): T } ``` ## 用例 ```html ``` ================================================ FILE: packages/reactive/docs/guide/best-practice.md ================================================ # Best Practices When using @formily/reactive, we only need to pay attention to the following points: - Minimize the use of observable/observable.deep for deep packaging, instead of using observable.ref/observable.shallow as a last resort, the performance will be better - Multi-use computed properties in the domain model, which can intelligently cache the calculation results - Although batch operation is not necessary, use batch mode as much as possible to reduce the number of executions of Reaction - When using autorun/reaction, you must remember to call the dispose release function (that is, the second-order function returned by the calling function), otherwise there will be memory leaks ================================================ FILE: packages/reactive/docs/guide/best-practice.zh-CN.md ================================================ # 最佳实践 在使用@formily/reactive 的时候,我们只需要注意以下几点即可: - 尽量少用 observable/observable.deep 进行深度包装,不是非不得已就多用 observable.ref/observable.shallow,这样性能会更好 - 领域模型中多用 computed 计算属性,它可以智能缓存计算结果 - 虽然批量操作不是必须的,但是尽量多用 batch 模式,这样可以减少 Reaction 执行次数 - 使用 autorun/reaction 的时候,一定记得调用 dispose 释放函数(也就是调用函数所返回的二阶函数),否则会内存泄漏 ================================================ FILE: packages/reactive/docs/guide/concept.md ================================================ # Core idea ## Observable Observable is the most important part of the reactive programming model. Its core concepts are: An observable object, literally means a subscribable object, **we create a subscribable object, each time we manipulate the attribute data of the object, we will automatically notify the subscriber**, @formily/reactive creates an observable object mainly It is created by ES Proxy, which can perfectly hijack data operations We mainly use the following APIs to create observable objects in @formily/reactive: - The observable function creates a deep observable object - The observable.deep function creates a deep hijacking observable object - The observable.shallow function creates shallow hijacked observable objects - The observable.computed function creates a cache calculator - The observable.box function creates observable objects with get/set methods - The observable.ref function creates a reference-level observable object - The define function defines the observable domain model, which can be combined with the observable function and its static attribute (such as observable.computed) function to complete the definition of the domain model - The model function defines an automatic observable domain model. It will wrap the getter setter attribute as a computed attribute, wrap the function as an action, and wrap other data attributes with observable (note that this is a deep hijacking) ## Reaction In the reactive programming model, reaction is equivalent to the subscriber of the subscribeable object. It receives a tracker function. When this function is executed, if there is a **read operation* on an attribute in the observable object inside the function. * (Dependency collection), then the current reaction will be bound to the attribute (dependency tracking), until the attribute has a **write operation\*\* in other places, it will trigger the tracker function to repeat execution, using a picture Means: ![](https://img.alicdn.com/imgextra/i4/O1CN01DQMGUL22mFICDsKfY_!!6000000007162-2-tps-1234-614.png) You can see that from subscribing to dispatching subscriptions, it is actually a closed loop state machine. Each time the tracker function is executed, the dependencies are re-collected, and the tracker execution is re-triggered when the dependencies change. So, if we don't want to subscribe to the reaction anymore, we must manually dispose, otherwise there will be memory leaks. In @formily/reactive, we mainly use the following APIs to create reactions: - autorun creates an automatically executed responder - reaction creates a responder that can implement dirty checks - Tracker creates a dependency tracker that requires users to manually perform tracking ## Computed Computed is also a relatively important concept in the reactive programming model. In one sentence, **computed is a Reaction that can cache calculation results** Its caching strategy is: as long as the observable data that the computed function relies on changes, the function will re-execute the calculation, otherwise the cached result will always be read The requirement here is that the computed function must be a pure function. The internally dependent data is either observable data or external constant data. If it is external variable data (non-observable), then if the external variable data changes, the computed will not be re-executed computational. ## Batch As mentioned earlier, @formily/reactive is a reactive programming model based on Proxy hijacking. Therefore, any atomic operation will trigger the execution of Reaction, which is obviously a waste of computing resources. For example, we have a function for multiple observables. Property to operate: ```ts import { observable, autorun } from '@formily/reactive' const obs = observable({}) const handler = () => { obs.aa = 123 obs.bb = 321 } autorun(() => { console.log(obs.aa, obs.bb) }) handler() ``` This will execute 3 prints, autorun is executed once by default, plus the assignment of obs.aa is executed once, and the assignment of obs.bb is executed once. If there are more atomic operations, the number of executions will be more. Therefore, we recommend using batch mode To merge the updates: ```ts import { observable, autorun, batch } from '@formily/reactive' const obs = observable({}) const handler = () => { obs.aa = 123 obs.bb = 321 } autorun(() => { console.log(obs.aa, obs.bb) }) batch(() => { handler() }) ``` Of course, we can also use action for high-level packaging: ```ts import { observable, autorun, action } from '@formily/reactive' const obs = observable({}) const handler = action.bound(() => { obs.aa = 123 obs.bb = 321 }) autorun(() => { console.log(obs.aa, obs.bb) }) handler() ``` The final number of executions is 2 times, even if there are more operations inside the handler, it is still 2 times ================================================ FILE: packages/reactive/docs/guide/concept.zh-CN.md ================================================ # 核心概念 ## Observable observable 是响应式编程模型中最重要的一块,它的核心概念就是: 一个 observable 对象,字面意思是可订阅对象,**我们通过创建一个可订阅对象,在每次操作该对象的属性数据的过程中,会自动通知订阅者**,@formily/reactive 创建 observable 对象主要是通过 ES Proxy 来创建的,它可以做到完美劫持数据操作 我们在@formily/reactive 中主要用以下几个 API 来创建 observable 对象: - observable 函数创建深度 observable 对象 - observable.deep 函数创建深劫持 observable 对象 - observable.shallow 函数创建浅劫持 observable 对象 - observable.computed 函数创建缓存计算器 - observable.box 函数创建带 get/set 方法的 observable 对象 - observable.ref 函数创建引用级 observable 对象 - define 函数定义 observable 领域模型,可以组合 observable 函数与其静态属性(比如 observable.computed)函数完成领域模型的定义 - model 函数定义自动 observable 领域模型,它会将 getter setter 属性包装为 computed 计算属性,将函数包装为 action,将其他数据属性用 observable 包装(注意这里是深劫持) ## Reaction reaction 在响应式编程模型中,它就相当于是可订阅对象的订阅者,它接收一个 tracker 函数,这个函数在执行的时候,如果函数内部有对 observable 对象中的某个属性进行**读操作**(依赖收集),那当前 reaction 就会与该属性进行一个绑定(依赖追踪),直到该属性在其他地方发生了**写操作**,就会触发 tracker 函数重复执行,用一张图表示: ![](https://img.alicdn.com/imgextra/i4/O1CN01DQMGUL22mFICDsKfY_!!6000000007162-2-tps-1234-614.png) 可以看到从订阅到派发订阅,其实是一个封闭的循环状态机,每次 tracker 函数执行的时候都会重新收集依赖,依赖变化时又会重新触发 tracker 执行。所以,如果一旦我们不想再订阅 reaction 了,一定要手动 dispose,否则会内存泄漏。 在@formily/reactive 中的我们主要是使用以下几个 API 来创建 reaction: - autorun 创建一个自动执行的响应器 - reaction 创建一个可以实现脏检查的响应器 - Tracker 创建一个依赖追踪器,需要用户手动执行追踪 ## Computed computed 在响应式编程模型中也是属于一个比较重要的概念,一句话表达的话,**computed 是一个可以缓存计算结果的 Reaction** 它的缓存策略是:只要 computed 函数内部所依赖的 observable 数据发生变化,函数才会重新执行计算,否则永远读取缓存结果 这里要求的就是 computed 函数必须是纯函数,内部依赖的数据要么是 observable 数据,要么是外部常量数据,如果是外部变量数据(非 observable),那如果外部变量数据发生变化,computed 是不会重新执行计算的。 ## Batch 前面有讲到@formily/reactive 是基于 Proxy 劫持来实现的响应式编程模型,所以任何一个原子操作都会触发 Reaction 执行,这样明显是浪费了计算资源的,比如我们有一个函数内部是对多个 observable 属性进行操作的: ```ts import { observable, autorun } from '@formily/reactive' const obs = observable({}) const handler = () => { obs.aa = 123 obs.bb = 321 } autorun(() => { console.log(obs.aa, obs.bb) }) handler() ``` 这样就会执行 3 次打印,autorun 默认执行一次,加上 obs.aa 赋值执行一次,obs.bb 赋值执行一次,如果原子操作更多一些,那执行次数会更多,所以,我们推荐使用 batch 模式,将更新进行合并: ```ts import { observable, autorun, batch } from '@formily/reactive' const obs = observable({}) const handler = () => { obs.aa = 123 obs.bb = 321 } autorun(() => { console.log(obs.aa, obs.bb) }) batch(() => { handler() }) ``` 当然,我们也可以使用 action 进行高阶包装: ```ts import { observable, autorun, action } from '@formily/reactive' const obs = observable({}) const handler = action.bound(() => { obs.aa = 123 obs.bb = 321 }) autorun(() => { console.log(obs.aa, obs.bb) }) handler() ``` 最终执行次数就是 2 次了,即便 handler 内部的操作再多也还是 2 次 ================================================ FILE: packages/reactive/docs/guide/index.md ================================================ # Introduction The core idea of @formily/reactive is to refer to [Mobx](https://mobx.js.org/), so why reinvent the wheel? There are four main reasons: - mobx does not support dependency collection within actions - The observable function of mobx does not support filtering special objects such as react node, moment, and immutable - mobx's observable function will automatically turn the function into action - The observer of mobx-react-lite does not support React concurrent rendering For the above reasons, Formily had to reinvent the wheel, but the wheel strongly relies on Proxy, that is, it does not support IE browser. Of course, reinventing the wheel also has its advantages: - More controllability, you can do more in-depth optimization and customization for formily scenes - Regardless of the historical burden of Mobx, the code can be cleaner - If the Mobx version is Break Change or there are security vulnerabilities, it will have no impact on Formily ================================================ FILE: packages/reactive/docs/guide/index.zh-CN.md ================================================ # 介绍 @formily/reactive 的核心思想是参考 [Mobx](https://mobx.js.org/) 的,那为什么要重新造轮子呢? 主要有 4 点原因: - mobx 不支持 action 内部进行依赖收集 - mobx 的 observable 函数不支持过滤 react node,moment,immutable 之类的特殊对象 - mobx 的 observable 函数会自动将函数变成 action - mobx-react-lite 的 observer 不支持 React 并发渲染 基于以上原因,formily 不得不重新造轮子,不过该轮子是强依赖 Proxy 的,也就是不支持 IE 浏览器,当然,重新造轮子也有它的好处: - 把控性更强,可以为 formily 场景做更深的优化定制 - 不用考虑 Mobx 的历史包袱,代码可以更干净 - 如果 Mobx 版本 Break Change 或者存在安全漏洞,对 Formily 无影响 ================================================ FILE: packages/reactive/docs/index.md ================================================ --- title: Formily-Alibaba unified front-end form solution order: 10 hero: title: Reactive Library desc: DDD-oriented Responsive State Management Solution actions: - text: Home Site link: //formilyjs.org - text: Document link: /guide features: - icon: https://img.alicdn.com/imgextra/i1/O1CN01bHdrZJ1rEOESvXEi5_!!6000000005599-55-tps-800-800.svg title: High Performance desc: Efficient update, Demand rendering - icon: https://img.alicdn.com/imgextra/i2/O1CN01YqmcpN1tDalwgyHBH_!!6000000005868-55-tps-800-800.svg title: Zero Dependencies desc: Cross Device,Cross Framework - icon: https://img.alicdn.com/imgextra/i4/O1CN01u6jHgs1ZMwXpjAYnh_!!6000000003181-55-tps-800-800.svg title: Smart Tips desc: Embrace Typescript footer: Open-source MIT Licensed | Copyright © 2019-present
Powered by self --- ## Installation ```bash $ npm install --save @formily/reactive ``` ## Quick start ```tsx /** * defaultShowCode: true */ import React from 'react' import { observable } from '@formily/reactive' import { observer } from '@formily/reactive-react' const obs = observable({ value: 'Hello world', }) export default observer(() => { return (
{ obs.value = e.target.value }} />
{obs.value}
) }) ``` ================================================ FILE: packages/reactive/docs/index.zh-CN.md ================================================ --- title: Formily - 阿里巴巴统一前端表单解决方案 order: 10 hero: title: Reactive Library desc: 面向DDD的响应式状态管理方案 actions: - text: 主站文档 link: //formilyjs.org - text: 开发指南 link: /zh-CN/guide features: - icon: https://img.alicdn.com/imgextra/i1/O1CN01bHdrZJ1rEOESvXEi5_!!6000000005599-55-tps-800-800.svg title: 超高性能 desc: 依赖追踪,高效更新,按需渲染 - icon: https://img.alicdn.com/imgextra/i2/O1CN01YqmcpN1tDalwgyHBH_!!6000000005868-55-tps-800-800.svg title: 跨终端,跨框架 desc: UI无关,框架无关 - icon: https://img.alicdn.com/imgextra/i4/O1CN01u6jHgs1ZMwXpjAYnh_!!6000000003181-55-tps-800-800.svg title: 智能提示 desc: 拥抱Typescript footer: Open-source MIT Licensed | Copyright © 2019-present
Powered by self --- ## 安装 ```bash $ npm install --save @formily/reactive ``` ## 快速开始 ```tsx /** * defaultShowCode: true */ import React from 'react' import { observable } from '@formily/reactive' import { observer } from '@formily/reactive-react' const obs = observable({ value: 'Hello world', }) export default observer(() => { return (
{ obs.value = e.target.value }} />
{obs.value}
) }) ``` ================================================ FILE: packages/reactive/package.json ================================================ { "name": "@formily/reactive", "version": "2.3.7", "license": "MIT", "main": "lib", "module": "esm", "umd:main": "dist/formily.reactive.umd.production.js", "unpkg": "dist/formily.reactive.umd.production.js", "jsdelivr": "dist/formily.reactive.umd.production.js", "jsnext:main": "esm", "repository": { "type": "git", "url": "git+https://github.com/alibaba/formily.git" }, "types": "esm/index.d.ts", "bugs": { "url": "https://github.com/alibaba/formily/issues" }, "homepage": "https://github.com/alibaba/formily#readme", "engines": { "npm": ">=3.0.0" }, "scripts": { "start": "dumi dev", "build": "rimraf -rf lib esm dist && npm run build:cjs && npm run build:esm && npm run build:umd", "build:cjs": "tsc --project tsconfig.build.json", "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm", "build:umd": "rollup --config", "build:docs": "dumi build", "benchmark": "ts-node ./benchmark" }, "devDependencies": { "@vue/reactivity": "^3.0.11", "benny": "^3.6.15", "dumi": "^1.1.0-rc.8", "lodash": "^4.17.21", "mobx": "^6.3.0" }, "publishConfig": { "access": "public" }, "gitHead": "ac79c196ae9324889aca5e0501146f9b37b04283" } ================================================ FILE: packages/reactive/rollup.config.js ================================================ import baseConfig from '../../scripts/rollup.base.js' export default baseConfig('formily.reactive', 'Formily.Reactive') ================================================ FILE: packages/reactive/src/__tests__/action.spec.ts ================================================ import { observable, action, autorun } from '..' import { reaction } from '../autorun' import { batch } from '../batch' import { define } from '../model' describe('normal action', () => { test('no action', () => { const obs = observable({ aa: { bb: 123, }, }) const handler = jest.fn() autorun(() => { handler(obs.aa.bb) }) obs.aa.bb = 111 obs.aa.bb = 222 expect(handler).toBeCalledTimes(3) obs.aa.bb = 333 obs.aa.bb = 444 expect(handler).toBeCalledTimes(5) }) test('action', () => { const obs = observable({ aa: { bb: 123, }, }) const handler = jest.fn() autorun(() => { handler(obs.aa.bb) }) obs.aa.bb = 111 obs.aa.bb = 222 expect(handler).toBeCalledTimes(3) action(() => { obs.aa.bb = 333 obs.aa.bb = 444 }) action(() => {}) action() expect(handler).toBeCalledTimes(4) }) test('action track', () => { const obs = observable({ aa: { bb: 123, }, cc: 1, }) const handler = jest.fn() autorun(() => { action(() => { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } }) }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) }) test('action.bound', () => { const obs = observable({ aa: { bb: 123, }, }) const handler = jest.fn() const setData = action.bound(() => { obs.aa.bb = 333 obs.aa.bb = 444 }) autorun(() => { handler(obs.aa.bb) }) obs.aa.bb = 111 obs.aa.bb = 222 expect(handler).toBeCalledTimes(3) setData() action.bound(() => {}) expect(handler).toBeCalledTimes(4) }) test('action.bound track', () => { const obs = observable({ aa: { bb: 123, }, cc: 1, }) const handler = jest.fn() autorun(() => { action.bound(() => { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } })() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) }) test('action.scope xxx', () => { const obs = observable({}) const handler = jest.fn() autorun(() => { handler(obs.aa, obs.bb, obs.cc, obs.dd) }) action(() => { action.scope(() => { obs.aa = 123 }) action.scope(() => { obs.cc = 'ccccc' }) obs.bb = 321 obs.dd = 'ddddd' }) expect(handler).toBeCalledTimes(4) }) test('action.scope bound', () => { const obs = observable({}) const handler = jest.fn() autorun(() => { handler(obs.aa, obs.bb, obs.cc, obs.dd) }) const scope1 = action.scope.bound(() => { obs.aa = 123 }) action(() => { scope1() action.scope.bound(() => { obs.cc = 'ccccc' })() obs.bb = 321 obs.dd = 'ddddd' }) expect(handler).toBeCalledTimes(4) }) test('action.scope track', () => { const obs = observable({ aa: { bb: 123, }, cc: 1, }) const handler = jest.fn() autorun(() => { action.scope(() => { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } }) }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) }) test('action.scope bound track', () => { const obs = observable({ aa: { bb: 123, }, cc: 1, }) const handler = jest.fn() autorun(() => { action.scope.bound(() => { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } })() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) }) }) describe('annotation action', () => { test('action', () => { const obs = define( { aa: { bb: 123, }, setData() { this.aa.bb = 333 this.aa.bb = 444 }, }, { aa: observable, setData: action, } ) const handler = jest.fn() autorun(() => { handler(obs.aa.bb) }) obs.aa.bb = 111 obs.aa.bb = 222 expect(handler).toBeCalledTimes(3) obs.setData() expect(handler).toBeCalledTimes(4) }) test('action track', () => { const obs = define( { aa: { bb: 123, }, cc: 1, setData() { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } }, }, { aa: observable, setData: action, } ) const handler = jest.fn() autorun(() => { obs.setData() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) }) test('action.bound', () => { const obs = define( { aa: { bb: 123, }, setData() { this.aa.bb = 333 this.aa.bb = 444 }, }, { aa: observable, setData: action.bound, } ) const handler = jest.fn() autorun(() => { handler(obs.aa.bb) }) obs.aa.bb = 111 obs.aa.bb = 222 expect(handler).toBeCalledTimes(3) obs.setData() expect(handler).toBeCalledTimes(4) }) test('action.bound track', () => { const obs = define( { aa: { bb: 123, }, cc: 1, setData() { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } }, }, { aa: observable, setData: action.bound, } ) const handler = jest.fn() autorun(() => { obs.setData() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) }) test('action.scope', () => { const obs = define( { aa: null, bb: null, cc: null, dd: null, scope1() { this.aa = 123 }, scope2() { this.cc = 'ccccc' }, }, { aa: observable, bb: observable, cc: observable, dd: observable, scope1: action.scope, scope2: action.scope, } ) const handler = jest.fn() autorun(() => { handler(obs.aa, obs.bb, obs.cc, obs.dd) }) action(() => { obs.scope1() obs.scope2() obs.bb = 321 obs.dd = 'ddddd' }) expect(handler).toBeCalledTimes(4) }) test('action.scope bound', () => { const obs = define( { aa: null, bb: null, cc: null, dd: null, scope1() { this.aa = 123 }, scope2() { this.cc = 'ccccc' }, }, { aa: observable, bb: observable, cc: observable, dd: observable, scope1: action.scope.bound, scope2: action.scope.bound, } ) const handler = jest.fn() autorun(() => { handler(obs.aa, obs.bb, obs.cc, obs.dd) }) action(() => { obs.scope1() obs.scope2() obs.bb = 321 obs.dd = 'ddddd' }) expect(handler).toBeCalledTimes(4) }) test('action.scope track', () => { const obs = define( { aa: { bb: 123, }, cc: 1, scope() { if (this.cc > 0) { handler(this.aa.bb) this.cc = this.cc + 20 } }, }, { aa: observable, cc: observable, scope: action.scope, } ) const handler = jest.fn() autorun(() => { obs.scope() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) }) test('action.scope bound track', () => { const obs = define( { aa: { bb: 123, }, cc: 1, scope() { if (this.cc > 0) { handler(this.aa.bb) this.cc = this.cc + 20 } }, }, { aa: observable, cc: observable, scope: action.scope.bound, } ) const handler = jest.fn() autorun(() => { obs.scope() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) }) }) test('nested action to reaction', () => { const obs = observable({ aa: 0, }) const handler = jest.fn() reaction( () => obs.aa, (v) => handler(v) ) action(() => { obs.aa = 1 action(() => { obs.aa = 2 }) }) action(() => { obs.aa = 3 action(() => { obs.aa = 4 }) }) expect(handler).nthCalledWith(1, 2) expect(handler).nthCalledWith(2, 4) expect(handler).toBeCalledTimes(2) }) test('nested action/batch to reaction', () => { const obs = define( { bb: 0, get aa() { return this.bb }, set aa(v) { this.bb = v }, }, { aa: observable.computed, bb: observable, } ) const handler = jest.fn() reaction( () => obs.aa, (v) => handler(v) ) action(() => { obs.aa = 1 batch(() => { obs.aa = 2 }) }) action(() => { obs.aa = 3 batch(() => { obs.aa = 4 }) }) expect(handler).nthCalledWith(1, 2) expect(handler).nthCalledWith(2, 4) expect(handler).toBeCalledTimes(2) }) ================================================ FILE: packages/reactive/src/__tests__/annotations.spec.ts ================================================ import { observable, action, model, define } from '../' import { autorun, reaction } from '../autorun' import { observe } from '../observe' import { isObservable } from '../externals' import { untracked } from '../untracked' import { getObservableMaker } from '../internals' test('observable annotation', () => { const obs = observable({ aa: 111, }) const handler = jest.fn() const handler1 = jest.fn() observe(obs, handler1) reaction(() => { handler(obs.aa) }) obs.aa = { bb: { cc: 123 } } obs.aa.bb = 333 expect(handler).toBeCalledTimes(2) expect(handler1).toBeCalledTimes(2) const handler2 = jest.fn() const handler3 = jest.fn() const obsAnno = getObservableMaker(observable)({ value: obs }) observe(obsAnno, handler2) reaction(() => { handler3(obsAnno.aa) }) obsAnno.aa = { bb: { cc: 123 } } obsAnno.aa.bb = 333 expect(handler2).toBeCalledTimes(2) expect(handler3).toBeCalledTimes(2) }) test('shallow annotation', () => { const obs = observable.shallow({ aa: 111, }) const handler = jest.fn() const handler1 = jest.fn() observe(obs, handler1) reaction(() => { handler(obs.aa) }) obs.aa = { bb: { cc: 123 } } expect(isObservable(obs)).toBe(true) expect(isObservable(obs.aa)).toBe(false) expect(isObservable(obs.aa.bb)).toBe(false) obs.aa.bb = 333 obs.cc = 444 expect(handler).toBeCalledTimes(2) expect(handler1).toBeCalledTimes(2) }) test('box annotation', () => { const obs = observable.box(123) const handler = jest.fn() const handler1 = jest.fn() observe(obs, handler1) reaction(() => { handler(obs.get()) }) const boxValue = 333 obs.set(boxValue) expect(handler1).toBeCalledTimes(1) expect(handler1.mock.calls[0][0]).toMatchObject({ value: boxValue, }) expect(handler).toBeCalledTimes(2) expect(handler.mock.calls[0][0]).toBe(123) expect(handler.mock.calls[1][0]).toBe(boxValue) }) test('ref annotation', () => { const obs = observable.ref(123) const handler = jest.fn() const handler1 = jest.fn() observe(obs, handler1) reaction(() => { handler(obs.value) }) obs.value = 333 expect(handler).nthCalledWith(1, 123) expect(handler).nthCalledWith(2, 333) expect(handler1).toBeCalledTimes(1) }) test('action annotation', () => { const obs = observable({}) const setData = action.bound(() => { obs.aa = 123 obs.bb = 321 }) const handler = jest.fn() reaction(() => { return [obs.aa, obs.bb] }, handler) setData() expect(handler).toBeCalledTimes(1) expect(handler).toBeCalledWith([123, 321], [undefined, undefined]) }) test('no action annotation', () => { const obs = observable({}) const setData = () => { obs.aa = 123 obs.bb = 321 } const handler = jest.fn() reaction(() => { return [obs.aa, obs.bb] }, handler) setData() expect(handler).toBeCalledTimes(2) expect(handler).nthCalledWith(1, [123, undefined], [undefined, undefined]) expect(handler).nthCalledWith(2, [123, 321], [123, undefined]) }) test('computed annotation', () => { const obs = observable({ aa: 11, bb: 22, }) const handler = jest.fn(() => obs.aa + obs.bb) const runner1 = jest.fn() const runner2 = jest.fn() const runner3 = jest.fn() const compu = observable.computed(handler) expect(compu.value).toEqual(33) expect(handler).toBeCalledTimes(1) obs.aa = 22 expect(handler).toBeCalledTimes(1) const dispose = autorun(() => { compu.value runner1() }) const dispose2 = autorun(() => { compu.value runner2() }) expect(compu.value).toEqual(44) expect(handler).toBeCalledTimes(2) obs.bb = 33 expect(runner1).toBeCalledTimes(2) expect(runner2).toBeCalledTimes(2) expect(handler).toBeCalledTimes(3) expect(compu.value).toEqual(55) expect(handler).toBeCalledTimes(3) obs.aa = 11 expect(runner1).toBeCalledTimes(3) expect(runner2).toBeCalledTimes(3) expect(handler).toBeCalledTimes(4) expect(compu.value).toEqual(44) expect(handler).toBeCalledTimes(4) dispose() obs.aa = 22 expect(runner1).toBeCalledTimes(3) expect(runner2).toBeCalledTimes(4) expect(handler).toBeCalledTimes(5) expect(compu.value).toEqual(55) expect(handler).toBeCalledTimes(5) dispose2() obs.aa = 33 expect(runner1).toBeCalledTimes(3) expect(runner2).toBeCalledTimes(4) expect(handler).toBeCalledTimes(5) expect(compu.value).toEqual(66) expect(handler).toBeCalledTimes(6) expect(compu.value).toEqual(66) expect(handler).toBeCalledTimes(6) autorun(() => { compu.value runner3() }) expect(compu.value).toEqual(66) expect(handler).toBeCalledTimes(6) expect(compu.value).toEqual(66) expect(handler).toBeCalledTimes(6) obs.aa = 11 expect(handler).toBeCalledTimes(7) expect(compu.value).toEqual(44) expect(handler).toBeCalledTimes(7) }) test('computed chain annotation', () => { const obs = observable({ aa: 11, bb: 22, }) const handler = jest.fn(() => obs.aa + obs.bb) const compu1 = observable.computed(handler) const handler1 = jest.fn(() => compu1.value + 33) const compu2 = observable.computed(handler1) const dispose = autorun(() => { compu2.value }) expect(handler).toBeCalledTimes(1) expect(handler1).toBeCalledTimes(1) expect(compu2.value).toEqual(66) expect(handler).toBeCalledTimes(1) expect(handler1).toBeCalledTimes(1) obs.aa = 22 expect(handler).toBeCalledTimes(2) expect(handler1).toBeCalledTimes(2) expect(compu2.value).toEqual(77) expect(handler).toBeCalledTimes(2) expect(handler1).toBeCalledTimes(2) dispose() obs.aa = 11 expect(handler).toBeCalledTimes(2) expect(handler1).toBeCalledTimes(2) expect(compu2.value).toEqual(66) expect(handler).toBeCalledTimes(3) expect(handler1).toBeCalledTimes(3) }) test('computed with array length', () => { const obs = model({ arr: [], get isEmpty() { return this.arr.length === 0 }, get isNotEmpty() { return !this.isEmpty }, }) const handler = jest.fn() autorun(() => { handler(obs.isEmpty) handler(obs.isNotEmpty) }) expect(handler).toBeCalledTimes(2) obs.arr = ['1'] obs.arr = [] expect(handler).toBeCalledTimes(6) }) test('computed with computed array length', () => { const obs = model({ arr: [], get arr2() { return this.arr.map((item: number) => item + 1) }, get isEmpty() { return this.arr2.length === 0 }, get isNotEmpty() { return !this.isEmpty }, }) const handler = jest.fn() const handler2 = jest.fn() autorun(() => { handler(obs.isNotEmpty) handler2(obs.arr2) }) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(false) expect(handler2).toBeCalledTimes(1) expect(handler2.mock.calls[0][0]).toEqual([]) obs.arr.push(1) expect(handler).lastCalledWith(true) expect(handler2.mock.calls[1][0]).toEqual([2]) obs.arr = [] expect(handler).lastCalledWith(false) expect(handler2.mock.calls[2][0]).toEqual([]) }) test('computed recollect dependencies', () => { const computed = jest.fn() const obs = model({ aa: 'aaa', bb: 'bbb', cc: 'ccc', get compute() { computed() if (this.aa === 'aaa') { return this.bb } return this.cc }, }) const handler = jest.fn() autorun(() => { handler(obs.compute) }) obs.aa = '111' obs.bb = '222' expect(computed).toBeCalledTimes(2) }) test('computed no params', () => { observable.computed(null) }) test('computed object params', () => { observable.computed({ get: () => {} }) }) test('computed no track get', () => { const obs = observable({ aa: 123 }) const compu = observable.computed({ get: () => obs.aa }) untracked(() => { expect(compu.value).toBe(123) }) }) test('computed cache descriptor', () => { class A { _value = 0 constructor() { define(this, { _value: observable.ref, value: observable.computed, }) } get value() { return this._value } } const obs1 = new A() const obs2 = new A() const handler1 = jest.fn() const handler2 = jest.fn() autorun(() => { handler1(obs1.value) }) autorun(() => { handler2(obs2.value) }) expect(handler1).toBeCalledTimes(1) expect(handler2).toBeCalledTimes(1) obs1._value = 123 obs2._value = 123 expect(handler1).toBeCalledTimes(2) expect(handler2).toBeCalledTimes(2) }) test('computed normal object', () => { const obs = define( { _value: 0, get value() { return this._value }, }, { _value: observable.ref, value: observable.computed, } ) const handler = jest.fn() autorun(() => { handler(obs.value) }) expect(handler).toBeCalledTimes(1) obs._value = 123 expect(handler).toBeCalledTimes(2) }) ================================================ FILE: packages/reactive/src/__tests__/array.spec.ts ================================================ import { toArray, ArraySet } from '../array' test('toArray', () => { expect(toArray([])).toEqual([]) expect(toArray(null)).toEqual([]) expect(toArray(undefined)).toEqual([]) expect(toArray(0)).toEqual([0]) }) test('ArraySet', () => { const set = new ArraySet() set.add(11) set.add(11) set.add(22) expect(set.value).toEqual([11, 22]) const handler = jest.fn() set.forEach(handler) expect(handler).toBeCalledTimes(2) expect(handler).nthCalledWith(1, 11) expect(handler).nthCalledWith(2, 22) expect(set.value).toEqual([11, 22]) expect(set.has(11)).toBe(true) set.delete(11) expect(set.has(11)).toBe(false) expect(set.value).toEqual([22]) set.clear() expect(set.value).toEqual([]) const handler1 = jest.fn() set.add(11) set.add(22) set.batchDelete(handler1) expect(handler1).toBeCalledTimes(2) expect(handler1).nthCalledWith(1, 11) expect(handler1).nthCalledWith(2, 22) expect(set.value).toEqual([]) }) ================================================ FILE: packages/reactive/src/__tests__/autorun.spec.ts ================================================ import { observable, reaction, autorun } from '../' import { batch } from '../batch' import { define } from '../model' const sleep = (duration = 100) => new Promise((resolve) => setTimeout(resolve, duration)) test('autorun', () => { const obs = observable({ aa: { bb: 123, }, }) const handler = jest.fn() const dispose = autorun(() => { handler(obs.aa.bb) }) obs.aa.bb = 123 expect(handler).toBeCalledTimes(1) obs.aa.bb = 111 expect(handler).toBeCalledTimes(2) dispose() obs.aa.bb = 222 expect(handler).toBeCalledTimes(2) }) test('reaction', () => { const obs = observable({ aa: { bb: 123, }, }) const handler = jest.fn() const dispose = reaction(() => { return obs.aa.bb }, handler) obs.aa.bb = 123 expect(handler).toBeCalledTimes(0) obs.aa.bb = 111 expect(handler).toBeCalledTimes(1) dispose() obs.aa.bb = 222 expect(handler).toBeCalledTimes(1) }) test('reaction fireImmediately', () => { const obs = observable({ aa: { bb: 123, }, }) const handler = jest.fn() const dispose = reaction( () => { return obs.aa.bb }, handler, { fireImmediately: true, } ) expect(handler).toBeCalledTimes(1) obs.aa.bb = 123 expect(handler).toBeCalledTimes(1) obs.aa.bb = 111 expect(handler).toBeCalledTimes(2) dispose() obs.aa.bb = 222 expect(handler).toBeCalledTimes(2) }) test('reaction untrack handler', () => { const obs = observable({ aa: { bb: 123, cc: 123, }, }) const handler = jest.fn() const dispose = reaction( () => { return obs.aa.bb }, () => { handler(obs.aa.cc) } ) obs.aa.bb = 222 obs.aa.cc = 222 expect(handler).toBeCalledTimes(1) dispose() }) test('reaction dirty check', () => { const obs: any = { aa: 123, } define(obs, { aa: observable.ref, }) const handler = jest.fn() reaction(() => { return obs.aa }, handler) batch(() => { obs.aa = 123 obs.aa = 123 }) expect(handler).toBeCalledTimes(0) }) test('reaction with shallow equals', () => { const obs: any = { aa: { bb: 123 }, } define(obs, { aa: observable.ref, }) const handler = jest.fn() reaction(() => { return obs.aa }, handler) obs.aa = { bb: 123 } expect(handler).toBeCalledTimes(1) expect(handler.mock.calls[0][0]).toEqual({ bb: 123 }) }) test('reaction with deep equals', () => { const obs: any = { aa: { bb: 123 }, } define(obs, { aa: observable.ref, }) const handler = jest.fn() reaction( () => { return obs.aa }, handler, { equals: (a, b) => JSON.stringify(a) === JSON.stringify(b), } ) obs.aa = { bb: 123 } expect(handler).toBeCalledTimes(0) }) test('autorun direct recursive react', () => { const obs = observable({ value: 1 }) autorun(() => { obs.value++ }) expect(obs.value).toEqual(2) }) test('autorun direct recursive react with if', () => { const obs1 = observable({}) const obs2 = observable({}) const fn = jest.fn() autorun(() => { if (!obs1.value) { obs1.value = '111' return } fn(obs1.value, obs2.value) }) obs2.value = '222' expect(fn).toBeCalledTimes(0) }) test('autorun indirect recursive react', () => { const obs1 = observable({}) const obs2 = observable({}) const obs3 = observable({}) autorun(() => { obs1.value = obs2.value + 1 }) autorun(() => { obs2.value = obs3.value + 1 }) autorun(() => { if (obs1.value) { obs3.value = obs1.value + 1 } else { obs3.value = 0 } }) expect(obs2.value).toEqual(1) expect(obs1.value).toEqual(2) obs3.value = 1 expect(obs2.value).toEqual(2) expect(obs1.value).toEqual(3) }) test('autorun indirect alive recursive react', () => { const aa = observable({}) const bb = observable({}) const cc = observable({}) batch(() => { autorun(() => { if (aa.value) { bb.value = aa.value + 1 } }) autorun(() => { if (aa.value && bb.value) { cc.value = aa.value + bb.value } }) batch(() => { aa.value = 1 }) }) expect(aa.value).toEqual(1) expect(bb.value).toEqual(2) expect(cc.value).toEqual(3) }) test('autorun direct recursive react with head track', () => { const obs1 = observable({}) const obs2 = observable({}) const fn = jest.fn() autorun(() => { const obs2Value = obs2.value if (!obs1.value) { obs1.value = '111' return } fn(obs1.value, obs2Value) }) obs2.value = '222' expect(fn).toBeCalledTimes(1) expect(fn).lastCalledWith('111', '222') }) test('autorun.memo', () => { const obs = observable({ bb: 0, }) const fn = jest.fn() autorun(() => { const value = autorun.memo(() => ({ aa: 0, })) fn(obs.bb, value.aa++) }) obs.bb++ obs.bb++ obs.bb++ obs.bb++ expect(fn).toBeCalledTimes(5) expect(fn).nthCalledWith(1, 0, 0) expect(fn).nthCalledWith(2, 1, 1) expect(fn).nthCalledWith(3, 2, 2) expect(fn).nthCalledWith(4, 3, 3) expect(fn).nthCalledWith(5, 4, 4) }) test('autorun.memo with observable', () => { const obs1 = observable({ aa: 0, }) const fn = jest.fn() const dispose = autorun(() => { const obs2 = autorun.memo(() => observable({ bb: 0, }) ) fn(obs1.aa, obs2.bb++) }) obs1.aa++ obs1.aa++ obs1.aa++ expect(fn).toBeCalledTimes(4) expect(fn).nthCalledWith(1, 0, 0) expect(fn).nthCalledWith(2, 1, 1) expect(fn).nthCalledWith(3, 2, 2) expect(fn).nthCalledWith(4, 3, 3) dispose() obs1.aa++ expect(fn).toBeCalledTimes(4) }) test('autorun.memo with observable and effect', async () => { const obs1 = observable({ aa: 0, }) const fn = jest.fn() const dispose = autorun(() => { const obs2 = autorun.memo(() => observable({ bb: 0, }) ) fn(obs1.aa, obs2.bb++) autorun.effect(() => { obs2.bb++ }, []) }) obs1.aa++ obs1.aa++ obs1.aa++ await sleep(0) expect(fn).toBeCalledTimes(5) expect(fn).nthCalledWith(1, 0, 0) expect(fn).nthCalledWith(2, 1, 1) expect(fn).nthCalledWith(3, 2, 2) expect(fn).nthCalledWith(4, 3, 3) expect(fn).nthCalledWith(5, 3, 5) dispose() obs1.aa++ expect(fn).toBeCalledTimes(5) }) test('autorun.memo with deps', () => { const obs = observable({ bb: 0, cc: 0, }) const fn = jest.fn() autorun(() => { const value = autorun.memo( () => ({ aa: 0, }), [obs.cc] ) fn(obs.bb, value.aa++) }) obs.bb++ obs.bb++ obs.bb++ obs.bb++ expect(fn).toBeCalledTimes(5) expect(fn).nthCalledWith(1, 0, 0) expect(fn).nthCalledWith(2, 1, 1) expect(fn).nthCalledWith(3, 2, 2) expect(fn).nthCalledWith(4, 3, 3) expect(fn).nthCalledWith(5, 4, 4) obs.cc++ expect(fn).toBeCalledTimes(6) expect(fn).nthCalledWith(6, 4, 0) }) test('autorun.memo with deps and dispose', () => { const obs = observable({ bb: 0, cc: 0, }) const fn = jest.fn() const dispose = autorun(() => { const value = autorun.memo( () => ({ aa: 0, }), [obs.cc] ) fn(obs.bb, value.aa++) }) obs.bb++ obs.bb++ obs.bb++ obs.bb++ expect(fn).toBeCalledTimes(5) expect(fn).lastCalledWith(4, 4) obs.cc++ expect(fn).toBeCalledTimes(6) expect(fn).lastCalledWith(4, 0) dispose() obs.bb++ obs.cc++ expect(fn).toBeCalledTimes(6) }) test('autorun.memo with invalid params', () => { const obs = observable({ bb: 0, }) const fn = jest.fn() autorun(() => { const value = autorun.memo({ aa: 0 } as any) fn(obs.bb, value) }) obs.bb++ obs.bb++ obs.bb++ obs.bb++ expect(fn).toBeCalledTimes(5) expect(fn).lastCalledWith(4, undefined) }) test('autorun.memo not in autorun', () => { expect(() => autorun.memo(() => ({ aa: 0 }))).toThrow() }) test('autorun no memo', () => { const obs = observable({ bb: 0, }) const fn = jest.fn() autorun(() => { const value = { aa: 0, } fn(obs.bb, value.aa++) }) obs.bb++ obs.bb++ obs.bb++ obs.bb++ expect(fn).toBeCalledTimes(5) expect(fn).nthCalledWith(1, 0, 0) expect(fn).nthCalledWith(2, 1, 0) expect(fn).nthCalledWith(3, 2, 0) expect(fn).nthCalledWith(4, 3, 0) expect(fn).nthCalledWith(5, 4, 0) }) test('autorun.effect', async () => { const obs = observable({ bb: 0, }) const fn = jest.fn() const effect = jest.fn() const disposer = jest.fn() const dispose = autorun(() => { autorun.effect(() => { effect() return disposer }, []) fn(obs.bb) }) obs.bb++ obs.bb++ obs.bb++ obs.bb++ await sleep(0) expect(fn).toBeCalledTimes(5) expect(fn).lastCalledWith(4) expect(effect).toBeCalledTimes(1) expect(disposer).toBeCalledTimes(0) dispose() await sleep(0) expect(effect).toBeCalledTimes(1) expect(disposer).toBeCalledTimes(1) }) test('autorun.effect dispose when autorun dispose', async () => { const obs = observable({ bb: 0, }) const fn = jest.fn() const effect = jest.fn() const disposer = jest.fn() const dispose = autorun(() => { autorun.effect(() => { effect() return disposer }, []) fn(obs.bb) }) obs.bb++ obs.bb++ obs.bb++ obs.bb++ dispose() await sleep(0) expect(fn).toBeCalledTimes(5) expect(fn).lastCalledWith(4) expect(effect).toBeCalledTimes(0) expect(disposer).toBeCalledTimes(0) }) test('autorun.effect with deps', async () => { const obs = observable({ bb: 0, cc: 0, }) const fn = jest.fn() const effect = jest.fn() const dispose = autorun(() => { autorun.effect(() => { effect() }, [obs.cc]) fn(obs.bb) }) obs.bb++ obs.bb++ obs.bb++ obs.bb++ expect(effect).toBeCalledTimes(0) await sleep(0) expect(fn).toBeCalledTimes(5) expect(fn).lastCalledWith(4) expect(effect).toBeCalledTimes(1) obs.cc++ expect(effect).toBeCalledTimes(1) await sleep(0) expect(fn).toBeCalledTimes(6) expect(fn).lastCalledWith(4) expect(effect).toBeCalledTimes(2) dispose() await sleep(0) expect(effect).toBeCalledTimes(2) }) test('autorun.effect with default deps', async () => { const obs = observable({ bb: 0, }) const fn = jest.fn() const effect = jest.fn() const dispose = autorun(() => { autorun.effect(() => { effect() }) fn(obs.bb) }) obs.bb++ obs.bb++ obs.bb++ obs.bb++ expect(effect).toBeCalledTimes(0) await sleep(0) expect(fn).toBeCalledTimes(5) expect(fn).lastCalledWith(4) expect(effect).toBeCalledTimes(5) dispose() await sleep(0) expect(effect).toBeCalledTimes(5) }) test('autorun.effect not in autorun', () => { expect(() => autorun.effect(() => {})).toThrow() }) test('autorun.effect with invalid params', () => { autorun.effect({} as any) }) test('autorun dispose in batch', () => { const obs = observable({ value: 123, }) const handler = jest.fn() const dispose = autorun(() => { handler(obs.value) }) batch(() => { obs.value = 321 dispose() }) expect(handler).toBeCalledTimes(1) }) test('set value by computed depend', () => { const obs = observable({}) const comp1 = observable.computed(() => { return obs.aa?.bb }) const comp2 = observable.computed(() => { return obs.aa?.cc }) const handler = jest.fn() autorun(() => { handler(comp1.value, comp2.value) }) obs.aa = { bb: 123, cc: 321, } expect(handler).toBeCalledTimes(2) expect(handler).nthCalledWith(1, undefined, undefined) expect(handler).nthCalledWith(2, 123, 321) }) test('delete value by computed depend', () => { const handler = jest.fn() const obs = observable({ a: { b: 1, c: 2, }, }) const comp1 = observable.computed(() => { return obs.a?.b }) const comp2 = observable.computed(() => { return obs.a?.c }) autorun(() => { handler(comp1.value, comp2.value) }) delete obs.a expect(handler).toBeCalledTimes(2) expect(handler).nthCalledWith(1, 1, 2) expect(handler).nthCalledWith(2, undefined, undefined) }) test('set Set value by computed depend', () => { const handler = jest.fn() const obs = observable({ set: new Set(), }) const comp1 = observable.computed(() => { return obs.set.has(1) }) const comp2 = observable.computed(() => { return obs.set.size }) autorun(() => { handler(comp1.value, comp2.value) }) obs.set.add(1) expect(handler).toBeCalledTimes(2) expect(handler).nthCalledWith(1, false, 0) expect(handler).nthCalledWith(2, true, 1) }) test('delete Set by computed depend', () => { const handler = jest.fn() const obs = observable({ set: new Set([1]), }) const comp1 = observable.computed(() => { return obs.set.has(1) }) const comp2 = observable.computed(() => { return obs.set.size }) autorun(() => { handler(comp1.value, comp2.value) }) obs.set.delete(1) expect(handler).toBeCalledTimes(2) expect(handler).nthCalledWith(1, true, 1) expect(handler).nthCalledWith(2, false, 0) }) test('set Map value by computed depend', () => { const handler = jest.fn() const obs = observable({ map: new Map(), }) const comp1 = observable.computed(() => { return obs.map.has(1) }) const comp2 = observable.computed(() => { return obs.map.size }) autorun(() => { handler(comp1.value, comp2.value) }) obs.map.set(1, 1) expect(handler).toBeCalledTimes(2) expect(handler).nthCalledWith(1, false, 0) expect(handler).nthCalledWith(2, true, 1) }) test('delete Map by computed depend', () => { const handler = jest.fn() const obs = observable({ map: new Map([[1, 1]]), }) const comp1 = observable.computed(() => { return obs.map.has(1) }) const comp2 = observable.computed(() => { return obs.map.size }) autorun(() => { handler(comp1.value, comp2.value) }) obs.map.delete(1) expect(handler).toBeCalledTimes(2) expect(handler).nthCalledWith(1, true, 1) expect(handler).nthCalledWith(2, false, 0) }) test('autorun recollect dependencies', () => { const obs = observable({ aa: 'aaa', bb: 'bbb', cc: 'ccc', }) const fn = jest.fn() autorun(() => { fn() if (obs.aa === 'aaa') { return obs.bb } return obs.cc }) obs.aa = '111' obs.bb = '222' expect(fn).toBeCalledTimes(2) }) test('reaction recollect dependencies', () => { const obs = observable({ aa: 'aaa', bb: 'bbb', cc: 'ccc', }) const fn1 = jest.fn() const fn2 = jest.fn() const trigger1 = jest.fn() const trigger2 = jest.fn() reaction(() => { fn1() if (obs.aa === 'aaa') { return obs.bb } return obs.cc }, trigger1) reaction( () => { fn2() if (obs.aa === 'aaa') { return obs.bb } return obs.cc }, trigger2, { fireImmediately: true, } ) obs.aa = '111' obs.bb = '222' expect(fn1).toBeCalledTimes(2) expect(trigger1).toBeCalledTimes(1) expect(fn2).toBeCalledTimes(2) expect(trigger2).toBeCalledTimes(2) }) ================================================ FILE: packages/reactive/src/__tests__/batch.spec.ts ================================================ import { observable, batch, autorun, reaction } from '..' import { define } from '../model' describe('normal batch', () => { test('no batch', () => { const obs = observable({ aa: { bb: 123, }, }) const handler = jest.fn() autorun(() => { handler(obs.aa.bb) }) obs.aa.bb = 111 obs.aa.bb = 222 expect(handler).toBeCalledTimes(3) obs.aa.bb = 333 obs.aa.bb = 444 expect(handler).toBeCalledTimes(5) }) test('batch', () => { const obs = observable({ aa: { bb: 123, }, }) const handler = jest.fn() autorun(() => { handler(obs.aa.bb) }) obs.aa.bb = 111 obs.aa.bb = 222 expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(222) batch(() => { obs.aa.bb = 333 obs.aa.bb = 444 }) batch(() => {}) batch() expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(444) }) test('batch track', () => { const obs = observable({ aa: { bb: 123, }, cc: 1, }) const handler = jest.fn() autorun(() => { batch(() => { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } }) }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(2) expect(obs.cc).toEqual(41) }) test('batch.bound', () => { const obs = observable({ aa: { bb: 123, }, }) const handler = jest.fn() const setData = batch.bound(() => { obs.aa.bb = 333 obs.aa.bb = 444 }) autorun(() => { handler(obs.aa.bb) }) obs.aa.bb = 111 obs.aa.bb = 222 expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(222) setData() batch(() => {}) expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(444) }) test('batch.bound track', () => { const obs = observable({ aa: { bb: 123, }, cc: 1, }) const handler = jest.fn() autorun(() => { batch.bound(() => { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } })() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(2) expect(obs.cc).toEqual(41) }) test('batch.scope', () => { const obs = observable({}) const handler = jest.fn() autorun(() => { handler(obs.aa, obs.bb, obs.cc, obs.dd) }) batch(() => { batch.scope(() => { obs.aa = 123 }) batch.scope(() => { obs.cc = 'ccccc' }) obs.bb = 321 obs.dd = 'ddddd' }) expect(handler).toBeCalledTimes(4) expect(handler).nthCalledWith(1, undefined, undefined, undefined, undefined) expect(handler).nthCalledWith(2, 123, undefined, undefined, undefined) expect(handler).nthCalledWith(3, 123, undefined, 'ccccc', undefined) expect(handler).nthCalledWith(4, 123, 321, 'ccccc', 'ddddd') }) test('batch.scope bound', () => { const obs = observable({}) const handler = jest.fn() autorun(() => { handler(obs.aa, obs.bb, obs.cc, obs.dd) }) const scope1 = batch.scope.bound(() => { obs.aa = 123 }) batch(() => { scope1() batch.scope.bound(() => { obs.cc = 'ccccc' })() obs.bb = 321 obs.dd = 'ddddd' }) expect(handler).toBeCalledTimes(4) expect(handler).nthCalledWith(1, undefined, undefined, undefined, undefined) expect(handler).nthCalledWith(2, 123, undefined, undefined, undefined) expect(handler).nthCalledWith(3, 123, undefined, 'ccccc', undefined) expect(handler).nthCalledWith(4, 123, 321, 'ccccc', 'ddddd') }) test('batch.scope track', () => { const obs = observable({ aa: { bb: 123, }, cc: 1, }) const handler = jest.fn() autorun(() => { batch.scope(() => { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } }) }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(2) expect(obs.cc).toEqual(41) }) test('batch.scope bound track', () => { const obs = observable({ aa: { bb: 123, }, cc: 1, }) const handler = jest.fn() autorun(() => { batch.scope.bound(() => { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } })() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(2) expect(obs.cc).toEqual(41) }) test('batch error', () => { let error = null try { batch(() => { throw '123' }) } catch (e) { error = e } expect(error).toEqual('123') }) }) describe('annotation batch', () => { test('batch', () => { const obs = define( { aa: { bb: 123, }, setData() { this.aa.bb = 333 this.aa.bb = 444 }, }, { aa: observable, setData: batch, } ) const handler = jest.fn() autorun(() => { handler(obs.aa.bb) }) obs.aa.bb = 111 obs.aa.bb = 222 expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(222) obs.setData() expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(444) }) test('batch track', () => { const obs = define( { aa: { bb: 123, }, cc: 1, setData() { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } }, }, { aa: observable, setData: batch, } ) const handler = jest.fn() autorun(() => { obs.setData() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(2) expect(obs.cc).toEqual(41) }) test('batch.bound', () => { const obs = define( { aa: { bb: 123, }, setData() { this.aa.bb = 333 this.aa.bb = 444 }, }, { aa: observable, setData: batch.bound, } ) const handler = jest.fn() autorun(() => { handler(obs.aa.bb) }) obs.aa.bb = 111 obs.aa.bb = 222 expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(222) obs.setData() expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(444) }) test('batch.bound track', () => { const obs = define( { aa: { bb: 123, }, cc: 1, setData() { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } }, }, { aa: observable, setData: batch.bound, } ) const handler = jest.fn() autorun(() => { obs.setData() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(2) expect(obs.cc).toEqual(41) }) test('batch.scope', () => { const obs = define( { aa: null, bb: null, cc: null, dd: null, scope1() { this.aa = 123 }, scope2() { this.cc = 'ccccc' }, }, { aa: observable, bb: observable, cc: observable, dd: observable, scope1: batch.scope, scope2: batch.scope, } ) const handler = jest.fn() autorun(() => { handler(obs.aa, obs.bb, obs.cc, obs.dd) }) batch(() => { obs.scope1() obs.scope2() obs.bb = 321 obs.dd = 'ddddd' }) expect(handler).toBeCalledTimes(4) expect(handler).nthCalledWith(1, null, null, null, null) expect(handler).nthCalledWith(2, 123, null, null, null) expect(handler).nthCalledWith(3, 123, null, 'ccccc', null) expect(handler).nthCalledWith(4, 123, 321, 'ccccc', 'ddddd') }) test('batch.scope bound', () => { const obs = define( { aa: null, bb: null, cc: null, dd: null, scope1() { this.aa = 123 }, scope2() { this.cc = 'ccccc' }, }, { aa: observable, bb: observable, cc: observable, dd: observable, scope1: batch.scope.bound, scope2: batch.scope.bound, } ) const handler = jest.fn() autorun(() => { handler(obs.aa, obs.bb, obs.cc, obs.dd) }) batch(() => { obs.scope1() obs.scope2() obs.bb = 321 obs.dd = 'ddddd' }) expect(handler).toBeCalledTimes(4) expect(handler).nthCalledWith(1, null, null, null, null) expect(handler).nthCalledWith(2, 123, null, null, null) expect(handler).nthCalledWith(3, 123, null, 'ccccc', null) expect(handler).nthCalledWith(4, 123, 321, 'ccccc', 'ddddd') }) test('batch.scope track', () => { const obs = define( { aa: { bb: 123, }, cc: 1, scope() { if (this.cc > 0) { handler(this.aa.bb) this.cc = this.cc + 20 } }, }, { aa: observable, cc: observable, scope: batch.scope, } ) const handler = jest.fn() autorun(() => { obs.scope() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(2) expect(obs.cc).toEqual(41) }) test('batch.scope bound track', () => { const obs = define( { aa: { bb: 123, }, cc: 1, scope() { if (this.cc > 0) { handler(this.aa.bb) this.cc = this.cc + 20 } }, }, { aa: observable, cc: observable, scope: batch.scope.bound, } ) const handler = jest.fn() autorun(() => { obs.scope() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(2) expect(obs.cc).toEqual(41) }) }) describe('batch endpoint', () => { test('normal endpoint', () => { const tokens = [] const inner = batch.bound(() => { batch.endpoint(() => { tokens.push('endpoint') }) tokens.push('inner') }) const wrapper = batch.bound(() => { inner() tokens.push('wrapper') }) wrapper() expect(tokens).toEqual(['inner', 'wrapper', 'endpoint']) }) test('unexpect endpoint', () => { const tokens = [] const inner = batch.bound(() => { batch.endpoint() tokens.push('inner') }) const wrapper = batch.bound(() => { inner() tokens.push('wrapper') }) wrapper() expect(tokens).toEqual(['inner', 'wrapper']) }) test('no wrapper endpoint', () => { const tokens = [] batch.endpoint(() => { tokens.push('endpoint') }) expect(tokens).toEqual(['endpoint']) }) }) test('reaction collect in batch valid', () => { const obs = observable({ aa: 11, bb: 22, cc: 33, }) reaction( () => obs.aa, () => { void obs.cc } ) const fn = jest.fn() autorun(() => { batch.scope(() => { obs.aa = obs.bb }) fn() }) obs.bb = 44 expect(fn).toBeCalledTimes(2) }) test('reaction collect in batch invalid', () => { const obs = observable({ aa: 11, bb: 22, cc: 33, }) reaction( () => obs.aa, () => { void obs.cc } ) const fn = jest.fn() autorun(() => { batch.scope(() => { obs.aa = obs.bb }) fn() }) obs.bb = 44 obs.cc = 55 expect(fn).toBeCalledTimes(3) }) ================================================ FILE: packages/reactive/src/__tests__/collections-map.spec.ts ================================================ import { observable, autorun, raw } from '..' describe('Map', () => { test('should be a proper JS Map', () => { const map = observable(new Map()) expect(map).toBeInstanceOf(Map) expect(raw(map)).toBeInstanceOf(Map) }) test('should autorun mutations', () => { const handler = jest.fn() const map = observable(new Map()) autorun(() => handler(map.get('key'))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(undefined) map.set('key', 'value') expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith('value') map.set('key', 'value2') expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith('value2') map.delete('key') expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(undefined) }) test('should autorun size mutations', () => { const handler = jest.fn() const map = observable(new Map()) autorun(() => handler(map.size)) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) map.set('key1', 'value') map.set('key2', 'value2') expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(2) map.delete('key1') expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(1) map.clear() expect(handler).toBeCalledTimes(5) expect(handler).lastCalledWith(0) }) test('should autorun for of iteration', () => { const handler = jest.fn() const map = observable(new Map()) autorun(() => { let sum = 0 // eslint-disable-next-line no-unused-vars for (let [, num] of map) { sum += num } handler(sum) }) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) map.set('key0', 3) expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(3) map.set('key1', 2) expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(5) map.delete('key0') expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(2) map.clear() expect(handler).toBeCalledTimes(5) expect(handler).lastCalledWith(0) }) test('should autorun forEach iteration', () => { const handler = jest.fn() const map = observable(new Map()) autorun(() => { let sum = 0 map.forEach((num) => (sum += num)) handler(sum) }) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) map.set('key0', 3) expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(3) map.set('key1', 2) expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(5) map.delete('key0') expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(2) map.clear() expect(handler).toBeCalledTimes(5) expect(handler).lastCalledWith(0) }) test('should autorun keys iteration', () => { const handler = jest.fn() const map = observable(new Map()) autorun(() => { let sum = 0 for (let key of map.keys()) { sum += key } handler(sum) }) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) map.set(3, 3) expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(3) map.set(2, 2) expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(5) map.delete(3) expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(2) map.clear() expect(handler).toBeCalledTimes(5) expect(handler).lastCalledWith(0) }) test('should autorun values iteration', () => { const handler = jest.fn() const map = observable(new Map()) autorun(() => { let sum = 0 for (let num of map.values()) { sum += num } handler(sum) }) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) map.set('key0', 3) expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(3) map.set('key1', 2) expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(5) map.delete('key0') expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(2) map.clear() expect(handler).toBeCalledTimes(5) expect(handler).lastCalledWith(0) }) test('should autorun entries iteration', () => { const handler = jest.fn() const map = observable(new Map()) autorun(() => { let sum = 0 // eslint-disable-next-line no-unused-vars for (let [, num] of map.entries()) { sum += num } handler(sum) }) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) map.set('key0', 3) expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(3) map.set('key1', 2) expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(5) map.delete('key0') expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(2) map.clear() expect(handler).toBeCalledTimes(5) expect(handler).lastCalledWith(0) }) test('should be triggered by clearing', () => { const handler = jest.fn() const map = observable(new Map()) autorun(() => handler(map.get('key'))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(undefined) map.set('key', 3) expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(3) map.clear() expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(undefined) }) test('should not autorun custom property mutations', () => { const handler = jest.fn() const map = observable(new Map()) autorun(() => handler(map['customProp'])) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(undefined) map['customProp'] = 'Hello World' expect(handler).toBeCalledTimes(1) }) test('should not autorun non value changing mutations', () => { const handler = jest.fn() const map = observable(new Map()) autorun(() => handler(map.get('key'))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(undefined) map.set('key', 'value') expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith('value') map.set('key', 'value') expect(handler).toBeCalledTimes(2) map.delete('key') expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(undefined) map.delete('key') expect(handler).toBeCalledTimes(3) map.clear() expect(handler).toBeCalledTimes(3) }) test('should not autorun raw data', () => { const handler = jest.fn() const map = observable(new Map()) autorun(() => handler(raw(map).get('key'))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(undefined) map.set('key', 'Hello') expect(handler).toBeCalledTimes(1) map.delete('key') expect(handler).toBeCalledTimes(1) }) test('should not autorun raw iterations', () => { const handler = jest.fn() const map = observable(new Map()) autorun(() => { let sum = 0 // eslint-disable-next-line no-unused-vars for (let [, num] of raw(map).entries()) { sum += num } for (let key of raw(map).keys()) { sum += raw(map).get(key) } for (let num of raw(map).values()) { sum += num } raw(map).forEach((num) => { sum += num }) // eslint-disable-next-line no-unused-vars for (let [, num] of raw(map)) { sum += num } handler(sum) }) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) map.set('key1', 2) map.set('key2', 3) expect(handler).toBeCalledTimes(1) map.delete('key1') expect(handler).toBeCalledTimes(1) }) test('should not be triggered by raw mutations', () => { const handler = jest.fn() const map = observable(new Map()) autorun(() => handler(map.get('key'))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(undefined) raw(map).set('key', 'Hello') expect(handler).toBeCalledTimes(1) raw(map).delete('key') expect(handler).toBeCalledTimes(1) raw(map).clear() expect(handler).toBeCalledTimes(1) }) test('should not autorun raw size mutations', () => { const handler = jest.fn() const map = observable(new Map()) autorun(() => handler(raw(map).size)) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) map.set('key', 'value') expect(handler).toBeCalledTimes(1) }) test('should not be triggered by raw size mutations', () => { const handler = jest.fn() const map = observable(new Map()) autorun(() => handler(map.size)) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) raw(map).set('key', 'value') expect(handler).toBeCalledTimes(1) }) test('should support objects as key', () => { const handler = jest.fn() const key = {} const map = observable(new Map()) autorun(() => handler(map.get(key))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(undefined) map.set(key, 1) expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(1) map.set({}, 2) expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(1) }) test('observer object', () => { const handler = jest.fn() const map = observable(new Map>([])) map.set('key', {}) map.set('key2', observable({})) autorun(() => { const [obs1, obs2] = [...map.values()] handler(obs1.aa, obs2.aa) }) expect(handler).toBeCalledTimes(1) const obs1 = map.get('key') const obs2 = map.get('key2') obs1.aa = '123' obs2.aa = '234' expect(handler).toBeCalledTimes(3) }) test('shallow', () => { const handler = jest.fn() const map = observable.shallow(new Map>([])) map.set('key', {}) autorun(() => { const [obs] = [...map.values()] handler(obs.aa) }) expect(handler).toBeCalledTimes(1) const obs = map.get('key') obs.aa = '123' expect(handler).toBeCalledTimes(1) }) }) ================================================ FILE: packages/reactive/src/__tests__/collections-set.spec.ts ================================================ import { observable, autorun, raw } from '..' describe('Set', () => { test('should be a proper JS Set', () => { const set = observable(new Set()) expect(set).toBeInstanceOf(Set) expect(raw(set)).toBeInstanceOf(Set) }) test('should autorun mutations', () => { const handler = jest.fn() const set = observable(new Set()) autorun(() => handler(set.has('value'))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(false) set.add('value') expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(true) set.delete('value') expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(false) }) test('should autorun size mutations', () => { const handler = jest.fn() const set = observable(new Set()) autorun(() => handler(set.size)) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) set.add('value') set.add('value2') expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(2) set.delete('value') expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(1) set.clear() expect(handler).toBeCalledTimes(5) expect(handler).lastCalledWith(0) }) test('should autorun for of iteration', () => { const handler = jest.fn() const set = observable(new Set()) autorun(() => { let sum = 0 // eslint-disable-next-line no-unused-vars for (let num of set) { sum += num } handler(sum) }) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) set.add(3) expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(3) set.add(2) expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(5) set.delete(3) expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(2) set.clear() expect(handler).toBeCalledTimes(5) expect(handler).lastCalledWith(0) }) test('should autorun forEach iteration', () => { const handler = jest.fn() const set = observable(new Set()) autorun(() => { let sum = 0 set.forEach((num) => (sum += num)) handler(sum) }) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) set.add(3) expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(3) set.add(2) expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(5) set.delete(3) expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(2) set.clear() expect(handler).toBeCalledTimes(5) expect(handler).lastCalledWith(0) }) test('should autorun keys iteration', () => { const handler = jest.fn() const set = observable(new Set()) autorun(() => { let sum = 0 for (let key of set.keys()) { sum += key } handler(sum) }) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) set.add(3) expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(3) set.add(2) expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(5) set.delete(3) expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(2) set.clear() expect(handler).toBeCalledTimes(5) expect(handler).lastCalledWith(0) }) test('should autorun values iteration', () => { const handler = jest.fn() const set = observable(new Set()) autorun(() => { let sum = 0 for (let num of set.values()) { sum += num } handler(sum) }) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) set.add(3) expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(3) set.add(2) expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(5) set.delete(3) expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(2) set.clear() expect(handler).toBeCalledTimes(5) expect(handler).lastCalledWith(0) }) test('should autorun entries iteration', () => { const handler = jest.fn() const set = observable(new Set()) autorun(() => { let sum = 0 // eslint-disable-next-line no-unused-vars for (let [, num] of set.entries()) { sum += num } handler(sum) }) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) set.add(3) expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(3) set.add(2) expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(5) set.delete(3) expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(2) set.clear() expect(handler).toBeCalledTimes(5) expect(handler).lastCalledWith(0) }) test('should not autorun custom property mutations', () => { const handler = jest.fn() const set = observable(new Set()) autorun(() => handler(set['customProp'])) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(undefined) set['customProp'] = 'Hello World' expect(handler).toBeCalledTimes(1) }) test('should not autorun non value changing mutations', () => { const handler = jest.fn() const set = observable(new Set()) autorun(() => handler(set.has('value'))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(false) set.add('value') expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(true) set.add('value') expect(handler).toBeCalledTimes(2) set.delete('value') expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(false) set.delete('value') expect(handler).toBeCalledTimes(3) set.clear() expect(handler).toBeCalledTimes(3) }) test('should not autorun raw data', () => { const handler = jest.fn() const set = observable(new Set()) autorun(() => handler(raw(set).has('value'))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(false) set.add('value') expect(handler).toBeCalledTimes(1) set.delete('value') expect(handler).toBeCalledTimes(1) }) test('should not autorun raw iterations', () => { const handler = jest.fn() const set = observable(new Set()) autorun(() => { let sum = 0 // eslint-disable-next-line no-unused-vars for (let [, num] of raw(set).entries()) { sum += num } for (let key of raw(set).keys()) { sum += key } for (let num of raw(set).values()) { sum += num } raw(set).forEach((num) => { sum += num }) // eslint-disable-next-line no-unused-vars for (let num of raw(set)) { sum += num } handler(sum) }) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) set.add(2) set.add(3) expect(handler).toBeCalledTimes(1) set.delete(2) expect(handler).toBeCalledTimes(1) }) test('should not be triggered by raw mutations', () => { const handler = jest.fn() const set = observable(new Set()) autorun(() => handler(set.has('value'))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(false) raw(set).add('value') expect(handler).toBeCalledTimes(1) raw(set).delete('value') expect(handler).toBeCalledTimes(1) raw(set).clear() expect(handler).toBeCalledTimes(1) }) test('should not autorun raw size mutations', () => { const handler = jest.fn() const set = observable(new Set()) autorun(() => handler(raw(set).size)) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) set.add('value') expect(handler).toBeCalledTimes(1) }) test('should not be triggered by raw size mutations', () => { const handler = jest.fn() const set = observable(new Set()) autorun(() => handler(set.size)) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(0) raw(set).add('value') expect(handler).toBeCalledTimes(1) }) }) ================================================ FILE: packages/reactive/src/__tests__/collections-weakmap.spec.ts ================================================ import { observable, autorun, raw } from '..' describe('WeakMap', () => { test('should be a proper JS WeakMap', () => { const weakMap = observable(new WeakMap()) expect(weakMap).toBeInstanceOf(WeakMap) expect(raw(weakMap)).toBeInstanceOf(WeakMap) }) test('should autorun mutations', () => { const handler = jest.fn() const key = {} const weakMap = observable(new WeakMap()) autorun(() => handler(weakMap.get(key))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(undefined) weakMap.set(key, 'value') expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith('value') weakMap.set(key, 'value2') expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith('value2') weakMap.delete(key) expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(undefined) }) test('should not autorun custom property mutations', () => { const handler = jest.fn() const weakMap = observable(new WeakMap()) autorun(() => handler(weakMap['customProp'])) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(undefined) weakMap['customProp'] = 'Hello World' expect(handler).toBeCalledTimes(1) }) test('should not autorun non value changing mutations', () => { const handler = jest.fn() const key = {} const weakMap = observable(new WeakMap()) autorun(() => handler(weakMap.get(key))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(undefined) weakMap.set(key, 'value') expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith('value') weakMap.set(key, 'value') expect(handler).toBeCalledTimes(2) weakMap.delete(key) expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(undefined) weakMap.delete(key) expect(handler).toBeCalledTimes(3) }) test('should not autorun raw data', () => { const handler = jest.fn() const key = {} const weakMap = observable(new WeakMap()) autorun(() => handler(raw(weakMap).get(key))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(undefined) weakMap.set(key, 'Hello') expect(handler).toBeCalledTimes(1) weakMap.delete(key) expect(handler).toBeCalledTimes(1) }) test('should not be triggered by raw mutations', () => { const handler = jest.fn() const key = {} const weakMap = observable(new WeakMap()) autorun(() => handler(weakMap.get(key))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(undefined) raw(weakMap).set(key, 'Hello') expect(handler).toBeCalledTimes(1) raw(weakMap).delete(key) expect(handler).toBeCalledTimes(1) }) }) ================================================ FILE: packages/reactive/src/__tests__/collections-weakset.spec.ts ================================================ import { observable, autorun, raw } from '..' describe('WeakSet', () => { test('should be a proper JS WeakSet', () => { const weakSet = observable(new WeakSet()) expect(weakSet).toBeInstanceOf(WeakSet) expect(raw(weakSet)).toBeInstanceOf(WeakSet) }) test('should autorun mutations', () => { const handler = jest.fn() const value = {} const weakSet = observable(new WeakSet()) autorun(() => handler(weakSet.has(value))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(false) weakSet.add(value) expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(true) weakSet.delete(value) expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(false) }) test('should not autorun custom property mutations', () => { const handler = jest.fn() const weakSet = observable(new WeakSet()) autorun(() => handler(weakSet['customProp'])) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(undefined) weakSet['customProp'] = 'Hello World' expect(handler).toBeCalledTimes(1) }) test('should not autorun non value changing mutations', () => { const handler = jest.fn() const value = {} const weakSet = observable(new WeakSet()) autorun(() => handler(weakSet.has(value))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(false) weakSet.add(value) expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(true) weakSet.add(value) expect(handler).toBeCalledTimes(2) weakSet.delete(value) expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(false) weakSet.delete(value) expect(handler).toBeCalledTimes(3) }) test('should not autorun raw data', () => { const handler = jest.fn() const value = {} const weakSet = observable(new WeakSet()) autorun(() => handler(raw(weakSet).has(value))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(false) weakSet.add(value) expect(handler).toBeCalledTimes(1) weakSet.delete(value) expect(handler).toBeCalledTimes(1) }) test('should not be triggered by raw mutations', () => { const handler = jest.fn() const value = {} const weakSet = observable(new WeakSet()) autorun(() => handler(weakSet.has(value))) expect(handler).toBeCalledTimes(1) expect(handler).lastCalledWith(false) raw(weakSet).add(value) expect(handler).toBeCalledTimes(1) raw(weakSet).delete(value) expect(handler).toBeCalledTimes(1) }) }) ================================================ FILE: packages/reactive/src/__tests__/define.spec.ts ================================================ import { define, model, observable, autorun } from '..' import { observe } from '../observe' import { FormPath } from '@formily/shared' import { batch } from '../batch' describe('makeObservable', () => { test('observable annotation', () => { const target: any = { aa: {}, } define(target, { aa: observable, }) const handler = jest.fn() const handler1 = jest.fn() const handler2 = jest.fn() autorun(() => { handler(FormPath.getIn(target, 'aa.bb.cc')) }) observe(target, handler1) observe(target.aa, handler2) target.aa.bb = { cc: { dd: { ee: 123 } } } target.aa = { hh: 123 } expect(handler).toBeCalledTimes(3) expect(handler).nthCalledWith(1, undefined) expect(handler).nthCalledWith(2, { dd: { ee: 123 } }) expect(handler).nthCalledWith(3, undefined) expect(handler1).toBeCalledTimes(2) expect(handler2).toBeCalledTimes(2) }) test('shallow annotation', () => { const target: any = { aa: {}, } define(target, { aa: observable.shallow, }) const handler = jest.fn() const handler1 = jest.fn() const handler2 = jest.fn() autorun(() => { handler(FormPath.getIn(target, 'aa.bb.cc')) }) observe(target, handler1) observe(target.aa, handler2) target.aa.bb = { cc: { dd: { ee: 123 } } } target.aa.bb.cc.kk = 333 target.aa = { hh: 123 } expect(handler).toBeCalledTimes(3) expect(handler).nthCalledWith(1, undefined) expect(handler).nthCalledWith(2, { dd: { ee: 123 }, kk: 333 }) expect(handler).nthCalledWith(3, undefined) expect(handler1).toBeCalledTimes(2) expect(handler2).toBeCalledTimes(2) }) test('box annotation', () => { const target: any = {} define(target, { aa: observable.box, }) const handler = jest.fn() const handler1 = jest.fn() const handler2 = jest.fn() autorun(() => { handler(target.aa.get()) }) observe(target, handler1) observe(target.aa, handler2) expect(handler).lastCalledWith(undefined) target.aa.set(123) expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(123) expect(handler1).toBeCalledTimes(1) expect(handler2).toBeCalledTimes(1) }) test('ref annotation', () => { const target: any = {} define(target, { aa: observable.ref, }) const handler = jest.fn() const handler1 = jest.fn() autorun(() => { handler(target.aa) }) observe(target, handler1) expect(handler).lastCalledWith(undefined) target.aa = 123 expect(handler).toBeCalledTimes(2) expect(handler).lastCalledWith(123) expect(handler1).toBeCalledTimes(1) }) test('action annotation', () => { const target = { aa: { bb: null, cc: null, }, setData() { target.aa.bb = 123 target.aa.cc = 312 }, } define(target, { aa: observable, setData: batch, }) const handler = jest.fn() autorun(() => { handler([target.aa.bb, target.aa.cc]) }) expect(handler).toBeCalledTimes(1) target.setData() expect(handler).toBeCalledTimes(2) }) test('computed annotation', () => { const handler = jest.fn() const target = { aa: 11, bb: 22, get cc() { handler() return this.aa + this.bb }, } define(target, { aa: observable, bb: observable, cc: observable.computed, }) autorun(() => { target.cc }) expect(handler).toBeCalledTimes(1) expect(target.cc).toEqual(33) target.aa = 22 expect(handler).toBeCalledTimes(2) expect(target.cc).toEqual(44) }) test('unexpect target', () => { const testFn = jest.fn() const testArr = [] const obs1 = define(4 as any, { value: observable.computed, }) const obs2 = define('123' as any, { value: observable.computed, }) const obs3 = define(testFn as any, { value: observable.computed, }) const obs4 = define(testArr as any, { value: observable.computed, }) expect(obs1).toBe(4) expect(obs2).toBe('123') expect(obs3).toBe(testFn) expect(obs4).toBe(testArr) }) }) test('define model', () => { const obs = model({ aa: 1, action() { this.aa++ }, }) const { action } = obs action() expect(obs.aa).toEqual(2) }) ================================================ FILE: packages/reactive/src/__tests__/externals.spec.ts ================================================ import { isObservable, isSupportObservable, markObservable, markRaw, observable, toJS, } from '..' test('is support observable', () => { const obs = observable({ aa: 111 }) class Class {} expect(isSupportObservable(obs)).toBe(true) expect(isSupportObservable(new Class())).toBe(true) expect(isSupportObservable(null)).toBe(false) expect(isSupportObservable([])).toBe(true) expect(isSupportObservable({})).toBe(true) expect(isSupportObservable({ $$typeof: {}, _owner: {} })).toBe(false) expect(isSupportObservable({ _isAMomentObject: {} })).toBe(false) expect(isSupportObservable({ _isJSONSchemaObject: {} })).toBe(false) expect(isSupportObservable({ toJS: () => {} })).toBe(false) expect(isSupportObservable({ toJSON: () => {} })).toBe(false) expect(isSupportObservable(new Map())).toBe(true) expect(isSupportObservable(new WeakMap())).toBe(true) expect(isSupportObservable(new Set())).toBe(true) expect(isSupportObservable(new WeakSet())).toBe(true) }) describe('mark operation', () => { test('plain object should be observable', () => { const obs = observable({ aa: 111 }) expect(isObservable(obs)).toBe(true) }) test('class instance should be observable', () => { class Class {} const obs = observable(new Class()) const obs2 = observable(new Class()) expect(isObservable(obs)).toBe(true) expect(isObservable(obs2)).toBe(true) }) test('object with toJS function should NOT be observable', () => { const obs = observable({ aa: 111, toJS: () => {} }) expect(isObservable(obs)).toBe(false) }) test('plain object marked as raw should NOT be observable', () => { const obs = observable(markRaw({ aa: 111 })) expect(isObservable(obs)).toBe(false) }) test('class marked as raw instance should NOT be observable', () => { class Class {} markRaw(Class) const obs = observable(new Class()) const obs2 = observable(new Class()) expect(isObservable(obs)).toBe(false) expect(isObservable(obs2)).toBe(false) }) test('object with toJS function marked as observable should be observable', () => { const obs = observable(markObservable({ aa: 111, toJS: () => {} })) expect(isObservable(obs)).toBe(true) }) test('plain object marked as raw and observable should NOT be observable', () => { const obs = observable(markRaw(markObservable({ aa: 111 }))) expect(isObservable(obs)).toBe(false) }) test('plain object marked as observable and raw should NOT be observable', () => { const obs = observable(markObservable(markRaw({ aa: 111 }))) expect(isObservable(obs)).toBe(false) }) test('function marked as observable should NOT be observable', () => { const obs = observable(markObservable(() => {})) expect(isObservable(obs)).toBe(false) }) }) test('recursive references tojs', () => { const obj: any = { aa: 111 } obj.obj = obj const obs = observable(obj) obs.obs = obs expect(toJS(obs)).toBeTruthy() const arrObs = observable([{ aa: 1 }, { bb: 2 }, { cc: 3 }]) expect(toJS(arrObs)).toEqual([{ aa: 1 }, { bb: 2 }, { cc: 3 }]) }) ================================================ FILE: packages/reactive/src/__tests__/hasCollected.spec.ts ================================================ import { observable, hasCollected, autorun } from '../' test('hasCollected', () => { const obs = observable({ value: '' }) autorun(() => { expect( hasCollected(() => { obs.value }) ).toBe(true) expect(hasCollected(() => {})).toBe(false) expect(hasCollected()).toBe(false) }) }) ================================================ FILE: packages/reactive/src/__tests__/observable.spec.ts ================================================ import { observable } from '../' import { contains } from '../externals' test('array mutation', () => { const arr = observable([1, 2, 3, 4]) arr.splice(2, 1) expect(arr).toEqual([1, 2, 4]) }) test('observable contains', () => { const subElement = { cc: 333 } const element = { aa: subElement } const arr = observable([element, 2, 3, 4]) expect(contains(arr, arr[0])).toBe(true) expect(contains(arr, arr[0].aa)).toBe(true) expect(contains(arr, element)).toBe(true) expect(contains(arr, subElement)).toBe(true) expect(contains(element, subElement)).toBe(true) expect(contains(element, arr[0].aa)).toBe(true) expect(contains(arr[0], subElement)).toBe(true) const obj = observable({}) const other = { bb: 321 } expect(contains(obj, obj.other)).toBe(false) obj.other = other obj.arr = arr expect(contains(obj, obj.other)).toBe(true) expect(contains(obj, other)).toBe(true) expect(contains(obj, obj.arr)).toBe(true) expect(contains(obj, arr)).toBe(true) }) test('observable __proto__', () => { const observableArr = observable([] as any[]) // @ts-ignore observableArr.__proto__ = Object.create(Array.prototype) observableArr[0] = {} expect(observableArr).toEqual([{}]) const observableObj = observable({} as any) // @ts-ignore observableObj.__proto__ = Object.create(Object.prototype) observableObj.aa = {} expect(observableObj).toEqual({ aa: {} }) }) ================================================ FILE: packages/reactive/src/__tests__/observe.spec.ts ================================================ import { observable, observe } from '../' test('deep observe', () => { const obs = observable({ aa: { bb: { cc: [11, 22, 33], }, }, ee: observable([]), }) const handler = jest.fn() observe(obs, handler) obs.dd = 123 obs.aa.bb.cc.push(44) expect(obs.aa.bb.cc).toEqual([11, 22, 33, 44]) expect(handler).toHaveBeenCalledTimes(2) delete obs.aa expect(handler).toHaveBeenCalledTimes(3) // Are these expected behaviors? obs.ee.push(11) expect(handler).toHaveBeenCalledTimes(3) obs.ee = [] expect(handler).toHaveBeenCalledTimes(4) obs.ee.push(11) expect(handler).toHaveBeenCalledTimes(5) }) test('shallow observe', () => { const obs = observable({ aa: { bb: { cc: [11, 22, 33], }, }, }) const handler = jest.fn() observe(obs, handler, false) obs.dd = 123 obs.aa.bb.cc.push(44) expect(obs.aa.bb.cc).toEqual([11, 22, 33, 44]) expect(handler).toHaveBeenCalledTimes(1) delete obs.aa expect(handler).toHaveBeenCalledTimes(2) }) test('root replace observe', () => { const obs = observable({ aa: { bb: { cc: [11, 22, 33], }, }, }) const handler1 = jest.fn() const handler = jest.fn() observe(obs, handler1) observe(obs.aa, handler) obs.aa = { mm: 123, } expect(handler1).toBeCalledTimes(1) expect(handler).toBeCalledTimes(1) obs.aa = { bb: { cc: [11, 22, 33], }, } obs.aa.bb.cc.push(44) expect(handler1).toBeCalledTimes(3) expect(handler).toBeCalledTimes(3) }) test('dispose observe', () => { const obs = observable({ aa: { bb: { cc: [11, 22, 33], }, }, }) const handler = jest.fn() const dispose = observe(obs, handler) obs.kk = 123 expect(handler).toBeCalledTimes(1) dispose() obs.aa = 123 expect(handler).toBeCalledTimes(1) }) test('dispose observe', () => { const obs = observable({ aa: { bb: { cc: [11, 22, 33], }, }, }) const handler = jest.fn() const dispose = observe(obs.aa, handler) obs.kk = 111 expect(handler).toBeCalledTimes(0) obs.aa = { mm: 222 } expect(handler).toBeCalledTimes(1) obs.aa = { mm: 222 } expect(handler).toBeCalledTimes(2) obs.aa = { mm: '111' } expect(handler).toBeCalledTimes(3) obs.aa = { mm: 333 } expect(handler).toBeCalledTimes(4) dispose() obs.aa = { mm: 444 } expect(handler).toBeCalledTimes(4) }) test('array delete', () => { const array = observable([{ value: 1 }, { value: 2 }]) const fn = jest.fn() const dispose = observe(array, (change) => { if (change.type === 'set' && change.key === 'value') { fn(change.path?.join('.')) } }) array[0].value = 3 expect(fn.mock.calls[0][0]).toBe('0.value') array.splice(0, 1) array[0].value = 3 expect(fn.mock.calls[1][0]).toBe('0.value') dispose() }) test('observe dynamic tree', () => { const handler = jest.fn() const tree = observable({}) const childTree = observable({}) tree.children = childTree observe(tree, handler) tree.children.aa = 123 expect(handler).toBeCalledTimes(1) }) test('invalid target', () => { expect(() => observe(function () {})).toThrowError() }) ================================================ FILE: packages/reactive/src/__tests__/tracker.spec.ts ================================================ import { Tracker, observable } from '../' test('base tracker', () => { const obs = observable({}) const fn = jest.fn() const view = () => { fn(obs.value) } const scheduler = () => { tracker.track(view) } const tracker = new Tracker(scheduler) tracker.track(view) obs.value = 123 expect(fn).nthCalledWith(1, undefined) expect(fn).nthCalledWith(2, 123) tracker.dispose() }) test('nested tracker', () => { const obs = observable({}) const fn = jest.fn() const view = () => { obs.value = obs.value || 321 fn(obs.value) } const scheduler = () => { tracker.track(view) } const tracker = new Tracker(scheduler) tracker.track(view) expect(fn).toBeCalledTimes(1) expect(fn).nthCalledWith(1, 321) obs.value = 123 expect(fn).toBeCalledTimes(2) expect(fn).nthCalledWith(2, 123) tracker.dispose() }) test('tracker recollect dependencies', () => { const obs = observable({ aa: 'aaa', bb: 'bbb', cc: 'ccc', }) const fn = jest.fn() const view = () => { fn() if (obs.aa === 'aaa') { return obs.bb } return obs.cc } const scheduler = () => { tracker.track(view) } const tracker = new Tracker(scheduler) tracker.track(view) obs.aa = '111' obs.bb = '222' expect(fn).toBeCalledTimes(2) tracker.dispose() }) test('shared scheduler with multi tracker(mock react strict mode)', () => { const obs = observable({}) const component = () => obs.value const render = () => { tracker1.track(component) tracker2.track(component) } const scheduler1 = jest.fn(() => { tracker2.track(component) }) const scheduler2 = jest.fn(() => { tracker1.track(component) }) const tracker1 = new Tracker(scheduler1, 'tracker1') const tracker2 = new Tracker(scheduler2, 'tracker2') render() obs.value = 123 expect(scheduler1).toBeCalledTimes(1) expect(scheduler2).toBeCalledTimes(0) }) ================================================ FILE: packages/reactive/src/__tests__/untracked.spec.ts ================================================ import { untracked, observable, autorun } from '../' test('basic untracked', () => { const obs = observable({}) const fn = jest.fn() autorun(() => { untracked(() => { fn(obs.value) }) }) expect(fn).toBeCalledTimes(1) obs.value = 123 expect(fn).toBeCalledTimes(1) }) test('no params untracked', () => { untracked() }) ================================================ FILE: packages/reactive/src/action.ts ================================================ import { batchStart, batchEnd, batchScopeStart, batchScopeEnd, untrackStart, untrackEnd, } from './reaction' import { createBoundaryAnnotation } from './internals' import { IAction } from './types' export const action: IAction = createBoundaryAnnotation( () => { batchStart() untrackStart() }, () => { untrackEnd() batchEnd() } ) action.scope = createBoundaryAnnotation( () => { batchScopeStart() untrackStart() }, () => { untrackEnd() batchScopeEnd() } ) ================================================ FILE: packages/reactive/src/annotations/box.ts ================================================ import { ProxyRaw, RawProxy } from '../environment' import { createAnnotation } from '../internals' import { buildDataTree } from '../tree' import { bindTargetKeyWithCurrentReaction, runReactionsFromTargetKey, } from '../reaction' export interface IBox { (target: T): { get(): T; set(value: T): void } } export const box: IBox = createAnnotation(({ target, key, value }) => { const store = { value: target ? target[key] : value, } const proxy = { set, get, } ProxyRaw.set(proxy, store) RawProxy.set(store, proxy) buildDataTree(target, key, store) function get() { bindTargetKeyWithCurrentReaction({ target: store, key, type: 'get', }) return store.value } function set(value: any) { const oldValue = store.value store.value = value if (oldValue !== value) { runReactionsFromTargetKey({ target: store, key, type: 'set', oldValue, value, }) } } if (target) { Object.defineProperty(target, key, { value: proxy, enumerable: true, configurable: false, writable: false, }) return target } return proxy }) ================================================ FILE: packages/reactive/src/annotations/computed.ts ================================================ import { ObModelSymbol, ReactionStack } from '../environment' import { createAnnotation } from '../internals' import { buildDataTree } from '../tree' import { isFn } from '../checkers' import { bindTargetKeyWithCurrentReaction, runReactionsFromTargetKey, bindComputedReactions, hasRunningReaction, isUntracking, batchStart, batchEnd, releaseBindingReactions, } from '../reaction' interface IValue { value?: T } export interface IComputed { (compute: () => T): IValue (compute: { get?: () => T; set?: (value: T) => void }): IValue } const getDescriptor = Object.getOwnPropertyDescriptor const getProto = Object.getPrototypeOf const ClassDescriptorSymbol = Symbol('ClassDescriptorSymbol') function getPropertyDescriptor(obj: any, key: PropertyKey) { if (!obj) return return getDescriptor(obj, key) || getPropertyDescriptor(getProto(obj), key) } function getPropertyDescriptorCache(obj: any, key: PropertyKey) { const constructor = obj.constructor if (constructor === Object || constructor === Array) return getPropertyDescriptor(obj, key) const cache = constructor[ClassDescriptorSymbol] || {} const descriptor = cache[key] if (descriptor) return descriptor const newDesc = getPropertyDescriptor(obj, key) constructor[ClassDescriptorSymbol] = cache cache[key] = newDesc return newDesc } function getPrototypeDescriptor( target: any, key: PropertyKey, value: any ): PropertyDescriptor { if (!target) { if (value) { if (isFn(value)) { return { get: value } } else { return value } } return {} } const descriptor = getPropertyDescriptorCache(target, key) if (descriptor) { return descriptor } return {} } export const computed: IComputed = createAnnotation( ({ target, key, value }) => { const store: IValue = {} const proxy = {} const context = target ? target : store const property = target ? key : 'value' const descriptor = getPrototypeDescriptor(target, property, value) function compute() { store.value = descriptor.get?.call(context) } function reaction() { if (ReactionStack.indexOf(reaction) === -1) { releaseBindingReactions(reaction) try { ReactionStack.push(reaction) compute() } finally { ReactionStack.pop() } } } reaction._name = 'ComputedReaction' reaction._scheduler = () => { reaction._dirty = true runReactionsFromTargetKey({ target: context, key: property, value: store.value, type: 'set', }) } reaction._isComputed = true reaction._dirty = true reaction._context = context reaction._property = property function get() { if (hasRunningReaction()) { bindComputedReactions(reaction) } if (!isUntracking()) { //如果允许untracked过程中收集依赖,那么永远不会存在绑定,因为_dirty已经设置为false if (reaction._dirty) { reaction() reaction._dirty = false } } else { compute() } bindTargetKeyWithCurrentReaction({ target: context, key: property, type: 'get', }) return store.value } function set(value: any) { try { batchStart() descriptor.set?.call(context, value) } finally { batchEnd() } } if (target) { Object.defineProperty(target, key, { get, set, enumerable: true, }) return target } else { Object.defineProperty(proxy, 'value', { set, get, }) buildDataTree(target, key, store) proxy[ObModelSymbol] = store } return proxy } ) ================================================ FILE: packages/reactive/src/annotations/index.ts ================================================ export * from './observable' export * from './box' export * from './ref' export * from './shallow' export * from './computed' ================================================ FILE: packages/reactive/src/annotations/observable.ts ================================================ import { createAnnotation, createObservable } from '../internals' import { bindTargetKeyWithCurrentReaction, runReactionsFromTargetKey, } from '../reaction' export interface IObservable { (target: T): T } export const observable: IObservable = createAnnotation( ({ target, key, value }) => { const store = { value: createObservable(target, key, target ? target[key] : value), } function get() { bindTargetKeyWithCurrentReaction({ target: target, key: key, type: 'get', }) return store.value } function set(value: any) { const oldValue = store.value value = createObservable(target, key, value) store.value = value if (oldValue === value) return runReactionsFromTargetKey({ target: target, key: key, type: 'set', oldValue, value, }) } if (target) { Object.defineProperty(target, key, { set, get, enumerable: true, configurable: false, }) return target } return store.value } ) ================================================ FILE: packages/reactive/src/annotations/ref.ts ================================================ import { ObModelSymbol } from '../environment' import { createAnnotation } from '../internals' import { buildDataTree } from '../tree' import { bindTargetKeyWithCurrentReaction, runReactionsFromTargetKey, } from '../reaction' export interface IRef { (target: T): { value: T } } export const ref: IRef = createAnnotation(({ target, key, value }) => { const store = { value: target ? target[key] : value, } const proxy = {} const context = target ? target : store const property = target ? key : 'value' function get() { bindTargetKeyWithCurrentReaction({ target: context, key: property, type: 'get', }) return store.value } function set(value: any) { const oldValue = store.value store.value = value if (oldValue !== value) { runReactionsFromTargetKey({ target: context, key: property, type: 'set', oldValue, value, }) } } if (target) { Object.defineProperty(target, key, { get, set, enumerable: true, }) return target } else { Object.defineProperty(proxy, 'value', { set, get, }) buildDataTree(target, key, store) proxy[ObModelSymbol] = store } return proxy }) ================================================ FILE: packages/reactive/src/annotations/shallow.ts ================================================ import { createAnnotation, createObservable } from '../internals' import { bindTargetKeyWithCurrentReaction, runReactionsFromTargetKey, } from '../reaction' import { IObservable } from './observable' export const shallow: IObservable = createAnnotation( ({ target, key, value }) => { const store = { value: createObservable(target, key, target ? target[key] : value, true), } function get() { bindTargetKeyWithCurrentReaction({ target: target, key: key, type: 'get', }) return store.value } function set(value: any) { const oldValue = store.value value = createObservable(target, key, value, true) store.value = value if (oldValue === value) return runReactionsFromTargetKey({ target: target, key: key, type: 'set', oldValue, value, }) } if (target) { Object.defineProperty(target, key, { set, get, enumerable: true, configurable: false, }) return target } return store.value } ) ================================================ FILE: packages/reactive/src/array.ts ================================================ export const toArray = (value: any) => { return Array.isArray(value) ? value : value !== undefined && value !== null ? [value] : [] } export class ArraySet { value: T[] forEachIndex = 0 constructor(value: T[] = []) { this.value = value } add(item: T) { if (!this.has(item)) { this.value.push(item) } } has(item: T) { return this.value.indexOf(item) > -1 } delete(item: T) { const len = this.value.length if (len === 0) return if (len === 1 && this.value[0] === item) { this.value = [] return } const findIndex = this.value.indexOf(item) if (findIndex > -1) { this.value.splice(findIndex, 1) if (findIndex <= this.forEachIndex) { this.forEachIndex -= 1 } } } forEach(callback: (value: T) => void) { if (this.value.length === 0) return this.forEachIndex = 0 for (; this.forEachIndex < this.value.length; this.forEachIndex++) { callback(this.value[this.forEachIndex]) } } batchDelete(callback: (value: T) => void) { if (this.value.length === 0) return this.forEachIndex = 0 for (; this.forEachIndex < this.value.length; this.forEachIndex++) { const value = this.value[this.forEachIndex] this.value.splice(this.forEachIndex, 1) this.forEachIndex-- callback(value) } } clear() { this.value.length = 0 } } ================================================ FILE: packages/reactive/src/autorun.ts ================================================ import { batchEnd, batchStart, disposeBindingReactions, releaseBindingReactions, disposeEffects, hasDepsChange, } from './reaction' import { isFn } from './checkers' import { ReactionStack } from './environment' import { Reaction, IReactionOptions, Dispose } from './types' import { toArray } from './array' interface IValue { currentValue?: any oldValue?: any } export const autorun = (tracker: Reaction, name = 'AutoRun') => { const reaction: Reaction = () => { if (!isFn(tracker)) return if (reaction._boundary > 0) return if (ReactionStack.indexOf(reaction) === -1) { releaseBindingReactions(reaction) try { batchStart() ReactionStack.push(reaction) tracker() } finally { ReactionStack.pop() reaction._boundary++ batchEnd() reaction._boundary = 0 reaction._memos.cursor = 0 reaction._effects.cursor = 0 } } } const cleanRefs = () => { reaction._memos = { queue: [], cursor: 0, } reaction._effects = { queue: [], cursor: 0, } } reaction._boundary = 0 reaction._name = name cleanRefs() reaction() return () => { disposeBindingReactions(reaction) disposeEffects(reaction) cleanRefs() } } autorun.memo = (callback: () => T, dependencies?: any[]): T => { if (!isFn(callback)) return const current = ReactionStack[ReactionStack.length - 1] if (!current || !current._memos) throw new Error('autorun.memo must used in autorun function body.') const deps = toArray(dependencies || []) const id = current._memos.cursor++ const old = current._memos.queue[id] if (!old || hasDepsChange(deps, old.deps)) { const value = callback() current._memos.queue[id] = { value, deps, } return value } return old.value } autorun.effect = (callback: () => void | Dispose, dependencies?: any[]) => { if (!isFn(callback)) return const current = ReactionStack[ReactionStack.length - 1] if (!current || !current._effects) throw new Error('autorun.effect must used in autorun function body.') const effects = current._effects const deps = toArray(dependencies || [{}]) const id = effects.cursor++ const old = effects.queue[id] if (!old || hasDepsChange(deps, old.deps)) { Promise.resolve(0).then(() => { if (current._disposed) return const dispose = callback() if (isFn(dispose)) { effects.queue[id].dispose = dispose } }) effects.queue[id] = { deps, } } } export const reaction = ( tracker: () => T, subscriber?: (value: T, oldValue: T) => void, options?: IReactionOptions ) => { const realOptions = { name: 'Reaction', ...options, } const value: IValue = {} const dirtyCheck = () => { if (isFn(realOptions.equals)) return !realOptions.equals(value.oldValue, value.currentValue) return value.oldValue !== value.currentValue } const fireAction = () => { try { //如果untrack的话,会导致用户如果在scheduler里同步调用setState影响下次React渲染的依赖收集 batchStart() if (isFn(subscriber)) subscriber(value.currentValue, value.oldValue) } finally { batchEnd() } } const reaction: Reaction = () => { if (ReactionStack.indexOf(reaction) === -1) { releaseBindingReactions(reaction) try { ReactionStack.push(reaction) value.currentValue = tracker() } finally { ReactionStack.pop() } } } reaction._scheduler = (looping) => { looping() if (dirtyCheck()) fireAction() value.oldValue = value.currentValue } reaction._name = realOptions.name reaction() value.oldValue = value.currentValue if (realOptions.fireImmediately) { fireAction() } return () => { disposeBindingReactions(reaction) } } ================================================ FILE: packages/reactive/src/batch.ts ================================================ import { batchStart, batchEnd, batchScopeStart, batchScopeEnd, } from './reaction' import { BatchEndpoints, BatchCount } from './environment' import { createBoundaryAnnotation } from './internals' import { IBatch } from './types' import { isFn } from './checkers' export const batch: IBatch = createBoundaryAnnotation(batchStart, batchEnd) batch.scope = createBoundaryAnnotation(batchScopeStart, batchScopeEnd) batch.endpoint = (callback?: () => void) => { if (!isFn(callback)) return if (BatchCount.value === 0) { callback() } else { BatchEndpoints.add(callback) } } ================================================ FILE: packages/reactive/src/checkers.ts ================================================ const toString = Object.prototype.toString export const isMap = (val: any): val is Map => val && val instanceof Map export const isSet = (val: any): val is Set => val && val instanceof Set export const isWeakMap = (val: any): val is WeakMap => val && val instanceof WeakMap export const isWeakSet = (val: any): val is WeakSet => val && val instanceof WeakSet export const isFn = (val: any): val is Function => typeof val === 'function' export const isArr = Array.isArray export const isPlainObj = (val: any): val is object => toString.call(val) === '[object Object]' export const isValid = (val: any) => val !== null && val !== undefined export const isCollectionType = (target: any) => { return ( isMap(target) || isWeakMap(target) || isSet(target) || isWeakSet(target) ) } export const isNormalType = (target: any) => { return isPlainObj(target) || isArr(target) } ================================================ FILE: packages/reactive/src/environment.ts ================================================ import { ObservableListener, Reaction, ReactionsMap } from './types' import { ArraySet } from './array' import { DataNode } from './tree' export const ProxyRaw = new WeakMap() export const RawProxy = new WeakMap() export const RawShallowProxy = new WeakMap() export const RawNode = new WeakMap() export const RawReactionsMap = new WeakMap() export const ReactionStack: Reaction[] = [] export const BatchCount = { value: 0 } export const UntrackCount = { value: 0 } export const BatchScope = { value: false } export const DependencyCollected = { value: false } export const PendingReactions = new ArraySet() export const PendingScopeReactions = new ArraySet() export const BatchEndpoints = new ArraySet<() => void>() export const ObserverListeners = new ArraySet() export const MakeObModelSymbol = Symbol('MakeObModelSymbol') export const ObModelSymbol = Symbol('ObModelSymbol') export const ObModelNodeSymbol = Symbol('ObModelNodeSymbol') ================================================ FILE: packages/reactive/src/externals.ts ================================================ import { isValid, isFn, isMap, isWeakMap, isSet, isWeakSet, isPlainObj, isArr, } from './checkers' import { ProxyRaw, MakeObModelSymbol, DependencyCollected, ObModelSymbol, } from './environment' import { getDataNode } from './tree' import { Annotation } from './types' const RAW_TYPE = Symbol('RAW_TYPE') const OBSERVABLE_TYPE = Symbol('OBSERVABLE_TYPE') const hasOwnProperty = Object.prototype.hasOwnProperty export const isObservable = (target: any) => { return ProxyRaw.has(target) || !!target?.[ObModelSymbol] } export const isAnnotation = (target: any): target is Annotation => { return target && !!target[MakeObModelSymbol] } export const isSupportObservable = (target: any) => { if (!isValid(target)) return false if (isArr(target)) return true if (isPlainObj(target)) { if (target[RAW_TYPE]) { return false } if (target[OBSERVABLE_TYPE]) { return true } if ('$$typeof' in target && '_owner' in target) { return false } if (target['_isAMomentObject']) { return false } if (target['_isJSONSchemaObject']) { return false } if (isFn(target['toJS'])) { return false } if (isFn(target['toJSON'])) { return false } return true } if (isMap(target) || isWeakMap(target) || isSet(target) || isWeakSet(target)) return true return false } export const markRaw = (target: T): T => { if (!target) return if (isFn(target)) { target.prototype[RAW_TYPE] = true } else { target[RAW_TYPE] = true } return target } export const markObservable = (target: T): T => { if (!target) return if (isFn(target)) { target.prototype[OBSERVABLE_TYPE] = true } else { target[OBSERVABLE_TYPE] = true } return target } export const raw = (target: T): T => { if (target?.[ObModelSymbol]) return target[ObModelSymbol] return ProxyRaw.get(target as any) || target } export const toJS = (values: T): T => { const visited = new WeakSet() const _toJS: typeof toJS = (values: any) => { if (visited.has(values)) { return values } if (values && values[RAW_TYPE]) return values if (isArr(values)) { if (isObservable(values)) { visited.add(values) const res: any = [] values.forEach((item: any) => { res.push(_toJS(item)) }) visited.delete(values) return res } } else if (isPlainObj(values)) { if (isObservable(values)) { visited.add(values) const res: any = {} for (const key in values) { if (hasOwnProperty.call(values, key)) { res[key] = _toJS(values[key]) } } visited.delete(values) return res } } return values } return _toJS(values) } export const contains = (target: any, property: any) => { const targetRaw = raw(target) const propertyRaw = raw(property) if (targetRaw === propertyRaw) return true const targetNode = getDataNode(targetRaw) const propertyNode = getDataNode(propertyRaw) if (!targetNode) return false if (!propertyNode) return false return targetNode.contains(propertyNode) } export const hasCollected = (callback?: () => void) => { DependencyCollected.value = false callback?.() return DependencyCollected.value } ================================================ FILE: packages/reactive/src/global.d.ts ================================================ import * as Types from './types' declare global { namespace Formily.Reactive { export { Types } } } ================================================ FILE: packages/reactive/src/handlers.ts ================================================ import { bindTargetKeyWithCurrentReaction, runReactionsFromTargetKey, } from './reaction' import { ProxyRaw, RawProxy } from './environment' import { isObservable, isSupportObservable } from './externals' import { createObservable } from './internals' const wellKnownSymbols = new Set( Object.getOwnPropertyNames(Symbol).reduce((buf: Symbol[], key) => { if (key === 'arguments' || key === 'caller') return buf const value = Symbol[key] if (typeof value === 'symbol') return buf.concat(value) return buf }, []) ) const hasOwnProperty = Object.prototype.hasOwnProperty function findObservable(target: any, key: PropertyKey, value: any) { const observableObj = RawProxy.get(value) if (observableObj) { return observableObj } if (!isObservable(value) && isSupportObservable(value)) { return createObservable(target, key, value) } return value } function patchIterator( target: any, key: PropertyKey, iterator: IterableIterator, isEntries: boolean ) { const originalNext = iterator.next iterator.next = () => { let { done, value } = originalNext.call(iterator) if (!done) { if (isEntries) { value[1] = findObservable(target, key, value[1]) } else { value = findObservable(target, key, value) } } return { done, value } } return iterator } const instrumentations = { has(key: PropertyKey) { const target = ProxyRaw.get(this) const proto = Reflect.getPrototypeOf(this) as any bindTargetKeyWithCurrentReaction({ target, key, type: 'has' }) return proto.has.apply(target, arguments) }, get(key: PropertyKey) { const target = ProxyRaw.get(this) const proto = Reflect.getPrototypeOf(this) as any bindTargetKeyWithCurrentReaction({ target, key, type: 'get' }) return findObservable(target, key, proto.get.apply(target, arguments)) }, add(key: PropertyKey) { const target = ProxyRaw.get(this) const proto = Reflect.getPrototypeOf(this) as any const hadKey = proto.has.call(target, key) // forward the operation before queueing reactions const result = proto.add.apply(target, arguments) if (!hadKey) { runReactionsFromTargetKey({ target, key, value: key, type: 'add' }) } return result }, set(key: PropertyKey, value: any) { const target = ProxyRaw.get(this) const proto = Reflect.getPrototypeOf(this) as any const hadKey = proto.has.call(target, key) const oldValue = proto.get.call(target, key) // forward the operation before queueing reactions const result = proto.set.apply(target, arguments) if (!hadKey) { runReactionsFromTargetKey({ target, key, value, type: 'add' }) } else if (value !== oldValue) { runReactionsFromTargetKey({ target, key, value, oldValue, type: 'set' }) } return result }, delete(key: PropertyKey) { const target = ProxyRaw.get(this) const proto = Reflect.getPrototypeOf(this) as any const hadKey = proto.has.call(target, key) const oldValue = proto.get ? proto.get.call(target, key) : undefined // forward the operation before queueing reactions const result = proto.delete.apply(target, arguments) if (hadKey) { runReactionsFromTargetKey({ target, key, oldValue, type: 'delete' }) } return result }, clear() { const target = ProxyRaw.get(this) const proto = Reflect.getPrototypeOf(this) as any const hadItems = target.size !== 0 const oldTarget = target instanceof Map ? new Map(target) : new Set(target) // forward the operation before queueing reactions const result = proto.clear.apply(target, arguments) if (hadItems) { runReactionsFromTargetKey({ target, oldTarget, type: 'clear' }) } return result }, forEach(cb: any, ...args: any[]) { const target = ProxyRaw.get(this) const proto = Reflect.getPrototypeOf(this) as any bindTargetKeyWithCurrentReaction({ target, type: 'iterate' }) // swap out the raw values with their observable pairs // before passing them to the callback const wrappedCb = (value: any, key: PropertyKey, ...args: any) => cb(findObservable(target, key, value), key, ...args) return proto.forEach.call(target, wrappedCb, ...args) }, keys() { const target = ProxyRaw.get(this) const proto = Reflect.getPrototypeOf(this) as any bindTargetKeyWithCurrentReaction({ target, type: 'iterate' }) return proto.keys.apply(target, arguments) }, values() { const target = ProxyRaw.get(this) const proto = Reflect.getPrototypeOf(this) as any bindTargetKeyWithCurrentReaction({ target, type: 'iterate' }) const iterator = proto.values.apply(target, arguments) return patchIterator(target, '', iterator, false) }, entries() { const target = ProxyRaw.get(this) const proto = Reflect.getPrototypeOf(this) as any bindTargetKeyWithCurrentReaction({ target, type: 'iterate' }) const iterator = proto.entries.apply(target, arguments) return patchIterator(target, '', iterator, true) }, [Symbol.iterator]() { const target = ProxyRaw.get(this) const proto = Reflect.getPrototypeOf(this) bindTargetKeyWithCurrentReaction({ target, type: 'iterate' }) const iterator = proto[Symbol.iterator].apply(target, arguments) return patchIterator(target, '', iterator, target instanceof Map) }, get size() { const target = ProxyRaw.get(this) const proto = Reflect.getPrototypeOf(this) bindTargetKeyWithCurrentReaction({ target, type: 'iterate' }) return Reflect.get(proto, 'size', target) }, } export const collectionHandlers = { get(target: any, key: PropertyKey, receiver: any) { // instrument methods and property accessors to be reactive target = hasOwnProperty.call(instrumentations, key) ? instrumentations : target return Reflect.get(target, key, receiver) }, } export const baseHandlers: ProxyHandler = { get(target, key, receiver) { if (!key) return const result = target[key] // use Reflect.get is too slow if (typeof key === 'symbol' && wellKnownSymbols.has(key)) { return result } bindTargetKeyWithCurrentReaction({ target, key, receiver, type: 'get' }) const observableResult = RawProxy.get(result) if (observableResult) { return observableResult } if (!isObservable(result) && isSupportObservable(result)) { const descriptor = Reflect.getOwnPropertyDescriptor(target, key) if ( !descriptor || !(descriptor.writable === false && descriptor.configurable === false) ) { return createObservable(target, key, result) } } return result }, has(target, key) { const result = Reflect.has(target, key) bindTargetKeyWithCurrentReaction({ target, key, type: 'has' }) return result }, ownKeys(target) { const keys = Reflect.ownKeys(target) bindTargetKeyWithCurrentReaction({ target, type: 'iterate' }) return keys }, set(target, key, value, receiver) { // vue2中有对数组原型重写,因此需去除此处proxy if (key === '__proto__') { target[key] = value return true } const hadKey = hasOwnProperty.call(target, key) const newValue = createObservable(target, key, value) const oldValue = target[key] target[key] = newValue // use Reflect.set is too slow if (!hadKey) { runReactionsFromTargetKey({ target, key, value: newValue, oldValue, receiver, type: 'add', }) } else if (value !== oldValue) { runReactionsFromTargetKey({ target, key, value: newValue, oldValue, receiver, type: 'set', }) } return true }, deleteProperty(target, key) { const oldValue = target[key] delete target[key] runReactionsFromTargetKey({ target, key, oldValue, type: 'delete', }) return true }, } ================================================ FILE: packages/reactive/src/index.ts ================================================ export * from './batch' export * from './action' export * from './untracked' export * from './observable' export * from './model' export * from './autorun' export * from './tracker' export * from './observe' export * from './externals' export * from './types' ================================================ FILE: packages/reactive/src/internals.ts ================================================ import { isFn, isCollectionType, isNormalType } from './checkers' import { RawProxy, ProxyRaw, MakeObModelSymbol, RawShallowProxy, } from './environment' import { baseHandlers, collectionHandlers } from './handlers' import { buildDataTree, getDataNode } from './tree' import { isSupportObservable } from './externals' import { PropertyKey, IVisitor, BoundaryFunction } from './types' const createNormalProxy = (target: any, shallow?: boolean) => { const proxy = new Proxy(target, baseHandlers) ProxyRaw.set(proxy, target) if (shallow) { RawShallowProxy.set(target, proxy) } else { RawProxy.set(target, proxy) } return proxy } const createCollectionProxy = (target: any, shallow?: boolean) => { const proxy = new Proxy(target, collectionHandlers) ProxyRaw.set(proxy, target) if (shallow) { RawShallowProxy.set(target, proxy) } else { RawProxy.set(target, proxy) } return proxy } const createShallowProxy = (target: any) => { if (isNormalType(target)) return createNormalProxy(target, true) if (isCollectionType(target)) return createCollectionProxy(target, true) // never reach return target } export const createObservable = ( target: any, key?: PropertyKey, value?: any, shallow?: boolean ) => { if (typeof value !== 'object') return value const raw = ProxyRaw.get(value) if (!!raw) { const node = getDataNode(raw) if (!node.target) node.target = target node.key = key return value } if (!isSupportObservable(value)) return value if (target) { const parentRaw = ProxyRaw.get(target) || target const isShallowParent = RawShallowProxy.get(parentRaw) if (isShallowParent) return value } buildDataTree(target, key, value) if (shallow) return createShallowProxy(value) if (isNormalType(value)) return createNormalProxy(value) if (isCollectionType(value)) return createCollectionProxy(value) // never reach return value } export const createAnnotation = any>( maker: T ) => { const annotation = (target: any): ReturnType => { return maker({ value: target }) } if (isFn(maker)) { annotation[MakeObModelSymbol] = maker } return annotation } export const getObservableMaker = (target: any) => { if (target[MakeObModelSymbol]) { if (!target[MakeObModelSymbol][MakeObModelSymbol]) { return target[MakeObModelSymbol] } return getObservableMaker(target[MakeObModelSymbol]) } } export const createBoundaryFunction = ( start: (...args: any) => void, end: (...args: any) => void ) => { function boundary any>(fn?: F): ReturnType { let results: ReturnType try { start() if (isFn(fn)) { results = fn() } } finally { end() } return results } boundary.bound = createBindFunction(boundary) return boundary } export const createBindFunction = ( boundary: Boundary ) => { function bind any>( callback?: F, context?: any ): F { return ((...args: any[]) => boundary(() => callback.apply(context, args))) as any } return bind } export const createBoundaryAnnotation = ( start: (...args: any) => void, end: (...args: any) => void ) => { const boundary = createBoundaryFunction(start, end) const annotation = createAnnotation(({ target, key }) => { target[key] = boundary.bound(target[key], target) return target }) boundary[MakeObModelSymbol] = annotation boundary.bound[MakeObModelSymbol] = annotation return boundary } ================================================ FILE: packages/reactive/src/model.ts ================================================ import { isFn } from './checkers' import { buildDataTree } from './tree' import { observable } from './observable' import { getObservableMaker } from './internals' import { isObservable, isAnnotation, isSupportObservable } from './externals' import { Annotations } from './types' import { action } from './action' import { ObModelSymbol } from './environment' export function define( target: Target, annotations?: Annotations ): Target { if (isObservable(target)) return target if (!isSupportObservable(target)) return target target[ObModelSymbol] = target buildDataTree(undefined, undefined, target) for (const key in annotations) { const annotation = annotations[key] if (isAnnotation(annotation)) { getObservableMaker(annotation)({ target, key, }) } } return target } export function model(target: Target): Target { const annotations = Object.keys(target || {}).reduce((buf, key) => { const descriptor = Object.getOwnPropertyDescriptor(target, key) if (descriptor && descriptor.get) { buf[key] = observable.computed } else if (isFn(target[key])) { buf[key] = action } else { buf[key] = observable } return buf }, {}) return define(target, annotations) } ================================================ FILE: packages/reactive/src/observable.ts ================================================ import * as annotations from './annotations' import { MakeObModelSymbol } from './environment' import { createObservable } from './internals' export function observable(target: T): T { return createObservable(null, null, target) } observable.box = annotations.box observable.ref = annotations.ref observable.deep = annotations.observable observable.shallow = annotations.shallow observable.computed = annotations.computed observable[MakeObModelSymbol] = annotations.observable ================================================ FILE: packages/reactive/src/observe.ts ================================================ import { IOperation } from './types' import { ObserverListeners } from './environment' import { raw as getRaw } from './externals' import { isFn } from './checkers' import { DataChange, getDataNode } from './tree' export const observe = ( target: object, observer?: (change: DataChange) => void, deep = true ) => { const addListener = (target: any) => { const raw = getRaw(target) const node = getDataNode(raw) const listener = (operation: IOperation) => { const targetRaw = getRaw(operation.target) const targetNode = getDataNode(targetRaw) if (deep) { if (node.contains(targetNode)) { observer(new DataChange(operation, targetNode)) return } } if ( node === targetNode || (node.targetRaw === targetRaw && node.key === operation.key) ) { observer(new DataChange(operation, targetNode)) } } if (node && isFn(observer)) { ObserverListeners.add(listener) } return () => { ObserverListeners.delete(listener) } } if (target && typeof target !== 'object') throw Error(`Can not observe ${typeof target} type.`) return addListener(target) } ================================================ FILE: packages/reactive/src/reaction.ts ================================================ import { isFn } from './checkers' import { ArraySet } from './array' import { IOperation, ReactionsMap, Reaction, PropertyKey } from './types' import { ReactionStack, PendingScopeReactions, BatchEndpoints, DependencyCollected, RawReactionsMap, PendingReactions, BatchCount, UntrackCount, BatchScope, ObserverListeners, } from './environment' const ITERATION_KEY = Symbol('iteration key') const addRawReactionsMap = ( target: any, key: PropertyKey, reaction: Reaction ) => { const reactionsMap = RawReactionsMap.get(target) if (reactionsMap) { const reactions = reactionsMap.get(key) if (reactions) { reactions.add(reaction) } else { reactionsMap.set(key, new ArraySet([reaction])) } return reactionsMap } else { const reactionsMap: ReactionsMap = new Map([ [key, new ArraySet([reaction])], ]) RawReactionsMap.set(target, reactionsMap) return reactionsMap } } const addReactionsMapToReaction = ( reaction: Reaction, reactionsMap: ReactionsMap ) => { const bindSet = reaction._reactionsSet if (bindSet) { bindSet.add(reactionsMap) } else { reaction._reactionsSet = new ArraySet([reactionsMap]) } return bindSet } const getReactionsFromTargetKey = (target: any, key: PropertyKey) => { const reactionsMap = RawReactionsMap.get(target) const reactions = [] if (reactionsMap) { const map = reactionsMap.get(key) if (map) { map.forEach((reaction) => { if (reactions.indexOf(reaction) === -1) { reactions.push(reaction) } }) } } return reactions } const runReactions = (target: any, key: PropertyKey) => { const reactions = getReactionsFromTargetKey(target, key) const prevUntrackCount = UntrackCount.value UntrackCount.value = 0 for (let i = 0, len = reactions.length; i < len; i++) { const reaction = reactions[i] if (reaction._isComputed) { reaction._scheduler(reaction) } else if (isScopeBatching()) { PendingScopeReactions.add(reaction) } else if (isBatching()) { PendingReactions.add(reaction) } else { // never reach if (isFn(reaction._scheduler)) { reaction._scheduler(reaction) } else { reaction() } } } UntrackCount.value = prevUntrackCount } const notifyObservers = (operation: IOperation) => { ObserverListeners.forEach((fn) => fn(operation)) } export const bindTargetKeyWithCurrentReaction = (operation: IOperation) => { let { key, type, target } = operation if (type === 'iterate') { key = ITERATION_KEY } const reactionLen = ReactionStack.length if (reactionLen === 0) return const current = ReactionStack[reactionLen - 1] if (isUntracking()) return if (current) { DependencyCollected.value = true addReactionsMapToReaction(current, addRawReactionsMap(target, key, current)) } } export const bindComputedReactions = (reaction: Reaction) => { if (isFn(reaction)) { const current = ReactionStack[ReactionStack.length - 1] if (current) { const computes = current._computesSet if (computes) { computes.add(reaction) } else { current._computesSet = new ArraySet([reaction]) } } } } export const runReactionsFromTargetKey = (operation: IOperation) => { let { key, type, target, oldTarget } = operation batchStart() notifyObservers(operation) if (type === 'clear') { oldTarget.forEach((_: any, key: PropertyKey) => { runReactions(target, key) }) } else { runReactions(target, key) } if (type === 'add' || type === 'delete' || type === 'clear') { const newKey = Array.isArray(target) ? 'length' : ITERATION_KEY runReactions(target, newKey) } batchEnd() } export const hasRunningReaction = () => { return ReactionStack.length > 0 } export const releaseBindingReactions = (reaction: Reaction) => { reaction._reactionsSet?.forEach((reactionsMap) => { reactionsMap.forEach((reactions) => { reactions.delete(reaction) }) }) PendingReactions.delete(reaction) PendingScopeReactions.delete(reaction) delete reaction._reactionsSet } export const suspendComputedReactions = (current: Reaction) => { current._computesSet?.forEach((reaction) => { const reactions = getReactionsFromTargetKey( reaction._context, reaction._property ) if (reactions.length === 0) { disposeBindingReactions(reaction) reaction._dirty = true } }) } export const disposeBindingReactions = (reaction: Reaction) => { reaction._disposed = true releaseBindingReactions(reaction) suspendComputedReactions(reaction) } export const batchStart = () => { BatchCount.value++ } export const batchEnd = () => { BatchCount.value-- if (BatchCount.value === 0) { const prevUntrackCount = UntrackCount.value UntrackCount.value = 0 executePendingReactions() executeBatchEndpoints() UntrackCount.value = prevUntrackCount } } export const batchScopeStart = () => { BatchScope.value = true } export const batchScopeEnd = () => { const prevUntrackCount = UntrackCount.value BatchScope.value = false UntrackCount.value = 0 PendingScopeReactions.batchDelete((reaction) => { if (isFn(reaction._scheduler)) { reaction._scheduler(reaction) } else { reaction() } }) UntrackCount.value = prevUntrackCount } export const untrackStart = () => { UntrackCount.value++ } export const untrackEnd = () => { UntrackCount.value-- } export const isBatching = () => BatchCount.value > 0 export const isScopeBatching = () => BatchScope.value export const isUntracking = () => UntrackCount.value > 0 export const executePendingReactions = () => { PendingReactions.batchDelete((reaction) => { if (isFn(reaction._scheduler)) { reaction._scheduler(reaction) } else { reaction() } }) } export const executeBatchEndpoints = () => { BatchEndpoints.batchDelete((callback) => { callback() }) } export const hasDepsChange = (newDeps: any[], oldDeps: any[]) => { if (newDeps === oldDeps) return false if (newDeps.length !== oldDeps.length) return true if (newDeps.some((value, index) => value !== oldDeps[index])) return true return false } export const disposeEffects = (reaction: Reaction) => { if (reaction._effects) { try { batchStart() reaction._effects.queue.forEach((item) => { if (!item || !item.dispose) return item.dispose() }) } finally { batchEnd() } } } ================================================ FILE: packages/reactive/src/tracker.ts ================================================ import { ReactionStack } from './environment' import { isFn } from './checkers' import { Reaction } from './types' import { batchEnd, batchStart, disposeBindingReactions, releaseBindingReactions, } from './reaction' export class Tracker { private results: any constructor( scheduler?: (reaction: Reaction) => void, name = 'TrackerReaction' ) { this.track._scheduler = (callback) => { if (this.track._boundary === 0) this.dispose() if (isFn(callback)) scheduler(callback) } this.track._name = name this.track._boundary = 0 } track: Reaction = (tracker: Reaction) => { if (!isFn(tracker)) return this.results if (this.track._boundary > 0) return if (ReactionStack.indexOf(this.track) === -1) { releaseBindingReactions(this.track) try { batchStart() ReactionStack.push(this.track) this.results = tracker() } finally { ReactionStack.pop() this.track._boundary++ batchEnd() this.track._boundary = 0 } } return this.results } dispose = () => { disposeBindingReactions(this.track) } } ================================================ FILE: packages/reactive/src/tree.ts ================================================ import { ObModelSymbol, ObModelNodeSymbol, RawNode } from './environment' import { raw as getRaw } from './externals' import { PropertyKey, IOperation } from './types' export class DataChange { node: DataNode key: PropertyKey object: object type: string value: any oldValue: any constructor(operation: IOperation, node: DataNode) { this.node = node this.key = operation.key this.type = operation.type this.object = operation.target this.value = operation.value this.oldValue = operation.oldValue } get path() { return this.node.path.concat(this.key) } } export class DataNode { target: any key: PropertyKey value: any constructor(target: any, key: PropertyKey, value: any) { this.target = target this.key = key this.value = value } get path() { if (!this.parent) return this.key ? [this.key] : [] return this.parent.path.concat(this.key) } get targetRaw() { return getRaw(this.target) } get parent() { if (!this.target) return return getDataNode(this.targetRaw) } isEqual(node: DataNode) { if (this.key) { return node.targetRaw === this.targetRaw && node.key === this.key } return node.value === this.value } contains(node: DataNode) { if (node === this) return true let parent = node.parent while (!!parent) { if (this.isEqual(parent)) return true parent = parent.parent } return false } } export const getDataNode = (raw: any) => { if (raw?.[ObModelNodeSymbol]) { return raw[ObModelNodeSymbol] } return RawNode.get(raw) } export const setDataNode = (raw: any, node: DataNode) => { if (raw?.[ObModelSymbol]) { raw[ObModelNodeSymbol] = node return } RawNode.set(raw, node) } export const buildDataTree = (target: any, key: PropertyKey, value: any) => { const raw = getRaw(value) const currentNode = getDataNode(raw) if (currentNode) return currentNode setDataNode(raw, new DataNode(target, key, value)) } ================================================ FILE: packages/reactive/src/types.ts ================================================ import { ArraySet } from './array' export * from './tree' export type PropertyKey = string | number | symbol export type OperationType = | 'add' | 'delete' | 'clear' | 'set' | 'get' | 'iterate' | 'has' export interface IOperation { target?: any oldTarget?: any key?: PropertyKey value?: any oldValue?: any type?: OperationType receiver?: any } export interface IChange { key?: PropertyKey path?: ObservablePath value?: any oldValue?: any type?: OperationType } export interface IEffectQueueItem { dispose?: void | Dispose deps?: any[] } export interface IMemoQueueItem { value?: any deps?: any[] } export interface IVisitor { target?: Target key?: PropertyKey value?: Value } export type Annotation = (...args: any[]) => any export type Annotations = { [key in keyof T]?: Annotation } export type ObservableListener = (operation: IOperation) => void export type ObservablePath = Array export type Dispose = () => void export type Effect = () => void | Dispose export type Reaction = ((...args: any[]) => any) & { _boundary?: number _name?: string _isComputed?: boolean _dirty?: boolean _context?: any _disposed?: boolean _property?: PropertyKey _computesSet?: ArraySet _reactionsSet?: ArraySet _scheduler?: (reaction: Reaction) => void _memos?: { queue: IMemoQueueItem[] cursor: number } _effects?: { queue: IEffectQueueItem[] cursor: number } } export type ReactionsMap = Map> export interface IReactionOptions { name?: string equals?: (oldValue: T, newValue: T) => boolean fireImmediately?: boolean } export type BindFunction any> = ( callback?: F, context?: any ) => F export type BoundaryFunction = any>( fn?: F ) => ReturnType export interface IBoundable { bound?: any>(callback: T, context?: any) => T //高阶绑定 } export interface IAction extends IBoundable { (callback?: () => T): T //原地action scope?: ((callback?: () => T) => T) & IBoundable //原地局部action } export interface IBatch extends IAction { endpoint?: (callback?: () => void) => void } ================================================ FILE: packages/reactive/src/untracked.ts ================================================ import { createBoundaryFunction } from './internals' import { untrackStart, untrackEnd } from './reaction' export const untracked = createBoundaryFunction(untrackStart, untrackEnd) ================================================ FILE: packages/reactive/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./lib", "paths": { "@formily/*": ["packages/*", "devtools/*"] }, "declaration": true } } ================================================ FILE: packages/reactive/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["./src/**/*.ts", "./src/**/*.tsx"], "exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"] } ================================================ FILE: packages/reactive-react/.npmignore ================================================ node_modules *.log build docs doc-site __tests__ .eslintrc jest.config.js tsconfig.json .umi src ================================================ FILE: packages/reactive-react/.umirc.js ================================================ import { resolve } from 'path' export default { mode: 'doc', logo: 'https://img.alicdn.com/imgextra/i2/O1CN01Kq3OHU1fph6LGqjIz_!!6000000004056-55-tps-1141-150.svg', title: 'Formily', hash: true, favicon: '//img.alicdn.com/imgextra/i3/O1CN01XtT3Tv1Wd1b5hNVKy_!!6000000002810-55-tps-360-360.svg', outputPath: './doc-site', headScripts: [ ` function loadAd(){ var header = document.querySelector('.__dumi-default-layout-content .markdown h1') if(header && !header.querySelector('#_carbonads_js')){ var script = document.createElement('script') script.src = '//cdn.carbonads.com/carbon.js?serve=CEAICK3M&placement=formilyjsorg' script.id = '_carbonads_js' script.classList.add('head-ad') header.appendChild(script) } } var request = null var observer = new MutationObserver(function(){ cancelIdleCallback(request) request = requestIdleCallback(loadAd) }) document.addEventListener('DOMContentLoaded',function(){ loadAd() observer.observe( document.body, { childList:true, subtree:true } ) }) `, ], styles: [ `.__dumi-default-navbar-logo{ background-size: 140px!important; background-position: center left!important; background-repeat: no-repeat!important; padding-left: 150px!important;/*可根据title的宽度调整*/ font-size: 22px!important; color: #000!important; font-weight: lighter!important; } .__dumi-default-navbar{ padding: 0 28px !important; } .__dumi-default-layout-hero{ background-image: url(//img.alicdn.com/imgextra/i4/O1CN01ZcvS4e26XMsdsCkf9_!!6000000007671-2-tps-6001-4001.png); background-size: cover; background-repeat: no-repeat; padding: 120px 0 !important; } .__dumi-default-layout-hero h1{ color:#45124e !important; font-size:80px !important; padding-bottom: 30px !important; } .__dumi-default-dark-switch { display:none } nav a{ text-decoration: none !important; } #carbonads * { margin: initial; padding: initial; } #carbonads { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Helvetica, Arial, sans-serif; } #carbonads { display: flex; max-width: 330px; background-color: hsl(0, 0%, 98%); box-shadow: 0 1px 4px 1px hsla(0, 0%, 0%, 0.1); z-index: 100; float:right; } #carbonads a { color: inherit; text-decoration: none; } #carbonads a:hover { color: inherit; } #carbonads span { position: relative; display: block; overflow: hidden; } #carbonads .carbon-wrap { display: flex; } #carbonads .carbon-img { display: block; margin: 0; line-height: 1; } #carbonads .carbon-img img { display: block; } #carbonads .carbon-text { font-size: 13px; padding: 10px; margin-bottom: 16px; line-height: 1.5; text-align: left; } #carbonads .carbon-poweredby { display: block; padding: 6px 8px; background: #f1f1f2; text-align: center; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; font-size: 8px; line-height: 1; border-top-left-radius: 3px; position: absolute; bottom: 0; right: 0; } `, ], } ================================================ FILE: packages/reactive-react/LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2015-present, Alibaba Group Holding Limited. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/reactive-react/README.md ================================================ # @formily/reactive-react ## QuikStart ```tsx import React from 'react' import { observable } from '@formily/reactive' import { observer } from '@formily/reactive-react' const obs = observable({ count: 0, }) export default observer(() => { return (
{obs.count}
) }) ``` ================================================ FILE: packages/reactive-react/package.json ================================================ { "name": "@formily/reactive-react", "version": "2.3.7", "license": "MIT", "main": "lib", "module": "esm", "umd:main": "dist/formily.reactive-react.umd.production.js", "unpkg": "dist/formily.reactive-react.umd.production.js", "jsdelivr": "dist/formily.reactive-react.umd.production.js", "jsnext:main": "esm", "repository": { "type": "git", "url": "git+https://github.com/alibaba/formily.git" }, "types": "esm/index.d.ts", "bugs": { "url": "https://github.com/alibaba/formily/issues" }, "homepage": "https://github.com/alibaba/formily#readme", "engines": { "npm": ">=3.0.0" }, "scripts": { "start": "dumi dev", "build": "rimraf -rf lib esm dist && npm run build:cjs && npm run build:esm && npm run build:umd", "build:cjs": "tsc --project tsconfig.build.json", "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm", "build:umd": "rollup --config", "build:docs": "dumi build" }, "peerDependencies": { "@types/react": ">=16.8.0", "@types/react-dom": ">=16.8.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", "react-is": ">=16.8.0" }, "peerDependenciesMeta": { "@types/react": { "optional": true }, "@types/react-dom": { "optional": true } }, "devDependencies": { "dumi": "^1.1.0-rc.8" }, "dependencies": { "@formily/reactive": "2.3.7", "hoist-non-react-statics": "^3.3.2" }, "publishConfig": { "access": "public" }, "gitHead": "ac79c196ae9324889aca5e0501146f9b37b04283" } ================================================ FILE: packages/reactive-react/rollup.config.js ================================================ import baseConfig from '../../scripts/rollup.base.js' export default baseConfig('formily.reactive-react', 'Formily.ReactiveReact') ================================================ FILE: packages/reactive-react/src/hooks/index.ts ================================================ import { useForceUpdate } from './useForceUpdate' import { useCompatEffect } from './useCompatEffect' import { useCompatFactory } from './useCompatFactory' import { useDidUpdate } from './useDidUpdate' import { useLayoutEffect } from './useLayoutEffect' import { useObserver } from './useObserver' export const unstable_useForceUpdate = useForceUpdate export const unstable_useCompatEffect = useCompatEffect export const unstable_useCompatFactory = useCompatFactory export const unstable_useDidUpdate = useDidUpdate export const unstable_useLayoutEffect = useLayoutEffect export const unstable_useObserver = useObserver ================================================ FILE: packages/reactive-react/src/hooks/useCompatEffect.ts ================================================ import { useEffect, useRef, EffectCallback, DependencyList } from 'react' import { immediate } from '../shared' const isArr = Array.isArray const isEqualDeps = (target: any, source: any) => { const arrA = isArr(target) const arrB = isArr(source) if (arrA !== arrB) return false if (arrA) { if (target.length !== source.length) return false return target.every((val, index) => val === source[index]) } return target === source } export const useCompatEffect = ( effect: EffectCallback, deps?: DependencyList ) => { const depsRef = useRef(null) const mountedRef = useRef(false) useEffect(() => { mountedRef.current = true const dispose = effect() return () => { mountedRef.current = false if (!isEqualDeps(depsRef.current, deps)) { if (dispose) dispose() return } immediate(() => { if (mountedRef.current) return if (dispose) dispose() }) } }, deps) depsRef.current = deps } ================================================ FILE: packages/reactive-react/src/hooks/useCompatFactory.ts ================================================ import React from 'react' import { GarbageCollector } from '../shared' import { useCompatEffect } from './useCompatEffect' class ObjectToBeRetainedByReact {} function objectToBeRetainedByReactFactory() { return new ObjectToBeRetainedByReact() } export const useCompatFactory = void }>( factory: () => T ): T => { const instRef = React.useRef(null) const gcRef = React.useRef() const [objectRetainedByReact] = React.useState( objectToBeRetainedByReactFactory ) if (!instRef.current) { instRef.current = factory() } //StrictMode/ConcurrentMode会导致组件无法正确触发UnMount,所以只能自己做垃圾回收 if (!gcRef.current) { gcRef.current = new GarbageCollector(() => { if (instRef.current) { instRef.current.dispose() } }) gcRef.current.open(objectRetainedByReact) } useCompatEffect(() => { gcRef.current.close() return () => { if (instRef.current) { instRef.current.dispose() instRef.current = null } } }, []) return instRef.current } ================================================ FILE: packages/reactive-react/src/hooks/useDidUpdate.ts ================================================ import { useRef } from 'react' import { useLayoutEffect } from './useLayoutEffect' import { immediate } from '../shared' export const useDidUpdate = (callback?: () => void) => { const request = useRef(null) request.current = immediate(callback) useLayoutEffect(() => { request.current() callback() }) } ================================================ FILE: packages/reactive-react/src/hooks/useForceUpdate.ts ================================================ import { useCallback, useRef, useState } from 'react' import { useLayoutEffect } from './useLayoutEffect' import { useDidUpdate } from './useDidUpdate' const EMPTY_ARRAY: any[] = [] const RENDER_COUNT = { value: 0 } const RENDER_QUEUE = new Set<() => void>() export function useForceUpdate() { const [, setState] = useState([]) const firstRenderedRef = useRef(false) const needUpdateRef = useRef(false) useLayoutEffect(() => { firstRenderedRef.current = true if (needUpdateRef.current) { setState([]) needUpdateRef.current = false } return () => { firstRenderedRef.current = false } }, EMPTY_ARRAY) const update = useCallback(() => { setState([]) }, EMPTY_ARRAY) const scheduler = useCallback(() => { if (!firstRenderedRef.current) { // 针对StrictMode无法快速回收内存,只能考虑拦截第一次渲染函数的setState, // 因为第一次渲染函数的setState会触发第二次渲染函数执行,从而清理掉第二次渲染函数内部的依赖 needUpdateRef.current = true return } if (RENDER_COUNT.value === 0) { update() } else { RENDER_QUEUE.add(update) } }, EMPTY_ARRAY) RENDER_COUNT.value++ useDidUpdate(() => { if (RENDER_COUNT.value > 0) { RENDER_COUNT.value-- } if (RENDER_COUNT.value === 0) { RENDER_QUEUE.forEach((update) => { RENDER_QUEUE.delete(update) update() }) } }) return scheduler } ================================================ FILE: packages/reactive-react/src/hooks/useLayoutEffect.ts ================================================ import { useEffect, useLayoutEffect as _useLayoutEffect } from 'react' export const useLayoutEffect = typeof document !== 'undefined' ? _useLayoutEffect : useEffect ================================================ FILE: packages/reactive-react/src/hooks/useObserver.ts ================================================ import { Tracker } from '@formily/reactive' import { IObserverOptions } from '../types' import { useForceUpdate } from './useForceUpdate' import { useCompatFactory } from './useCompatFactory' export const useObserver = any>( view: T, options?: IObserverOptions ): ReturnType => { const forceUpdate = useForceUpdate() const tracker = useCompatFactory( () => new Tracker(() => { if (typeof options?.scheduler === 'function') { options.scheduler(forceUpdate) } else { forceUpdate() } }, options?.displayName) ) return tracker.track(view) } ================================================ FILE: packages/reactive-react/src/index.ts ================================================ export * from './observer' export * from './hooks' export * from './types' ================================================ FILE: packages/reactive-react/src/observer.ts ================================================ import React, { forwardRef, memo, Fragment } from 'react' import hoistNonReactStatics from 'hoist-non-react-statics' import { useObserver } from './hooks/useObserver' import { IObserverOptions, IObserverProps, ReactFC } from './types' export function observer< P, Options extends IObserverOptions = IObserverOptions >( component: ReactFC

, options?: Options ): React.MemoExoticComponent< ReactFC< Options extends { forwardRef: true } ? P & { ref?: 'ref' extends keyof P ? P['ref'] : React.RefAttributes } : React.PropsWithoutRef

> > { const realOptions = { forwardRef: false, ...options, } const wrappedComponent = realOptions.forwardRef ? forwardRef((props: any, ref: any) => { return useObserver(() => component({ ...props, ref }), realOptions) }) : (props: any) => { return useObserver(() => component(props), realOptions) } const memoComponent = memo(wrappedComponent) hoistNonReactStatics(memoComponent, component) if (realOptions.displayName) { memoComponent.displayName = realOptions.displayName } return memoComponent } export const Observer = observer((props: IObserverProps) => { const children = typeof props.children === 'function' ? props.children() : props.children return React.createElement(Fragment, {}, children) }) ================================================ FILE: packages/reactive-react/src/shared/gc.ts ================================================ import { globalThisPolyfill } from './global' const registry: FinalizationRegistry = globalThisPolyfill['FinalizationRegistry'] && new globalThisPolyfill['FinalizationRegistry']((token: any) => token?.clean?.() ) type Token = { clean: () => void } export class GarbageCollector { private expireTime: number private request?: ReturnType; private token: Token constructor(clean?: () => void, expireTime = 10_000) { this.token = { clean, } this.expireTime = expireTime } open(target: T) { if (registry) { registry.register(target, this.token, this.token) } else { this.request = setTimeout(() => { this.token?.clean?.() }, this.expireTime) } } close() { if (registry) { registry.unregister(this.token) } else { clearTimeout(this.request) } } } ================================================ FILE: packages/reactive-react/src/shared/global.ts ================================================ /* istanbul ignore next */ function globalSelf() { try { if (typeof self !== 'undefined') { return self } } catch (e) {} try { if (typeof window !== 'undefined') { return window } } catch (e) {} try { if (typeof global !== 'undefined') { return global } } catch (e) {} return Function('return this')() } export const globalThisPolyfill: Window = globalSelf() ================================================ FILE: packages/reactive-react/src/shared/immediate.ts ================================================ export const immediate = (callback?: () => void) => { let disposed = false Promise.resolve(0).then(() => { if (disposed) { disposed = false return } callback() }) return () => { disposed = true } } ================================================ FILE: packages/reactive-react/src/shared/index.ts ================================================ export * from './gc' export * from './immediate' ================================================ FILE: packages/reactive-react/src/types.ts ================================================ import React from 'react' export interface IObserverOptions { forwardRef?: boolean scheduler?: (updater: () => void) => void displayName?: string } export interface IObserverProps { children?: (() => React.ReactElement) | React.ReactNode } export type Modify = Omit & R export type ReactPropsWithChildren

= Modify< { children?: React.ReactNode | undefined }, P > export type ReactFC

= React.FC> ================================================ FILE: packages/reactive-react/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./lib", "paths": { "@formily/*": ["packages/*", "devtools/*"] }, "declaration": true } } ================================================ FILE: packages/reactive-react/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["./src/**/*.ts", "./src/**/*.tsx"], "exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"], "compilerOptions": { "lib": ["ESNext", "DOM"] } } ================================================ FILE: packages/reactive-test-cases-for-react18/.npmignore ================================================ node_modules *.log build docs doc-site __tests__ .eslintrc jest.config.js tsconfig.json .umi src ================================================ FILE: packages/reactive-test-cases-for-react18/.umirc.js ================================================ import { resolve } from 'path' export default { mode: 'site', logo: '//img.alicdn.com/imgextra/i2/O1CN01Kq3OHU1fph6LGqjIz_!!6000000004056-55-tps-1141-150.svg', title: 'Formily', hash: true, favicon: '//img.alicdn.com/imgextra/i3/O1CN01XtT3Tv1Wd1b5hNVKy_!!6000000002810-55-tps-360-360.svg', outputPath: './doc-site', navs: { 'en-US': [ { title: 'Guide', path: '/guide', }, { title: 'API', path: '/api', }, { title: 'Home Site', path: 'https://formilyjs.org', }, { title: 'GITHUB', path: 'https://github.com/alibaba/formily', }, ], 'zh-CN': [ { title: '指南', path: '/zh-CN/guide', }, { title: 'API', path: '/zh-CN/api', }, { title: '主站', path: 'https://formilyjs.org', }, { title: 'GITHUB', path: 'https://github.com/alibaba/formily', }, ], }, links: [ { rel: 'stylesheet', href: 'https://esm.sh/antd@4.x/dist/antd.css', }, ], styles: [ `.__dumi-default-navbar-logo{ height: 60px !important; width: 150px !important; padding-left:0 !important; color: transparent !important; } .__dumi-default-navbar{ padding: 0 28px !important; } .__dumi-default-layout-hero{ background-image: url(//img.alicdn.com/imgextra/i4/O1CN01ZcvS4e26XMsdsCkf9_!!6000000007671-2-tps-6001-4001.png); background-size: cover; background-repeat: no-repeat; } nav a{ text-decoration: none !important; } `, ], menus: { '/guide': [ { title: 'Introduction', path: '/guide', }, { title: 'Architecture', path: '/guide/architecture' }, { title: 'Concept', path: '/guide/concept' }, ], '/zh-CN/guide': [ { title: '介绍', path: '/guide', }, { title: '核心架构', path: '/zh-CN/guide/architecture' }, { title: '核心概念', path: '/zh-CN/guide/concept' }, ], }, } ================================================ FILE: packages/reactive-test-cases-for-react18/LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2015-present, Alibaba Group Holding Limited. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/reactive-test-cases-for-react18/README.md ================================================ # @formily/reactive-test-cases-for-react18 ================================================ FILE: packages/reactive-test-cases-for-react18/package.json ================================================ { "name": "@formily/reactive-test-cases-for-react18", "version": "2.3.7", "license": "MIT", "private": true, "repository": { "type": "git", "url": "git+https://github.com/alibaba/formily.git" }, "types": "esm/index.d.ts", "bugs": { "url": "https://github.com/alibaba/formily/issues" }, "homepage": "https://github.com/alibaba/formily#readme", "engines": { "npm": ">=3.0.0" }, "scripts": { "start": "webpack-dev-server --config webpack.dev.ts" }, "devDependencies": { "file-loader": "^5.0.2", "html-webpack-plugin": "^3.2.0", "mini-css-extract-plugin": "^1.6.0", "raw-loader": "^4.0.0", "style-loader": "^1.1.3", "ts-loader": "^7.0.4", "webpack": "^4.41.5", "webpack-bundle-analyzer": "^3.9.0", "webpack-cli": "^3.3.10", "webpack-dev-server": "^3.10.1" }, "resolutions": { "react": "next", "react-dom": "next", "react-is": "next" }, "dependencies": { "@formily/reactive": "2.3.7", "@formily/reactive-react": "2.3.7", "react": "next", "react-dom": "next", "react-is": "next" }, "gitHead": "ac79c196ae9324889aca5e0501146f9b37b04283" } ================================================ FILE: packages/reactive-test-cases-for-react18/src/MySlowList.js ================================================ import React from 'react' import { observer } from '@formily/reactive-react' // Note: this file is exactly the same in both examples. function ListItem({ children }) { let now = performance.now() while (performance.now() - now < 10) { // Note: this is an INTENTIONALLY EMPTY loop that // DOES NOTHING for 3 milliseconds for EACH ITEM. // // It's meant to emulate what happens in a deep // component tree with calculations and other // work performed inside components that can't // trivially be optimized or removed. } return

{children}
} export default observer(function MySlowList({ text }) { let items = [] for (let i = 0; i < 50; i++) { items.push( {'Result ' + i + ' for ' + text.text} ) } return ( <>

Results for "{text.text}":

    {items}
) }) ================================================ FILE: packages/reactive-test-cases-for-react18/src/index.js ================================================ import React, { startTransition, useState } from 'react' import ReactDOM from 'react-dom' import MySlowList from './MySlowList' import { observable } from '@formily/reactive' import { observer } from '@formily/reactive-react' const App = observer(function App() { const [text, setText] = useState('hello') const [slowText] = useState(() => observable({ text, }) ) function handleChange(e) { setText(e.target.value) startTransition(() => { slowText.text = e.target.value }) } return (

Concurrent React

You entered: {text}

But we are showing:

But we are showing:

But we are showing:

But we are showing:

Even though{' '} each list item in this demo artificially blocks the main thread for 3 milliseconds , the app is able to stay responsive.


) }) let Indicator = observer(({ text }) => { const [hover, setHover] = useState(false) return (

setHover(true)} onMouseLeave={() => setHover(false)} style={{ border: '1px solid black', padding: 20, background: hover ? 'yellow' : '', }} > But we are showing: {text.text}

) }) const rootElement = document.getElementById('root') ReactDOM.createRoot(rootElement).render() ================================================ FILE: packages/reactive-test-cases-for-react18/template.ejs ================================================ React18 Demo
================================================ FILE: packages/reactive-test-cases-for-react18/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./lib", "paths": { "@formily/*": ["packages/*", "devtools/*"] }, "declaration": true } } ================================================ FILE: packages/reactive-test-cases-for-react18/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "allowJs": true }, "include": ["./src/**/*.js"], "exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"] } ================================================ FILE: packages/reactive-test-cases-for-react18/webpack.base.ts ================================================ import path from 'path' import fs from 'fs-extra' import { GlobSync } from 'glob' import MiniCssExtractPlugin from 'mini-css-extract-plugin' //import { getThemeVariables } from 'antd/dist/theme' const getWorkspaceAlias = () => { const basePath = path.resolve(__dirname, '../../') const pkg = fs.readJSONSync(path.resolve(basePath, 'package.json')) || {} const results = {} const workspaces = pkg.workspaces if (Array.isArray(workspaces)) { workspaces.forEach((pattern) => { const { found } = new GlobSync(pattern, { cwd: basePath }) found.forEach((name) => { const pkg = fs.readJSONSync( path.resolve(basePath, name, './package.json') ) results[pkg.name] = path.resolve(basePath, name, './src') }) }) } return results } export default { mode: 'development', devtool: 'inline-source-map', // 嵌入到源文件中 stats: { entrypoints: false, children: false, }, entry: { index: path.resolve(__dirname, './src/index'), }, output: { path: path.resolve(__dirname, '../build'), filename: '[name].[hash].bundle.js', }, resolve: { modules: ['node_modules'], extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], alias: getWorkspaceAlias(), }, externals: { react: 'React', 'react-dom': 'ReactDOM', moment: 'moment', antd: 'antd', }, module: { rules: [ { test: /\.(tsx?|jsx?)$/, use: [ { loader: require.resolve('ts-loader'), options: { transpileOnly: true, }, }, ], }, { test: /\.css$/, use: [MiniCssExtractPlugin.loader, require.resolve('css-loader')], }, { test: /\.less$/, use: [ MiniCssExtractPlugin.loader, { loader: 'css-loader' }, { loader: 'postcss-loader', }, { loader: 'less-loader', options: { // modifyVars: getThemeVariables({ // dark: true // 开启暗黑模式 // }), javascriptEnabled: true, }, }, ], }, { test: /\.(woff|woff2|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, use: ['url-loader'], }, { test: /\.html?$/, loader: require.resolve('file-loader'), options: { name: '[name].[ext]', }, }, ], }, } ================================================ FILE: packages/reactive-test-cases-for-react18/webpack.dev.ts ================================================ import baseConfig from './webpack.base' import HtmlWebpackPlugin from 'html-webpack-plugin' import MiniCssExtractPlugin from 'mini-css-extract-plugin' import webpack from 'webpack' import path from 'path' const PORT = 3000 const createPages = (pages) => { return pages.map(({ filename, template, chunk }) => { return new HtmlWebpackPlugin({ filename, template, inject: 'body', chunks: chunk, }) }) } for (const key in baseConfig.entry) { if (Array.isArray(baseConfig.entry[key])) { baseConfig.entry[key].push( require.resolve('webpack/hot/dev-server'), `${require.resolve('webpack-dev-server/client')}?http://localhost:${PORT}` ) } } export default { ...baseConfig, plugins: [ new MiniCssExtractPlugin({ filename: '[name].[hash].css', chunkFilename: '[id].[hash].css', }), ...createPages([ { filename: 'index.html', template: path.resolve(__dirname, './template.ejs'), chunk: ['index'], }, ]), new webpack.HotModuleReplacementPlugin(), // new BundleAnalyzerPlugin() ], devServer: { host: '127.0.0.1', open: true, port: PORT, }, } ================================================ FILE: packages/reactive-test-cases-for-react18/webpack.prod.ts ================================================ import baseConfig from './webpack.base' import HtmlWebpackPlugin from 'html-webpack-plugin' import MiniCssExtractPlugin from 'mini-css-extract-plugin' import path from 'path' const createPages = (pages) => { return pages.map(({ filename, template, chunk }) => { return new HtmlWebpackPlugin({ filename, template, inject: 'body', chunks: chunk, }) }) } export default { ...baseConfig, mode: 'production', plugins: [ new MiniCssExtractPlugin({ filename: '[name].[hash].css', chunkFilename: '[id].[hash].css', }), ...createPages([ { filename: 'index.html', template: path.resolve(__dirname, './template.ejs'), chunk: ['index'], }, ]), ], optimization: { minimize: true, }, } ================================================ FILE: packages/reactive-vue/.npmignore ================================================ node_modules *.log build docs doc-site __tests__ .eslintrc jest.config.js tsconfig.json .umi src ================================================ FILE: packages/reactive-vue/LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2015-present, Alibaba Group Holding Limited. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/reactive-vue/README.md ================================================ # @formily/reactive-vue ================================================ FILE: packages/reactive-vue/package.json ================================================ { "name": "@formily/reactive-vue", "version": "2.3.7", "license": "MIT", "main": "lib", "module": "esm", "umd:main": "dist/formily.reactive-vue.umd.production.js", "unpkg": "dist/formily.reactive-vue.umd.production.js", "jsdelivr": "dist/formily.reactive-vue.umd.production.js", "jsnext:main": "esm", "repository": { "type": "git", "url": "git+https://github.com/alibaba/formily.git" }, "types": "esm/index.d.ts", "bugs": { "url": "https://github.com/alibaba/formily/issues" }, "homepage": "https://github.com/alibaba/formily#readme", "engines": { "npm": ">=3.0.0" }, "scripts": { "build": "rimraf -rf lib esm dist && npm run build:cjs && npm run build:esm && npm run build:umd", "build:cjs": "tsc --project tsconfig.build.json", "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm", "build:umd": "rollup --config" }, "devDependencies": { "@vue/composition-api": "^1.0.0-rc.7", "@vue/test-utils": "1.0.0-beta.22", "core-js": "^2.4.0", "vue": "^2.6.12" }, "dependencies": { "@formily/reactive": "2.3.7", "vue-demi": ">=0.13.6" }, "peerDependencies": { "@vue/composition-api": "^1.0.0-beta.1", "vue": "^2.6.0 || >=3.0.0-rc.0" }, "peerDependenciesMeta": { "@vue/composition-api": { "optional": true } }, "publishConfig": { "access": "public" }, "gitHead": "ac79c196ae9324889aca5e0501146f9b37b04283" } ================================================ FILE: packages/reactive-vue/rollup.config.js ================================================ import baseConfig from '../../scripts/rollup.base.js' export default baseConfig('formily.reactive-vue', 'Formily.ReactiveVue') ================================================ FILE: packages/reactive-vue/src/__tests__/observer.spec.ts ================================================ import { shallowMount, createLocalVue } from '@vue/test-utils' import { observable, autorun } from '@formily/reactive' import { CreateElement } from 'vue' import CompositionAPI, { defineComponent, h } from '@vue/composition-api' import { observer } from '../' import collectData from '../observer/collectData' import { observer as observerInVue2 } from '../observer/observerInVue2' import expect from 'expect' test('observer: component', async () => { const model = observable({ age: 10, setAge() { model.age++ }, }) const Component = observer({ data() { return { model, } }, render(this: any, h: CreateElement) { return h('button', { on: { click: this.model.setAge }, domProps: { textContent: this.model.age }, }) }, }) const wrapper = shallowMount(Component) expect(wrapper.find('button').text()).toBe('10') wrapper.find('button').trigger('click') expect(wrapper.find('button').text()).toBe('11') wrapper.destroy() }) test('observer: component with setup', async () => { const Vue = createLocalVue() Vue.use(CompositionAPI) const model = observable({ age: 30, get sub10() { return model.age - 10 }, get sub20() { return model.sub10 - 10 }, setAge() { model.age++ }, }) const Component = observer( defineComponent({ setup() { return () => { return h('button', { on: { click: model.setAge }, domProps: { textContent: model.sub20 }, }) } }, // to fix 'Maximum call stack size exceeded' error of @vue/test-utils render() { return null }, }) ) const wrapper = shallowMount(Component) expect(wrapper.find('button').text()).toBe('10') wrapper.find('button').trigger('click') expect(wrapper.find('button').text()).toBe('11') model.age++ expect(wrapper.find('button').text()).toBe('12') wrapper.destroy() }) test('observer: component scheduler', async () => { let schedulerRequest = null const model = observable({ age: 10, setAge() { model.age++ }, }) const Component = observer( { data() { return { model, } }, render(this: any, h: CreateElement) { return h('button', { on: { click: this.model.setAge }, domProps: { textContent: this.model.age }, }) }, }, { scheduler: (update) => { clearTimeout(schedulerRequest) schedulerRequest = setTimeout(() => { update() }, 100) }, } ) const wrapper = shallowMount(Component) expect(wrapper.find('button').text()).toBe('10') wrapper.find('button').trigger('click') await new Promise((r) => setTimeout(r, 150)) expect(wrapper.find('button').text()).toBe('11') // test second render wrapper.find('button').trigger('click') await new Promise((r) => setTimeout(r, 150)) expect(wrapper.find('button').text()).toBe('12') wrapper.destroy() }) test('observer: stop tracking if watcher is destroyed', async () => { let count = 0 const model = observable({ age: 10, name: 'test', }) const Component = observer({ name: 'test', data() { return { model: model, } }, render() { count++ return h('div', [this.model.name, this.model.age]) }, }) const wrapper = shallowMount(Component) const childInst = wrapper.find({ name: 'test' }) expect(childInst.exists()).toBe(true) ;(childInst.vm as any)._isDestroyed = true model.age++ wrapper.destroy() expect(count).toEqual(1) // 不触发 reactiveRender }) test('collectData', async () => { const model = observable({ age: 10, name: 'test', }) const target = { value: 1, } const data = collectData( {}, { model, target, } ) const fn1 = jest.fn() const fn2 = jest.fn() autorun(() => fn1(model.age)) autorun(() => fn2(data.target.value)) model.age++ expect(fn1).toBeCalledTimes(2) target.value++ expect(fn2).toBeCalledTimes(1) }) test('observerInVue2', () => { const componentObj = Object.create(null) componentObj.data = () => { return {} } const ExtendedComponent1 = observerInVue2(componentObj) expect(ExtendedComponent1.name).toEqual('') function Component() {} Component.options = { data: () => { return {} }, } const ExtendedComponent2 = observerInVue2(Component, { name: 'abc' }) expect(ExtendedComponent2.name).toEqual('abc') }) ================================================ FILE: packages/reactive-vue/src/hooks/index.ts ================================================ export * from './useObserver' ================================================ FILE: packages/reactive-vue/src/hooks/useObserver.ts ================================================ import { Tracker } from '@formily/reactive' import { getCurrentInstance, onBeforeUnmount, isVue3 } from 'vue-demi' import { IObserverOptions } from '../types' /* istanbul ignore next */ export const useObserver = (options?: IObserverOptions) => { if (isVue3) { const vm = getCurrentInstance() let tracker: Tracker = null const disposeTracker = () => { if (tracker) { tracker.dispose() tracker = null } } const vmUpdate = () => { vm?.proxy?.$forceUpdate() } onBeforeUnmount(disposeTracker) Object.defineProperty(vm, 'effect', { get() { // https://github.com/alibaba/formily/issues/2655 return vm['_updateEffect'] || {} }, set(newValue) { vm['_updateEffectRun'] = newValue.run disposeTracker() const newTracker = () => { tracker = new Tracker(() => { if (options?.scheduler && typeof options.scheduler === 'function') { options.scheduler(vmUpdate) } else { vmUpdate() } }) } const update = function () { let refn = null tracker?.track(() => { refn = vm['_updateEffectRun'].call(newValue) }) return refn } newTracker() newValue.run = update vm['_updateEffect'] = newValue }, }) } } ================================================ FILE: packages/reactive-vue/src/index.ts ================================================ export * from './observer' export * from './hooks' export * from './types' ================================================ FILE: packages/reactive-vue/src/observer/collectData.ts ================================================ // https://github.com/mobxjs/mobx-vue/blob/master/src/collectData.ts /** * @author Kuitos * @homepage https://github.com/kuitos/ * @since 2018-06-08 10:16 */ import { isObservable } from '@formily/reactive' export default function collectData(vm: any, data?: any) { const dataDefinition = typeof data === 'function' ? data.call(vm, vm) : data || {} const filteredData = Object.keys(dataDefinition).reduce( (result: any, field) => { const value = dataDefinition[field] if (isObservable(value)) { Object.defineProperty(vm, field, { configurable: true, get() { return value }, }) } else { result[field] = value } return result }, {} ) return filteredData } ================================================ FILE: packages/reactive-vue/src/observer/index.ts ================================================ import { isVue2 } from 'vue-demi' import { observer as observerV2 } from './observerInVue2' import { observer as observerV3 } from './observerInVue3' import collectData from './collectData' import { IObserverOptions } from '../types' export function observer(baseComponent: C, options?: IObserverOptions): C { /* istanbul ignore else */ if (isVue2) { return observerV2(baseComponent, options) } else { return observerV3(baseComponent, options) } } export { collectData } ================================================ FILE: packages/reactive-vue/src/observer/observerInVue2.ts ================================================ // https://github.com/mobxjs/mobx-vue/blob/master/src/observer.ts /** * @author Kuitos * @homepage https://github.com/kuitos/ * @since 2018-05-22 16:39 */ import { Tracker, batch } from '@formily/reactive' import collectDataForVue from './collectData' import { Vue2 as Vue } from 'vue-demi' import { IObserverOptions } from '../types' const noop = () => {} const disposerSymbol = Symbol('disposerSymbol') function observer(Component: any, observerOptions?: IObserverOptions): any { const name = observerOptions?.name || (Component as any).name || (Component as any)._componentTag || (Component.constructor && Component.constructor.name) || '' const originalOptions = typeof Component === 'object' ? Component : (Component as any).options // To not mutate the original component options, we need to construct a new one const dataDefinition = originalOptions.data const options = { name, ...originalOptions, data(vm: any) { return collectDataForVue(vm || this, dataDefinition) }, // overrider the cached constructor to avoid extending skip // @see https://github.com/vuejs/vue/blob/6cc070063bd211229dff5108c99f7d11b6778550/src/core/global-api/extend.js#L24 _Ctor: {}, } // we couldn't use the Component as super class when Component was a VueClass, that will invoke the lifecycle twice after we called Component.extend const superProto = typeof Component === 'function' && Object.getPrototypeOf(Component.prototype) const Super = superProto instanceof (Vue as any) ? superProto.constructor : Vue const ExtendedComponent = Super.extend(options) const { $mount, $destroy } = ExtendedComponent.prototype ExtendedComponent.prototype.$mount = function (this: any, ...args: any[]) { let mounted = false this[disposerSymbol] = noop let nativeRenderOfVue: any const reactiveRender = () => { batch(() => { tracker.track(() => { if (!mounted) { $mount.apply(this, args) mounted = true nativeRenderOfVue = this._watcher.getter // rewrite the native render method of vue with our reactive tracker render // thus if component updated by vue watcher, we could re track and collect dependencies by @formily/reactive this._watcher.getter = reactiveRender } else { nativeRenderOfVue.call(this, this) } }) }) return this } reactiveRender.$vm = this const tracker = new Tracker(() => { if ( reactiveRender.$vm._isBeingDestroyed || reactiveRender.$vm._isDestroyed ) { return tracker.dispose() } if ( observerOptions?.scheduler && typeof observerOptions.scheduler === 'function' ) { observerOptions.scheduler(reactiveRender) } else { reactiveRender() } }) this[disposerSymbol] = tracker.dispose return reactiveRender() } ExtendedComponent.prototype.$destroy = function (this: any) { ;(this as any)[disposerSymbol]() $destroy.apply(this) } const extendedComponentNamePropertyDescriptor = Object.getOwnPropertyDescriptor(ExtendedComponent, 'name') || {} if (extendedComponentNamePropertyDescriptor.configurable === true) { Object.defineProperty(ExtendedComponent, 'name', { writable: false, value: name, enumerable: false, configurable: false, }) } return ExtendedComponent } export { observer, observer as Observer } ================================================ FILE: packages/reactive-vue/src/observer/observerInVue3.ts ================================================ import { IObserverOptions } from '../types' import { useObserver } from '../hooks/useObserver' /* istanbul ignore next */ export const observer = function (opts: any, options?: IObserverOptions): any { const name = options?.name || opts.name || 'ObservableComponent' return { name, ...opts, setup(props: Record, context: any) { useObserver(options) return opts?.setup?.(props, context) }, } } ================================================ FILE: packages/reactive-vue/src/types.ts ================================================ export interface IObserverOptions { name?: string scheduler?: (updater: () => void) => void } ================================================ FILE: packages/reactive-vue/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./lib", "paths": { "@formily/*": ["packages/*", "devtools/*"] }, "declaration": true } } ================================================ FILE: packages/reactive-vue/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "skipLibCheck": true }, "include": ["./src/**/*.ts", "./src/**/*.tsx"], "exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"] } ================================================ FILE: packages/shared/.npmignore ================================================ node_modules *.log build docs doc-site __tests__ .eslintrc jest.config.js tsconfig.json .umi src ================================================ FILE: packages/shared/LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2015-present, Alibaba Group Holding Limited. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/shared/README.md ================================================ # @formily/shared > Formily 工具函数集 ================================================ FILE: packages/shared/package.json ================================================ { "name": "@formily/shared", "version": "2.3.7", "license": "MIT", "main": "lib", "module": "esm", "umd:main": "dist/formily.shared.umd.production.js", "unpkg": "dist/formily.shared.umd.production.js", "jsdelivr": "dist/formily.shared.umd.production.js", "jsnext:main": "esm", "types": "esm/index.d.ts", "repository": { "type": "git", "url": "git+https://github.com/alibaba/formily.git" }, "bugs": { "url": "https://github.com/alibaba/formily/issues" }, "homepage": "https://github.com/alibaba/formily#readme", "engines": { "npm": ">=3.0.0" }, "publishConfig": { "access": "public" }, "gitHead": "ac79c196ae9324889aca5e0501146f9b37b04283", "scripts": { "build": "rimraf -rf lib esm dist && npm run build:cjs && npm run build:esm && npm run build:umd", "build:cjs": "tsc --project tsconfig.build.json", "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm", "build:umd": "rollup --config" }, "dependencies": { "@formily/path": "2.3.7", "camel-case": "^4.1.1", "lower-case": "^2.0.1", "no-case": "^3.0.4", "param-case": "^3.0.4", "pascal-case": "^3.1.1", "upper-case": "^2.0.1" } } ================================================ FILE: packages/shared/rollup.config.js ================================================ import baseConfig from '../../scripts/rollup.base.js' export default baseConfig('formily.shared', 'Formily.Shared') ================================================ FILE: packages/shared/src/__tests__/index.spec.ts ================================================ import moment from 'moment' import { Map as ImmutableMap } from 'immutable' import { isEqual } from '../compare' import { toArr, every, move, some, findIndex, find, includes, map, reduce, } from '../array' import { clone, shallowClone } from '../clone' import { lowerCase } from '../case' import { deprecate } from '../deprecate' import { globalThisPolyfill } from '../global' import { isValid, isEmpty } from '../isEmpty' import { stringLength } from '../string' import { Subscribable } from '../subscribable' import { lazyMerge, merge } from '../merge' import { instOf } from '../instanceof' import { isFn, isHTMLElement, isNumberLike, isReactElement, isMap, isWeakMap, isWeakSet, isSet, } from '../checkers' import { defaults } from '../defaults' import { applyMiddleware } from '../middleware' const sleep = (d = 100) => new Promise((resolve) => setTimeout(resolve, d)) describe('array', () => { test('toArr', () => { expect(isEqual(toArr([123]), [123])).toBeTruthy() expect(isEqual(toArr(123), [123])).toBeTruthy() expect(isEqual(toArr(null), [])).toBeTruthy() }) test('some', () => { const values1 = [1, 2, 3, 4, 5] const values2 = [] const values3 = { a: 1, b: 2, c: 3 } const values4 = {} expect(some(values1, (item) => item === 3)).toBeTruthy() expect(some(values1, (item) => item === 6)).toBeFalsy() expect(some(values2, () => true)).toBeFalsy() expect(some(values2, () => false)).toBeFalsy() expect(some(values3, (item) => item === 3)).toBeTruthy() expect(some(values3, (item) => item === 6)).toBeFalsy() expect(some(values4, () => true)).toBeFalsy() expect(some(values4, () => false)).toBeFalsy() }) test('every', () => { const values1 = [1, 2, 3, 4, 5] const values2 = [] const values3 = { a: 1, b: 2, c: 3 } const values4 = {} expect(every(values1, (item) => item < 6)).toBeTruthy() expect(every(values1, (item) => item < 3)).toBeFalsy() expect(every(values2, () => true)).toBeTruthy() expect(every(values2, () => false)).toBeTruthy() expect(every(values2, () => false)).toBeTruthy() expect(every(values3, (item) => item < 6)).toBeTruthy() expect(every(values3, (item) => item < 3)).toBeFalsy() expect(every(values4, () => false)).toBeTruthy() expect(every(values4, () => false)).toBeTruthy() }) test('findIndex', () => { const value = [1, 2, 3, 4, 5] expect( isEqual( findIndex(value, (item) => item > 3), 3 ) ).toBeTruthy() expect( isEqual( findIndex(value, (item) => item < 3, true), 1 ) ).toBeTruthy() expect( isEqual( findIndex(value, (item) => item > 6), -1 ) ).toBeTruthy() }) test('find', () => { const value = [1, 2, 3, 4, 5] expect( isEqual( find(value, (item) => item > 3), 4 ) ).toBeTruthy() expect( isEqual( find(value, (item) => item < 3, true), 2 ) ).toBeTruthy() expect( isEqual( find(value, (item) => item > 6), void 0 ) ).toBeTruthy() }) test('includes', () => { const value = [1, 2, 3, 4, 5] expect(includes(value, 3)).toBeTruthy() expect(includes(value, 6)).toBeFalsy() expect(includes('some test string', 'test')).toBeTruthy() expect(includes('some test string', 'test2')).toBeFalsy() }) test('map', () => { const value = [1, 2, 3, 4, 5] const stringVal = 'some test string' const obj = { k1: 'v1', k2: 'v2' } expect( isEqual( map(value, (item) => item + 1, true), [6, 5, 4, 3, 2] ) ).toBeTruthy() expect( isEqual( map(stringVal, (item) => item), stringVal.split('') ) ).toBeTruthy() expect( isEqual( map(obj, (item) => `${item}-copy`), { k1: 'v1-copy', k2: 'v2-copy' } ) ).toBeTruthy() }) test('reduce', () => { const value = [1, 2, 3, 4, 5] expect( isEqual( reduce(value, (acc, item) => acc + item, 0, true), 15 ) ).toBeTruthy() }) }) describe('case', () => { test('lowercase', () => { expect(lowerCase('SOME_UPPER_CASE_TEXT')).toEqual('some_upper_case_text') expect(lowerCase('')).toEqual('') }) }) describe('compare', () => { // base expect(isEqual('some test string', 'some test string')).toBeTruthy() // array expect( isEqual([{ k1: 'v1' }, { k2: 'v2' }], [{ k1: 'v1' }, { k2: 'v2' }]) ).toBeTruthy() expect(isEqual([{ k1: 'v1' }, { k2: 'v2' }], [{ k1: 'v1' }])).toBeFalsy() // moment const momentA = moment('2019-11-11', 'YYYY-MM-DD') const momentB = moment('2019-11-10', 'YYYY-MM-DD') expect(isEqual(momentA, {})).toBeFalsy() expect(isEqual(momentA, moment('2019-11-11', 'YYYY-MM-DD'))).toBeTruthy() expect(isEqual(momentA, momentB)).toBeFalsy() // immutable const immutableA = ImmutableMap({ key: 'val' }) const immutableB = ImmutableMap({ key1: 'val1' }) expect(isEqual(immutableA, {})).toBeFalsy() expect(isEqual(immutableA, immutableB)).toBeFalsy() // schema // todo // date const dateA = new Date('2019-11-11') const dateB = new Date('2019-11-10') expect(isEqual(dateA, {})).toBeFalsy() expect(isEqual(dateA, dateB)).toBeFalsy() expect(isEqual(dateA, new Date('2019-11-11'))).toBeTruthy() // regexp const regexpA = new RegExp(/test/) const regexpB = new RegExp(/test2/) expect(isEqual(regexpA, {})).toBeFalsy() expect(isEqual(regexpA, new RegExp(/test/))).toBeTruthy() expect(isEqual(regexpA, regexpB)).toBeFalsy() // URL const urlA = new URL('https://formilyjs.org/') const urlB = new URL('https://www.taobao.com') const urlC = new URL('https://formilyjs.org/') expect(isEqual(urlA, urlC)).toBeTruthy() expect(isEqual(urlA, urlB)).toBeFalsy() // object const objA = { key: 'val' } const objB = { key2: 'val2', key3: 'val3' } const objC = { key2: 'val2' } expect(isEqual(objA, { key: 'val' })).toBeTruthy() expect(isEqual(objA, objB)).toBeFalsy() expect(isEqual(objA, objC)).toBeFalsy() expect(isEqual([11, 22], [33, 44])).toBeFalsy() expect(isEqual([11, 22], {})).toBeFalsy() expect(isEqual(new URL('https://aa.test'), {})).toBeFalsy() expect(instOf(new URL('https://aa.test'), 'URL')).toBeTruthy() expect(instOf(new Date(), 'Date')).toBeTruthy() expect( isEqual(new URL('https://aa.test'), new URL('https://aa.test')) ).toBeTruthy() expect( isEqual( { $$typeof: true, _owner: true, aaa: 123, }, { $$typeof: true, _owner: true, aaa: 123, } ) ).toBeTruthy() expect( isEqual( { $$typeof: true, _owner: true, aaa: 123, }, { $$typeof: true, _owner: true, bbb: 123, } ) ).toBeFalsy() expect( isEqual( { $$typeof: true, _owner: true, aaa: 123, }, { $$typeof: true, _owner: true, aaa: 333, } ) ).toBeFalsy() }) describe('clone and compare', () => { test('clone form data', () => { let dd = new Map() dd.set('aaa', { bb: 123 }) let ee = new WeakMap() ee.set({}, 1) let ff = new WeakSet() ff.add({}) let gg = new Set() gg.add(3) let a = { aa: 123123, bb: [{ bb: 111 }, { bb: 222 }], cc: () => { // eslint-disable-next-line no-console console.log('123') }, dd, ee, ff, gg, } let cloned = clone(a) expect(isEqual(cloned, a)).toBeTruthy() expect(a === cloned).toBeFalsy() expect(a.bb[0] === cloned.bb[0]).toBeFalsy() expect(a.dd === cloned.dd).toBeTruthy() expect(a.dd.get('aaa') === cloned.dd.get('aaa')).toBeTruthy() expect(a.cc === cloned.cc).toBeTruthy() expect(a.ee === cloned.ee).toBeTruthy() expect(a.ff === cloned.ff).toBeTruthy() expect(a.gg === cloned.gg).toBeTruthy() expect( clone({ aa: { _isAMomentObject: true, }, bb: { _isJSONSchemaObject: true, }, cc: { $$typeof: true, _owner: true, }, dd: { _isBigNumber: true, }, }) ).toEqual({ aa: { _isAMomentObject: true, }, bb: { _isJSONSchemaObject: true, }, cc: { $$typeof: true, _owner: true, }, dd: { _isBigNumber: true, }, }) expect( clone({ toJS() { return 123 }, }) ).toEqual(123) expect( clone({ toJSON() { return 123 }, }) ).toEqual(123) }) test('native clone', () => { const map = new Map() map.set('key', 123) expect(clone(map) === map).toBeTruthy() const weakMap = new WeakMap() const key = {} weakMap.set(key, 123) expect(clone(weakMap) === weakMap).toBeTruthy() const weakSet = new WeakSet() const key2 = {} weakMap.set(key2, 123) expect(clone(weakSet) === weakSet).toBeTruthy() const set = new Set() expect(clone(set) === set).toBeTruthy() const date = new Date() expect(clone(date) === date).toBeTruthy() // @ts-ignore const file = new File([''], 'filename') expect(clone(file) === file).toBeTruthy() const url = new URL('https://test.com') expect(clone(url) === url).toBeTruthy() const regexp = /\d+/ expect(clone(regexp) === regexp).toBeTruthy() const promise = Promise.resolve(1) expect(clone(promise) === promise).toBeTruthy() }) test('shallowClone', () => { expect(shallowClone({ aa: 123 })).toEqual({ aa: 123 }) expect(shallowClone([123])).toEqual([123]) expect(shallowClone(/\d+/)).toEqual(/\d+/) expect(shallowClone({ _isAMomentObject: true })).toEqual({ _isAMomentObject: true, }) expect( shallowClone({ _isBigNumber: true, }) ).toEqual({ _isBigNumber: true, }) expect( shallowClone({ _isJSONSchemaObject: true, }) ).toEqual({ _isJSONSchemaObject: true, }) expect( shallowClone({ $$typeof: true, _owner: true, }) ).toEqual({ $$typeof: true, _owner: true, }) expect( shallowClone({ toJS() { return 123 }, }).toJS() ).toEqual(123) expect( shallowClone({ toJSON() { return 123 }, }).toJSON() ).toEqual(123) expect(shallowClone(1)).toEqual(1) }) }) describe('deprecate', () => { test('deprecate', () => { const test = jest.fn(() => { console.info('### deprecated function called ###') }) const deprecatedFn = jest.fn( deprecate(test, 'Some.Deprecated.Api', 'some deprecated error') ) // arguments - function deprecatedFn() expect(deprecatedFn).toHaveBeenCalledTimes(1) expect(test).toHaveBeenCalledTimes(1) // arguments - string const testDeprecatedFn = jest.fn(() => deprecate('Some.Deprecated.Api', 'some deprecated error') ) testDeprecatedFn() expect(testDeprecatedFn).toHaveBeenCalledTimes(1) // arguments - empty string const testDeprecatedFn2 = jest.fn(() => deprecate('Some.Deprecated.Api')) testDeprecatedFn2() expect(testDeprecatedFn2).toHaveBeenCalledTimes(1) }) }) describe('isEmpty', () => { test('isValid', () => { // val - undefined expect(isValid(undefined)).toBeFalsy() // val - any expect(isValid(!undefined)).toBeTruthy() }) test('isEmpty', () => { // val - null expect(isEmpty(null)).toBeTruthy() // val - boolean expect(isEmpty(true)).toBeFalsy() // val - number expect(isEmpty(2422)).toBeFalsy() // val - string expect(isEmpty('some text')).toBeFalsy() expect(isEmpty('')).toBeTruthy() // val - function const emptyFunc = function () {} const nonEmptyFunc = function (payload) { console.info(payload) } expect(isEmpty(emptyFunc)).toBeTruthy() expect(isEmpty(nonEmptyFunc)).toBeFalsy() // val - arrays expect(isEmpty([])).toBeTruthy() expect(isEmpty([0])).toBeTruthy() expect(isEmpty([''])).toBeTruthy() expect(isEmpty([''], true)).toBeFalsy() expect(isEmpty([0], true)).toBeFalsy() expect(isEmpty([1, 2, 3, 4, 5])).toBeFalsy() expect(isEmpty([0, undefined, null, ''])).toBeTruthy() // val - errors expect(isEmpty(new Error())).toBeTruthy() expect(isEmpty(new Error('some error'))).toBeFalsy() // val - objects // @ts-ignore const file = new File(['foo'], 'filename.txt', { type: 'text/plain' }) // The toString and Object.prototype.toString of the File in the Jest environment are inconsistent file.toString = Object.prototype.toString expect(isEmpty(file)).toBeFalsy() expect(isEmpty(new Map())).toBeTruthy() expect(isEmpty(new Map().set('key', 'val'))).toBeFalsy() expect(isEmpty(new Set())).toBeTruthy() expect(isEmpty(new Set([1, 2]))).toBeFalsy() expect(isEmpty({ key: 'val' })).toBeFalsy() expect(isEmpty({})).toBeTruthy() expect(isEmpty(Symbol())).toBeFalsy() }) }) describe('string', () => { test('stringLength', () => { expect(stringLength('🦄some text')).toEqual(10) }) }) describe('shared Subscribable', () => { test('Subscribable', () => { const cb = jest.fn((payload) => payload) // defualt subscribable const obj = new Subscribable() const handlerIdx = obj.subscribe(cb) expect(handlerIdx).toEqual(1) obj.notify({ key: 'val' }) expect(cb).toHaveBeenCalledTimes(1) expect(cb).toBeCalledWith({ key: 'val' }) obj.unsubscribe(handlerIdx) obj.notify({ key: 'val' }) expect(cb).toHaveBeenCalledTimes(1) // subscribable with custom filter const objWithCustomFilter = new Subscribable() const customFilter = (payload) => { payload.key2 = 'val2' return payload } objWithCustomFilter.subscription = { filter: customFilter, } objWithCustomFilter.subscribe(cb) const handlerIdx2 = objWithCustomFilter.subscribe(cb) expect(handlerIdx2).toEqual(2) objWithCustomFilter.notify({ key4: 'val4' }) expect(cb).toHaveBeenCalledTimes(3) expect(cb).toBeCalledWith({ key4: 'val4', key2: 'val2' }) // subscribable with custom notify const objWithCustomNotify = new Subscribable() const customNotify = jest.fn((payload) => { console.info(payload) return false }) objWithCustomNotify.subscription = { notify: customNotify, } objWithCustomNotify.subscribe(cb) objWithCustomNotify.notify({ key3: 'val3' }) expect(customNotify).toBeCalledTimes(1) objWithCustomNotify.unsubscribe() }) }) describe('types', () => { test('isFn', () => { const normalFunction = function normalFn() {} const asyncFunction = async function asyncFn() {} const generatorFunction = function* generatorFn() {} expect(isFn(() => {})).toBeTruthy() expect(isFn(normalFunction)).toBeTruthy() expect(isFn(asyncFunction)).toBeTruthy() expect(isFn(generatorFunction)).toBeTruthy() expect(isFn('')).toBeFalsy() expect(isFn(undefined)).toBeFalsy() expect(isFn(['🦄'])).toBeFalsy() }) test('isNumberLike', () => { expect(isNumberLike(123)).toBeTruthy() expect(isNumberLike('123')).toBeTruthy() expect(isNumberLike('aa')).toBeFalsy() }) test('isReactElement', () => { expect(isReactElement({ $$typeof: true, _owner: true })).toBeTruthy() }) test('isHTMLElement', () => { // @ts-ignore expect(isHTMLElement(document.createElement('div'))).toBeTruthy() }) test('isMap', () => { expect(isMap(new Map())).toBeTruthy() }) test('isSet', () => { expect(isSet(new Set())).toBeTruthy() }) test('isWeakMap', () => { expect(isWeakMap(new WeakMap())).toBeTruthy() expect(isWeakMap(new Map())).toBeFalsy() }) test('isWeakSet', () => { expect(isWeakSet(new WeakSet())).toBeTruthy() expect(isWeakSet(new Set())).toBeFalsy() }) }) describe('merge', () => { test('assign', () => { const target = { aa: { bb: { cc: { dd: 123, }, }, }, } const source = { aa: { bb: { cc: { ee: '1234', }, }, }, } expect( merge(target, source, { assign: true, }) ).toEqual({ aa: { bb: { cc: { dd: 123, ee: '1234', }, }, }, }) expect(target).toEqual({ aa: { bb: { cc: { dd: 123, ee: '1234', }, }, }, }) expect( merge( { react: { $$typeof: true, _owner: true, aa: 123, }, }, { react: { $$typeof: true, _owner: true, bb: 321, }, }, { assign: true, } ) ).toEqual({ react: { $$typeof: true, _owner: true, bb: 321, }, }) expect( merge( { react: { _isAMomentObject: true, aa: 123, }, }, { react: { _isAMomentObject: true, bb: 321, }, }, { assign: true, } ) ).toEqual({ react: { _isAMomentObject: true, bb: 321, }, }) expect( merge( { react: { _isJSONSchemaObject: true, aa: 123, }, }, { react: { _isJSONSchemaObject: true, bb: 321, }, }, { assign: true, } ) ).toEqual({ react: { _isJSONSchemaObject: true, bb: 321, }, }) expect( merge( { react: { _isBigNumber: true, c: [1, 234567890123], e: 0, s: 1, }, }, { react: { _isBigNumber: true, c: [2, 345678901234], e: 1, s: 0, }, }, { assign: true, } ) ).toEqual({ react: { _isBigNumber: true, c: [2, 345678901234], e: 1, s: 0, }, }) const toJSObj = { toJS: () => {}, bb: 321, } expect( merge( { toJSObj: { toJS: () => {}, aa: 123, }, }, { toJSObj, }, { assign: true, } ) ).toEqual({ toJSObj, }) const toJSONObj = { toJSON: () => {}, bb: 321, } expect( merge( { toJSONObj: { toJS: () => {}, aa: 123, }, }, { toJSONObj, }, { assign: true, } ) ).toEqual({ toJSONObj, }) }) test('empty', () => { expect( merge( { aa: undefined, }, { aa: {}, } ) ).toEqual({ aa: {} }) }) test('clone', () => { const target = { aa: { bb: { cc: { dd: 123, }, }, }, } const source = { aa: { bb: { cc: { ee: '1234', }, }, }, } expect(merge(target, source)).toEqual({ aa: { bb: { cc: { dd: 123, ee: '1234', }, }, }, }) expect(target).toEqual({ aa: { bb: { cc: { dd: 123, }, }, }, }) }) test('merge array', () => { expect(merge([11, 22], [333])).toEqual([11, 22, 333]) }) test('merge custom', () => { expect( merge( { aa: { cc: 123 } }, { aa: { bb: 321 } }, { customMerge() { return (a, b) => ({ ...a, ...b }) }, } ) ).toEqual({ aa: { cc: 123, bb: 321 } }) }) test('merge symbols', () => { const symbol = Symbol('xxx') expect(merge({ [symbol]: 123 }, { aa: 321 })).toEqual({ [symbol]: 123, aa: 321, }) const getOwnPropertySymbols = Object.getOwnPropertySymbols Object.getOwnPropertySymbols = null const mergedObject = merge({ [symbol]: 123 }, { aa: 321 }) Object.getOwnPropertySymbols = getOwnPropertySymbols expect(mergedObject).toEqual({ aa: 321, }) }) test('merge unmatch', () => { expect(merge({ aa: 123 }, [111])).toEqual([111]) }) test('lazy merge', () => { const merge1 = lazyMerge(1, 2) expect(merge1).toBe(2) const merge2 = lazyMerge('123', '321') expect(merge2).toBe('321') const merge3 = lazyMerge(1, undefined) expect(merge3).toBe(1) const merge4 = lazyMerge('123', undefined) expect(merge4).toBe('123') const merge5 = lazyMerge(undefined, '123') expect(merge5).toBe('123') const merge6 = lazyMerge([1, 2, 3], [3, 4]) expect(merge6[0]).toBe(3) expect(merge6[1]).toBe(4) expect(merge6[2]).toBe(3) const merge7 = lazyMerge( { get x() { return 'x' }, }, { get y() { return 'y' }, } ) expect(merge7.x).toBe('x') expect(merge7.y).toBe('y') const effects = { a: 1, b: 2, } const merge8 = lazyMerge( { get x() { return effects.a }, }, { get y() { return effects.b }, } ) expect(merge8.x).toBe(1) expect(merge8.y).toBe(2) effects.a = 123 effects.b = 321 expect(merge8.x).toBe(123) expect(merge8.y).toBe(321) expect(Object.keys(merge8)).toEqual(['x', 'y']) expect('x' in merge8).toBe(true) expect('y' in merge8).toBe(true) expect('z' in merge8).toBe(false) const merge9Source = { a: 1 } const merge9Target = { b: 2 } const merge9 = lazyMerge(merge9Target, merge9Source) merge9.a = 2 merge9.b = 3 merge9.c = 4 expect(merge9Source).toEqual({ a: 2, c: 4 }) expect(merge9Target).toEqual({ b: 3 }) }) }) describe('globalThis', () => { expect(globalThisPolyfill.requestAnimationFrame).not.toBeUndefined() }) describe('instanceof', () => { test('instOf', () => { expect(instOf(123, 123)).toBeFalsy() expect(instOf('123', '123')).toBeFalsy() }) }) test('defaults', () => { const toJSON = () => {} const toJS = () => {} expect( defaults( { aa: { _isAMomentObject: true, }, bb: { _isJSONSchemaObject: true, }, cc: { $$typeof: true, _owner: true, }, dd: { toJSON, }, ee: { toJS, }, ff: { _isBigNumber: true, toJSON, }, }, { aa: { value: 111 }, bb: { value: 222 }, cc: { value: 333 }, dd: { value: 444 }, ee: { value: 555 }, mm: { value: 123 }, ff: { value: { c: [1, 234567890123], e: 0, s: 1, }, }, } ) ).toEqual({ aa: { value: 111 }, bb: { value: 222 }, cc: { value: 333 }, dd: { value: 444 }, ee: { value: 555 }, mm: { value: 123 }, ff: { value: { c: [1, 234567890123], e: 0, s: 1, }, }, }) expect(defaults([1, 2, 3], [0, undefined])).toEqual([0, 2, 3]) const defaultDate = new RegExp('') // @ts-ignore defaultDate._name = 'name' const date2 = new RegExp('') expect(defaults(defaultDate, date2)._name).toEqual('name') }) test('applyMiddleware', async () => { expect(await applyMiddleware(0)).toEqual(0) expect( await applyMiddleware(0, [ (num: number, next) => next(num + 1), (num: number, next) => next(num + 1), (num: number, next) => next(num + 1), ]) ).toEqual(3) expect( await applyMiddleware(0, [ (num: number, next) => next(), (num: number, next) => next(num + 1), (num: number, next) => next(num + 1), ]) ).toEqual(2) const resolved = jest.fn() applyMiddleware(0, [ (num: number, next) => next(num + 1), () => '123', (num: number, next) => next(num + 1), ]).then(resolved) await sleep(16) expect(resolved).toBeCalledTimes(0) }) test('applyMiddleware with error', async () => { try { await applyMiddleware(0, [ () => { throw 'this is error' }, ]) } catch (e) { expect(e).toEqual('this is error') } }) test('move', () => { const array1 = [1] move(array1, 1, 0) expect(array1).toEqual([1]) move(array1, 0, 1) expect(array1).toEqual([1]) move(array1, -1, 1) expect(array1).toEqual([1]) move(array1, 0, 3) expect(array1).toEqual([1]) const array2 = [0, 1, 2] move(array2, 0, 2) expect(array2).toEqual([1, 2, 0]) move(array2, 1, 1) expect(array2).toEqual([1, 2, 0]) const array3 = [0, 1, 2, 3] move(array3, 3, 1) expect(array3).toEqual([0, 3, 1, 2]) }) ================================================ FILE: packages/shared/src/array.ts ================================================ import { isArr, isObj, isStr } from './checkers' type EachArrayIterator = (currentValue: T, key: number) => void | boolean type EachStringIterator = (currentValue: string, key: number) => void | boolean type EachObjectIterator = ( currentValue: T, key: string ) => void | boolean type MapArrayIterator = ( currentValue: TItem, key: number ) => TResult type MapStringIterator = (currentValue: string, key: number) => TResult type MapObjectIterator = ( currentValue: TItem, key: string ) => TResult type MemoArrayIterator = ( previousValue: U, currentValue: T, key: number ) => U type MemoStringIterator = ( previousValue: T, currentValue: string, key: number ) => T type MemoObjectIterator = ( previousValue: TResult, currentValue: TValue, key: string ) => TResult export const toArr = (val: any): any[] => (isArr(val) ? val : val ? [val] : []) export function each( val: string, iterator: EachStringIterator, revert?: boolean ): void export function each( val: T[], iterator: EachArrayIterator, revert?: boolean ): void export function each( val: T, iterator: EachObjectIterator, revert?: boolean ): void export function each(val: any, iterator: any, revert?: boolean): void { if (isArr(val) || isStr(val)) { if (revert) { for (let i: number = val.length - 1; i >= 0; i--) { if (iterator(val[i], i) === false) { return } } } else { for (let i = 0; i < val.length; i++) { if (iterator(val[i], i) === false) { return } } } } else if (isObj(val)) { let key: string for (key in val) { if (Object.hasOwnProperty.call(val, key)) { if (iterator(val[key], key) === false) { return } } } } } export function map( val: string, iterator: MapStringIterator, revert?: boolean ): T[] export function map( val: TItem[], iterator: MapArrayIterator, revert?: boolean ): TResult[] export function map( val: T, iterator: MapObjectIterator, revert?: boolean ): Record export function map(val: any, iterator: any, revert?: any): any { const res = isArr(val) || isStr(val) ? [] : {} each( val, (item, key) => { const value = iterator(item, key) if (isArr(res)) { ;(res as any).push(value) } else { res[key] = value } }, revert ) return res } export function reduce( val: T[], iterator: MemoArrayIterator, accumulator?: U, revert?: boolean ): U export function reduce( val: string, iterator: MemoStringIterator, accumulator?: T, revert?: boolean ): T export function reduce( val: T, iterator: MemoObjectIterator, accumulator?: TResult, revert?: boolean ): TResult export function reduce( val: any, iterator: any, accumulator?: any, revert?: boolean ): any { let result = accumulator each( val, (item, key) => { result = iterator(result, item, key) }, revert ) return result } export function every( val: T, iterator: EachStringIterator, revert?: boolean ): boolean export function every( val: T[], iterator: EachArrayIterator, revert?: boolean ): boolean export function every( val: T, iterator: EachObjectIterator, revert?: boolean ): boolean export function every(val: any, iterator: any, revert?: boolean): boolean { let res = true each( val, (item, key) => { if (!iterator(item, key)) { res = false return false } }, revert ) return res } export function some( val: T, iterator: EachStringIterator, revert?: boolean ): boolean export function some( val: T[], iterator: EachArrayIterator, revert?: boolean ): boolean export function some( val: T, iterator: EachObjectIterator, revert?: boolean ): boolean export function some(val: any, iterator: any, revert?: boolean): boolean { let res = false each( val, (item, key) => { if (iterator(item, key)) { res = true return false } }, revert ) return res } export function findIndex( val: T, iterator: EachStringIterator, revert?: boolean ): number export function findIndex( val: T[], iterator: EachArrayIterator, revert?: boolean ): number export function findIndex( val: T, iterator: EachObjectIterator, revert?: boolean ): keyof T export function findIndex( val: any, iterator: any, revert?: boolean ): string | number { let res: number | string = -1 each( val, (item, key) => { if (iterator(item, key)) { res = key return false } }, revert ) return res } export function find( val: T, iterator: EachStringIterator, revert?: boolean ): any export function find( val: T[], iterator: EachArrayIterator, revert?: boolean ): T export function find( val: T, iterator: EachObjectIterator, revert?: boolean ): T[keyof T] export function find(val: any, iterator: any, revert?: boolean): any { let res: any each( val, (item, key) => { if (iterator(item, key)) { res = item return false } }, revert ) return res } export function includes( val: T, searchElement: string, revert?: boolean ): boolean export function includes( val: T[], searchElement: T, revert?: boolean ): boolean export function includes(val: any, searchElement: any, revert?: boolean) { if (isStr(val)) return val.includes(searchElement) return some(val, (item) => item === searchElement, revert) } export function move( array: T[], fromIndex: number, toIndex: number ) { if (fromIndex === toIndex) return array if ( toIndex < 0 || fromIndex < 0 || toIndex > array.length - 1 || fromIndex > array.length - 1 ) { return array } if (fromIndex < toIndex) { const fromItem = array[fromIndex] for (let index = fromIndex; index < toIndex; index++) { array[index] = array[index + 1] } array[toIndex] = fromItem } else { const fromItem = array[fromIndex] for (let index = fromIndex; index > toIndex; index--) { array[index] = array[index - 1] } array[toIndex] = fromItem } return array } ================================================ FILE: packages/shared/src/case.ts ================================================ import { camelCase } from 'camel-case' import { pascalCase } from 'pascal-case' import { lowerCase } from 'lower-case' import { upperCase } from 'upper-case' import { paramCase } from 'param-case' export { lowerCase, upperCase, camelCase, pascalCase, paramCase } ================================================ FILE: packages/shared/src/checkers.ts ================================================ const toString = Object.prototype.toString const isType = (type: string | string[]) => (obj: unknown): obj is T => getType(obj) === `[object ${type}]` export const getType = (obj: any) => toString.call(obj) export const isFn = (val: any): val is Function => typeof val === 'function' export const isArr = Array.isArray export const isPlainObj = isType('Object') export const isStr = isType('String') export const isBool = isType('Boolean') export const isNum = isType('Number') export const isMap = (val: any): val is Map => val && val instanceof Map export const isSet = (val: any): val is Set => val && val instanceof Set export const isWeakMap = (val: any): val is WeakMap => val && val instanceof WeakMap export const isWeakSet = (val: any): val is WeakSet => val && val instanceof WeakSet export const isNumberLike = (index: any): index is number => isNum(index) || /^\d+$/.test(index) export const isObj = (val: unknown): val is object => typeof val === 'object' export const isRegExp = isType('RegExp') export const isReactElement = (obj: any): boolean => obj && obj['$$typeof'] && obj['_owner'] export const isHTMLElement = (target: any): target is EventTarget => { return Object.prototype.toString.call(target).indexOf('HTML') > -1 } export type Subscriber = (payload: S) => void export interface Subscription { notify?: (payload: S) => void | boolean filter?: (payload: S) => any } ================================================ FILE: packages/shared/src/clone.ts ================================================ import { isFn, isPlainObj } from './checkers' export const shallowClone = (values: any) => { if (Array.isArray(values)) { return values.slice(0) } else if (isPlainObj(values)) { if ('$$typeof' in values && '_owner' in values) { return values } if (values['_isBigNumber']) { return values } if (values['_isAMomentObject']) { return values } if (values['_isJSONSchemaObject']) { return values } if (isFn(values['toJS'])) { return values } if (isFn(values['toJSON'])) { return values } return { ...values, } } else if (typeof values === 'object') { return new values.constructor(values) } return values } export const clone = (values: any) => { if (Array.isArray(values)) { const res = [] values.forEach((item) => { res.push(clone(item)) }) return res } else if (isPlainObj(values)) { if ('$$typeof' in values && '_owner' in values) { return values } if (values['_isBigNumber']) { return values } if (values['_isAMomentObject']) { return values } if (values['_isJSONSchemaObject']) { return values } if (isFn(values['toJS'])) { return values['toJS']() } if (isFn(values['toJSON'])) { return values['toJSON']() } const res = {} for (const key in values) { if (Object.hasOwnProperty.call(values, key)) { res[key] = clone(values[key]) } } return res } else { return values } } ================================================ FILE: packages/shared/src/compare.ts ================================================ import { isArr } from './checkers' import { instOf } from './instanceof' const isArray = isArr const keyList = Object.keys const hasProp = Object.prototype.hasOwnProperty /* eslint-disable */ function equal(a: any, b: any) { // fast-deep-equal index.js 2.0.1 if (a === b) { return true } if (a && b && typeof a === 'object' && typeof b === 'object') { const arrA = isArray(a) const arrB = isArray(b) let i: number let length: number let key: string | number if (arrA && arrB) { length = a.length if (length !== b.length) { return false } for (i = length; i-- !== 0; ) { if (!equal(a[i], b[i])) { return false } } return true } if (arrA !== arrB) { return false } const momentA = a && a._isAMomentObject const momentB = b && b._isAMomentObject if (momentA !== momentB) return false if (momentA && momentB) return a.isSame(b) const immutableA = a && a.toJS const immutableB = b && b.toJS if (immutableA !== immutableB) return false if (immutableA) return a.is ? a.is(b) : a === b const dateA = instOf(a, 'Date') const dateB = instOf(b, 'Date') if (dateA !== dateB) { return false } if (dateA && dateB) { return a.getTime() === b.getTime() } const regexpA = instOf(a, 'RegExp') const regexpB = instOf(b, 'RegExp') if (regexpA !== regexpB) { return false } if (regexpA && regexpB) { return a.toString() === b.toString() } const urlA = instOf(a, 'URL') const urlB = instOf(b, 'URL') if (urlA !== urlB) { return false } if (urlA && urlB) { return a.href === b.href } const schemaA = a && a.toJSON const schemaB = b && b.toJSON if (schemaA !== schemaB) return false if (schemaA && schemaB) return equal(a.toJSON(), b.toJSON()) const keys = keyList(a) length = keys.length if (length !== keyList(b).length) { return false } for (i = length; i-- !== 0; ) { if (!hasProp.call(b, keys[i])) { return false } } // end fast-deep-equal // Custom handling for React for (i = length; i-- !== 0; ) { key = keys[i] if (key === '_owner' && a.$$typeof) { // React-specific: avoid traversing React elements' _owner. // _owner contains circular references // and is not needed when comparing the actual elements (and not their owners) // .$$typeof and ._store on just reasonable markers of a react element continue } else { // all other properties should be traversed as usual if (!equal(a[key], b[key])) { return false } } } // fast-deep-equal index.js 2.0.1 return true } return a !== a && b !== b } // end fast-deep-equal export const isEqual = function exportedEqual(a: any, b: any) { try { return equal(a, b) } catch (error) { /* istanbul ignore next */ if ( (error.message && error.message.match(/stack|recursion/i)) || error.number === -2146828260 ) { // warn on circular references, don't crash // browsers give this different errors name and messages: // chrome/safari: "RangeError", "Maximum call stack size exceeded" // firefox: "InternalError", too much recursion" // edge: "Error", "Out of stack space" console.warn( 'Warning: react-fast-compare does not handle circular references.', error.name, error.message ) return false } // some other error. we should definitely know about these /* istanbul ignore next */ throw error } } ================================================ FILE: packages/shared/src/defaults.ts ================================================ import { each } from './array' import { isEmpty, isValid } from './isEmpty' import { getType, isArr, isPlainObj } from './checkers' const isUnNormalObject = (value: any) => { if (value?._owner && value?.$$typeof) { return true } if (value?._isAMomentObject || value?._isJSONSchemaObject) { return true } if (value?.toJS || value?.toJSON) { return true } } const isEnumerableObject = (val: any) => { if (isUnNormalObject(val)) { return false } return typeof val === 'object' } /** * * @param defaults * @param targets */ export const defaults = (defaults_: any, targets: any) => { if ( getType(defaults_) !== getType(targets) || !isEnumerableObject(defaults_) || !isEnumerableObject(targets) ) { return !isEmpty(targets) ? targets : defaults_ } else { const results = isArr(defaults_) ? [] : isPlainObj(defaults_) ? {} : defaults_ each(targets, (value, key) => { results[key] = defaults(defaults_[key], value) }) each(defaults_, (value, key) => { if (!isValid(results[key])) { results[key] = value } }) return results } } ================================================ FILE: packages/shared/src/deprecate.ts ================================================ import { isFn, isStr } from './checkers' const caches = {} export function deprecate( method: any, message?: string, help?: string ) { if (isFn(method)) { // eslint-disable-next-line @typescript-eslint/no-unused-vars return function (p1?: P1, p2?: P2, p3?: P3, p4?: P4, p5?: P5) { deprecate(message, help) return method.apply(this, arguments) } } if (isStr(method) && !caches[method]) { caches[method] = true console.warn( new Error( `${method} has been deprecated. Do not continue to use this api.${ message || '' }` ) ) } } ================================================ FILE: packages/shared/src/global.ts ================================================ /* istanbul ignore next */ function globalSelf() { try { if (typeof self !== 'undefined') { return self } } catch (e) {} try { if (typeof window !== 'undefined') { return window } } catch (e) {} try { if (typeof global !== 'undefined') { return global } } catch (e) {} return Function('return this')() } export const globalThisPolyfill: Window = globalSelf() ================================================ FILE: packages/shared/src/index.ts ================================================ export * from './array' export * from './compare' export * from './checkers' export * from './clone' export * from './isEmpty' export * from './case' export * from './string' export * from './global' export * from './path' export * from './deprecate' export * from './subscribable' export * from './middleware' export * from './merge' export * from './instanceof' export * from './defaults' export * from './uid' ================================================ FILE: packages/shared/src/instanceof.ts ================================================ import { globalThisPolyfill } from './global' import { isStr, isFn } from './checkers' export const instOf = (value: any, cls: any) => { if (isFn(cls)) return value instanceof cls if (isStr(cls)) { return globalThisPolyfill[cls] ? value instanceof globalThisPolyfill[cls] : false } return false } ================================================ FILE: packages/shared/src/isEmpty.ts ================================================ import { instOf } from './instanceof' const has = Object.prototype.hasOwnProperty const toString = Object.prototype.toString export const isUndef = (val: any) => val === undefined export const isValid = (val: any) => val !== undefined && val !== null export function isEmpty(val: any, strict = false): boolean { // Null and Undefined... if (val == null) { return true } // Booleans... if (typeof val === 'boolean') { return false } // Numbers... if (typeof val === 'number') { return false } // Strings... if (typeof val === 'string') { return val.length === 0 } // Functions... if (typeof val === 'function') { return val.length === 0 } // Arrays... if (Array.isArray(val)) { if (val.length === 0) { return true } for (let i = 0; i < val.length; i++) { if (strict) { if (val[i] !== undefined && val[i] !== null) { return false } } else { if ( val[i] !== undefined && val[i] !== null && val[i] !== '' && val[i] !== 0 ) { return false } } } return true } // Errors... if (instOf(val, 'Error')) { return val.message === '' } // Objects... if (val.toString === toString) { switch (val.toString()) { // Maps, Sets, Files and Errors... case '[object File]': case '[object Map]': case '[object Set]': { return val.size === 0 } // Plain objects... case '[object Object]': { for (const key in val) { if (has.call(val, key)) { return false } } return true } } } // Anything else... return false } ================================================ FILE: packages/shared/src/merge.ts ================================================ import { isFn, isPlainObj } from './checkers' import { isEmpty, isValid } from './isEmpty' function defaultIsMergeableObject(value: any) { return isNonNullObject(value) && !isSpecial(value) } function isNonNullObject(value: any) { // TODO: value !== null && typeof value === 'object' return Boolean(value) && typeof value === 'object' } function isSpecial(value: any) { // TODO: use isComplexObject() if ('$$typeof' in value && '_owner' in value) { return true } if (value._isAMomentObject) { return true } if (value._isJSONSchemaObject) { return true } if (isFn(value.toJS)) { return true } if (isFn(value.toJSON)) { return true } return !isPlainObj(value) } function emptyTarget(val: any) { return Array.isArray(val) ? [] : {} } // @ts-ignore function cloneUnlessOtherwiseSpecified(value: any, options: Options) { if (options.clone !== false && options.isMergeableObject?.(value)) { return deepmerge(emptyTarget(value), value, options) } return value } function defaultArrayMerge(target: any, source: any, options: Options) { return target.concat(source).map(function (element: any) { return cloneUnlessOtherwiseSpecified(element, options) }) } function getMergeFunction(key: string, options: Options) { if (!options.customMerge) { return deepmerge } const customMerge = options.customMerge(key) return typeof customMerge === 'function' ? customMerge : deepmerge } function getEnumerableOwnPropertySymbols(target: any): any { return Object.getOwnPropertySymbols ? Object.getOwnPropertySymbols(target).filter(function (symbol) { return target.propertyIsEnumerable(symbol) }) : [] } function getKeys(target: any) { if (!isValid(target)) return [] return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target)) } function propertyIsOnObject(object: any, property: any) { /* istanbul ignore next */ try { return property in object } catch (_) { return false } } // Protects from prototype poisoning and unexpected merging up the prototype chain. function propertyIsUnsafe(target: any, key: PropertyKey) { return ( propertyIsOnObject(target, key) && // Properties are safe to merge if they don't exist in the target yet, !( Object.hasOwnProperty.call(target, key) && // unsafe if they exist up the prototype chain, Object.propertyIsEnumerable.call(target, key) ) ) // and also unsafe if they're nonenumerable. } function mergeObject(target: any, source: any, options: Options) { const destination = options.assign ? target || {} : {} if (!options.isMergeableObject(target)) return target if (!options.assign) { getKeys(target).forEach(function (key) { destination[key] = cloneUnlessOtherwiseSpecified(target[key], options) }) } getKeys(source).forEach(function (key) { /* istanbul ignore next */ if (propertyIsUnsafe(target, key)) { return } if (isEmpty(target[key])) { destination[key] = source[key] } else if ( propertyIsOnObject(target, key) && // @ts-ignore options.isMergeableObject(source[key]) ) { destination[key] = getMergeFunction(key, options)( target[key], source[key], options ) } else { destination[key] = cloneUnlessOtherwiseSpecified(source[key], options) } }) return destination } interface Options { arrayMerge?(target: any[], source: any[], options?: Options): any[] clone?: boolean assign?: boolean customMerge?: ( key: string, options?: Options ) => ((x: any, y: any) => any) | undefined isMergeableObject?(value: object): boolean cloneUnlessOtherwiseSpecified?: (value: any, options: Options) => any } // @ts-ignore function deepmerge(target: any, source: any, options?: Options) { options = options || {} options.arrayMerge = options.arrayMerge || defaultArrayMerge options.isMergeableObject = options.isMergeableObject || defaultIsMergeableObject // cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge() // implementations can use it. The caller may not replace it. options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified const sourceIsArray = Array.isArray(source) const targetIsArray = Array.isArray(target) const sourceAndTargetTypesMatch = sourceIsArray === targetIsArray if (!sourceAndTargetTypesMatch) { return cloneUnlessOtherwiseSpecified(source, options) } else if (sourceIsArray) { return options.arrayMerge(target, source, options) } else { return mergeObject(target, source, options) } } export const lazyMerge = ( target: T, ...args: T[] ): any => { const _lazyMerge = ( target: T, source: T ): {} => { if (!isValid(source)) return target if (!isValid(target)) return source const isTargetObject = typeof target === 'object' const isSourceObject = typeof source === 'object' const isTargetFn = typeof target === 'function' const isSourceFn = typeof source === 'function' if (!isTargetObject && !isTargetFn) return source if (!isSourceObject && !isSourceFn) return target const getTarget = () => (isTargetFn ? target() : target) const getSource = () => (isSourceFn ? source() : source) const set = (_: object, key: PropertyKey, value: any) => { const source = getSource() const target = getTarget() if (key in source) { // @ts-ignore source[key] = value } else if (key in target) { // @ts-ignore target[key] = value } else { source[key] = value } return true } const get = (_: object, key: PropertyKey) => { const source = getSource() // @ts-ignore if (key in source) { return source[key] } // @ts-ignore return getTarget()[key] } const ownKeys = () => { const source = getSource() const target = getTarget() const keys = Object.keys(target) for (const key in source) { if (!(key in target)) { keys.push(key) } } return keys } const getOwnPropertyDescriptor = (_: object, key: PropertyKey) => ({ value: get(_, key), enumerable: true, configurable: true, }) const has = (_: object, key: PropertyKey) => { if (key in getSource() || key in getTarget()) return true return false } const getPrototypeOf = () => Object.getPrototypeOf({}) return new Proxy(Object.create(null), { set, get, ownKeys, getPrototypeOf, getOwnPropertyDescriptor, has, }) as any } return args.reduce<{}>((buf, arg) => _lazyMerge(buf, arg), target) } export const merge = deepmerge ================================================ FILE: packages/shared/src/middleware.ts ================================================ export interface IMiddleware { (payload: Payload, next: (payload?: Payload) => Result): Result } export const applyMiddleware = (payload: any, fns: IMiddleware[] = []) => { const compose = (payload: any, fns: IMiddleware[]): Promise => { const prevPayload = payload return Promise.resolve( fns[0](payload, (payload) => compose(payload ?? prevPayload, fns.slice(1)) ) ) } return new Promise((resolve, reject) => { compose( payload, fns.concat((payload) => { resolve(payload) }) ).catch(reject) }) } ================================================ FILE: packages/shared/src/path.ts ================================================ import { Path as FormPath, Pattern as FormPathPattern } from '@formily/path' export { FormPath, FormPathPattern } ================================================ FILE: packages/shared/src/string.ts ================================================ // ansiRegex const ansiRegex = () => { const pattern = [ '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)', '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))', ].join('|') return new RegExp(pattern, 'g') } // astralRegex const regex = '[\uD800-\uDBFF][\uDC00-\uDFFF]' const astralRegex = (opts?: { exact: boolean }) => opts && opts.exact ? new RegExp(`^${regex}$`) : new RegExp(regex, 'g') // stripAnsi const stripAnsi = (input: any) => typeof input === 'string' ? input.replace(ansiRegex(), '') : input export const stringLength = (input: string) => stripAnsi(input).replace(astralRegex(), ' ').length ================================================ FILE: packages/shared/src/subscribable.ts ================================================ import { isFn, Subscriber, Subscription } from './checkers' import { each } from './array' export class Subscribable { subscribers: { index?: number [key: number]: Subscriber } = { index: 0, } subscription: Subscription subscribe = (callback?: Subscriber): number => { if (isFn(callback)) { const index: number = this.subscribers.index + 1 this.subscribers[index] = callback this.subscribers.index++ return index } } unsubscribe = (index?: number) => { if (this.subscribers[index]) { delete this.subscribers[index] } else if (!index) { this.subscribers = { index: 0, } } } notify = (payload?: Payload, silent?: boolean) => { if (this.subscription) { if (this.subscription && isFn(this.subscription.notify)) { if (this.subscription.notify.call(this, payload) === false) { return } } } if (silent) return const filter = (payload: Payload) => { if (this.subscription && isFn(this.subscription.filter)) { return this.subscription.filter.call(this, payload) } return payload } each(this.subscribers, (callback: any) => { if (isFn(callback)) callback(filter(payload)) }) } } ================================================ FILE: packages/shared/src/uid.ts ================================================ let IDX = 36, HEX = '' while (IDX--) HEX += IDX.toString(36) export function uid(len?: number) { let str = '', num = len || 11 while (num--) str += HEX[(Math.random() * 36) | 0] return str } ================================================ FILE: packages/shared/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./lib", "paths": { "@formily/*": ["packages/*", "devtools/*"] }, "declaration": true } } ================================================ FILE: packages/shared/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["./src/**/*.ts", "./src/**/*.tsx"], "exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"], "compilerOptions": { "lib": ["ESNext", "DOM"] } } ================================================ FILE: packages/validator/.npmignore ================================================ node_modules *.log build docs doc-site __tests__ .eslintrc jest.config.js tsconfig.json .umi src ================================================ FILE: packages/validator/LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2015-present, Alibaba Group Holding Limited. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/validator/README.md ================================================ # @formily/validator > Formily 数据校验工具 ================================================ FILE: packages/validator/package.json ================================================ { "name": "@formily/validator", "version": "2.3.7", "license": "MIT", "main": "lib", "module": "esm", "umd:main": "dist/formily.validator.umd.production.js", "unpkg": "dist/formily.validator.umd.production.js", "jsdelivr": "dist/formily.validator.umd.production.js", "jsnext:main": "esm", "repository": { "type": "git", "url": "git+https://github.com/alibaba/formily.git" }, "types": "esm/index.d.ts", "bugs": { "url": "https://github.com/alibaba/formily/issues" }, "homepage": "https://github.com/alibaba/formily#readme", "engines": { "npm": ">=3.0.0" }, "scripts": { "build": "rimraf -rf lib esm dist lib && npm run build:cjs && npm run build:esm && npm run build:umd", "build:cjs": "tsc --project tsconfig.build.json", "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm", "build:umd": "rollup --config" }, "dependencies": { "@formily/shared": "2.3.7" }, "publishConfig": { "access": "public" }, "gitHead": "ac79c196ae9324889aca5e0501146f9b37b04283" } ================================================ FILE: packages/validator/rollup.config.js ================================================ import baseConfig from '../../scripts/rollup.base.js' export default baseConfig('formily.validator', 'Formily.Validator') ================================================ FILE: packages/validator/src/__tests__/parser.spec.ts ================================================ import { parseValidatorDescriptions } from '../parser' describe('parseValidatorDescriptions', () => { test('basic', () => { expect(parseValidatorDescriptions('date')).toEqual([{ format: 'date' }]) const validator = () => { return '' } expect(parseValidatorDescriptions(validator)).toEqual([{ validator }]) expect(parseValidatorDescriptions(['date'])).toEqual([{ format: 'date' }]) expect(parseValidatorDescriptions([validator])).toEqual([{ validator }]) expect(parseValidatorDescriptions({ format: 'date' })).toEqual([ { format: 'date' }, ]) expect(parseValidatorDescriptions({ validator })).toEqual([{ validator }]) }) }) ================================================ FILE: packages/validator/src/__tests__/registry.spec.ts ================================================ import { getValidateLocaleIOSCode, getLocaleByPath, getValidateLocale, setValidateLanguage, } from '../' import locale from '../locale' test('getValidateLocaleIOSCode', () => { expect(getValidateLocaleIOSCode('zh-CN')).toEqual('zh-CN') expect(getValidateLocaleIOSCode('zh')).toEqual('zh') expect(getValidateLocaleIOSCode('ZH')).toEqual('zh') expect(getValidateLocaleIOSCode('cn')).toEqual('zh-CN') expect(getValidateLocaleIOSCode('en')).toEqual('en') expect(getValidateLocaleIOSCode('TW')).toEqual('zh-TW') }) test('getLocaleByPath', () => { expect(getLocaleByPath('pattern', 'vi')).toEqual(locale.en.pattern) expect(getLocaleByPath('pattern')).toEqual(locale.en.pattern) }) test('getValidateLocale', () => { setValidateLanguage('vi') expect(getValidateLocale('pattern')).toEqual(locale.en.pattern) }) ================================================ FILE: packages/validator/src/__tests__/validator.spec.ts ================================================ import { validate, registerValidateRules, registerValidateFormats, setValidateLanguage, registerValidateMessageTemplateEngine, } from '../index' registerValidateRules({ custom: (value) => (value === '123' ? 'custom error' : ''), customBool: () => false, customBool2: () => true, }) registerValidateFormats({ custom: /^[\u4e00-\u9fa5]+$/, }) const hasError = (results: any, message?: string) => { if (!message) { return expect(results?.error?.[0]).not.toBeUndefined() } return expect(results?.error?.[0]).toEqual(message) } const noError = (results: any) => { return expect(results?.error?.[0]).toBeUndefined() } test('empty string validate', async () => { const results = await validate('', { required: true }) expect(results).toEqual({ error: ['The field value is required'], success: [], warning: [], }) }) test('empty array validate', async () => { const results = await validate([], { required: true }) expect(results).toEqual({ error: ['The field value is required'], success: [], warning: [], }) noError(await validate([''], { required: true })) }) test('empty object validate', async () => { const results = await validate({}, { required: true }) expect(results).toEqual({ error: ['The field value is required'], success: [], warning: [], }) }) test('empty number validate', async () => { const results = await validate(0, { required: true }) expect(results).toEqual({ error: [], success: [], warning: [], }) }) test('multi validate', async () => { const results = await validate('', { required: true, validator() { return 'validate error' }, }) expect(results).toEqual({ error: ['The field value is required', 'validate error'], success: [], warning: [], }) }) test('message scope', async () => { const results = await validate( '', { required: true, validator() { return 'validate error {{name}}' }, }, { context: { name: 'scopeName', }, } ) expect(results).toEqual({ error: ['The field value is required', 'validate error scopeName'], success: [], warning: [], }) }) test('first validate', async () => { const results = await validate( '', { required: true, validator() { return 'validate error' }, }, { validateFirst: true, } ) expect(results).toEqual({ error: ['The field value is required'], success: [], warning: [], }) }) test('custom validate results', async () => { const results = await validate('', { validator() { return { type: 'error', message: 'validate error' } }, }) expect(results).toEqual({ error: ['validate error'], success: [], warning: [], }) }) test('exception validate', async () => { const results1 = await validate('', { validator() { throw new Error('validate error') }, }) expect(results1).toEqual({ error: ['validate error'], success: [], warning: [], }) const results2 = await validate('', { validator() { throw 'custom string' }, }) expect(results2).toEqual({ error: ['custom string'], success: [], warning: [], }) }) test('max/maxItems/maxLength/minItems/minLength/min/maximum/exclusiveMaximum/minimum/exclusiveMinimum/len', async () => { hasError(await validate(6, { max: 5 })) hasError(await validate(6, { maxLength: 5 })) hasError(await validate(6, { maxItems: 5 })) noError(await validate(5, { max: 5 })) noError(await validate(5, { maxLength: 5 })) noError(await validate(5, { maxItems: 5 })) hasError(await validate([1, 2, 3, 4, 5, 6], { max: 5 })) hasError(await validate([1, 2, 3, 4, 5, 6], { maxLength: 5 })) hasError(await validate([1, 2, 3, 4, 5, 6], { maxItems: 5 })) noError(await validate([1, 2, 3, 4, 5], { max: 5 })) noError(await validate([1, 2, 3, 4, 5], { maxLength: 5 })) noError(await validate([1, 2, 3, 4, 5], { maxItems: 5 })) hasError(await validate('123456', { max: 5 })) hasError(await validate('123456', { maxLength: 5 })) hasError(await validate('123456', { maxItems: 5 })) noError(await validate('12345', { max: 5 })) noError(await validate('12345', { maxLength: 5 })) noError(await validate('12345', { maxItems: 5 })) hasError(await validate(2, { min: 3 })) hasError(await validate(2, { minLength: 3 })) hasError(await validate(2, { minItems: 3 })) noError(await validate(3, { min: 3 })) noError(await validate(3, { minLength: 3 })) noError(await validate(3, { minItems: 3 })) hasError(await validate([1, 2], { min: 3 })) hasError(await validate([1, 2], { minLength: 3 })) hasError(await validate([1, 2], { minItems: 3 })) noError(await validate([1, 2, 3], { min: 3 })) noError(await validate([1, 2, 3], { minLength: 3 })) noError(await validate([1, 2, 3], { minItems: 3 })) hasError(await validate('12', { min: 3 })) hasError(await validate('12', { minLength: 3 })) hasError(await validate('12', { minItems: 3 })) noError(await validate('123', { min: 3 })) noError(await validate('123', { minLength: 3 })) noError(await validate('123', { minItems: 3 })) hasError(await validate(6, { maximum: 5 })) noError(await validate(5, { maximum: 5 })) hasError(await validate([1, 2, 3, 4, 5, 6], { maximum: 5 })) noError(await validate([1, 2, 3, 4, 5], { maximum: 5 })) hasError(await validate('123456', { maximum: 5 })) noError(await validate('12345', { maximum: 5 })) hasError(await validate(2, { minimum: 3 })) noError(await validate(3, { minimum: 3 })) hasError(await validate([1, 2], { minimum: 3 })) noError(await validate([1, 2, 3], { minimum: 3 })) hasError(await validate('12', { minimum: 3 })) noError(await validate('123', { minimum: 3 })) hasError(await validate(6, { exclusiveMaximum: 5 })) hasError(await validate(5, { exclusiveMaximum: 5 })) hasError(await validate([1, 2, 3, 4, 5, 6], { exclusiveMaximum: 5 })) hasError(await validate([1, 2, 3, 4, 5], { exclusiveMaximum: 5 })) hasError(await validate('123456', { exclusiveMaximum: 5 })) hasError(await validate('12345', { exclusiveMaximum: 5 })) hasError(await validate(2, { exclusiveMinimum: 3 })) hasError(await validate(3, { exclusiveMinimum: 3 })) hasError(await validate([1, 2], { exclusiveMinimum: 3 })) hasError(await validate([1, 2, 3], { exclusiveMinimum: 3 })) hasError(await validate('12', { exclusiveMinimum: 3 })) hasError(await validate('123', { exclusiveMinimum: 3 })) hasError(await validate('1234', { len: 3 })) hasError(await validate({ aa: 1, bb: 2, cc: 3 }, { maxProperties: 2 })) noError(await validate({ aa: 1, cc: 3 }, { maxProperties: 2 })) hasError(await validate({ aa: 1 }, { minProperties: 2 })) noError(await validate({ aa: 1, bb: 2, cc: 3 }, { minProperties: 2 })) noError(await validate({ aa: 1, cc: 3 }, { maxProperties: 2 })) }) test('const', async () => { noError(await validate('', { const: '123' })) noError(await validate('123', { const: '123' })) hasError(await validate('xxx', { const: '123' })) }) test('multipleOf', async () => { noError(await validate('', { multipleOf: 2 })) noError(await validate(4, { multipleOf: 2 })) hasError(await validate(3, { multipleOf: 2 })) }) test('uniqueItems', async () => { noError(await validate('', { uniqueItems: true })) noError(await validate(4, { uniqueItems: true })) hasError(await validate([1, 2], { uniqueItems: true })) hasError( await validate([{ label: '11', value: '11' }, { label: '11' }], { uniqueItems: true, }) ) noError(await validate([1, 1], { uniqueItems: true })) noError( await validate( [ { label: '11', value: '11' }, { label: '11', value: '11' }, ], { uniqueItems: true } ) ) }) test('pattern', async () => { hasError(await validate('aaa', { pattern: /^\d+$/ })) }) test('validator', async () => { hasError( await validate('aaa', { validator() { return false }, message: 'error', }), 'error' ) }) test('whitespace', async () => { hasError( await validate(' ', { whitespace: true, }) ) }) test('enum', async () => { hasError( await validate('11', { enum: ['22', '33'], }) ) noError( await validate('11', { enum: ['22', '33', '11'], }) ) }) test('filter trigger type(unmatch)', async () => { expect( await validate( '', { triggerType: 'onBlur', required: true, validator() { return 'validate error' }, }, { validateFirst: true, triggerType: 'onInput', } ) ).toEqual({ error: [], success: [], warning: [], }) }) test('filter trigger type(match first validate)', async () => { expect( await validate( '', { triggerType: 'onBlur', required: true, validator() { return 'validate error' }, }, { validateFirst: true, triggerType: 'onBlur', } ) ).toEqual({ error: ['The field value is required'], success: [], warning: [], }) }) test('filter trigger type(match multi validate)', async () => { expect( await validate( '', { triggerType: 'onBlur', required: true, validator() { return 'validate error' }, }, { triggerType: 'onBlur', } ) ).toEqual({ error: ['The field value is required', 'validate error'], success: [], warning: [], }) }) test('validate formats(date)', async () => { noError(await validate('', 'date')) hasError(await validate('2020-1', 'date')) hasError(await validate('2020-01- 11:23:33', 'date')) hasError(await validate('12/01/', 'date')) noError(await validate('2020-1-12', 'date')) noError(await validate('2020/1/12', 'date')) noError(await validate('2020-01-12', 'date')) noError(await validate('2020/01/12', 'date')) noError(await validate('12/01/2020', 'date')) noError(await validate('2020-01-12 11:23:33', 'date')) noError(await validate('2020/01/12 11:23:33', 'date')) noError(await validate('12/01/2020 11:23:33', 'date')) noError(await validate('12/1/2020 11:23:33', 'date')) }) test('validate formats(number)', async () => { noError(await validate('', 'number')) hasError(await validate('12323d', 'number')) noError(await validate('12323', 'number')) noError(await validate('12323.12', 'number')) noError(await validate('-12323.12', 'number')) noError(await validate('+12323.12', 'number')) }) test('validate formats(integer)', async () => { noError(await validate('', 'integer')) hasError(await validate('222.333', 'integer')) noError(await validate('12323', 'integer')) }) test('validate formats(phone)', async () => { noError(await validate('', 'phone')) hasError(await validate('222333', 'phone')) noError(await validate('15934567899', 'phone')) }) test('validate formats(money)', async () => { noError(await validate('$12', 'money')) hasError(await validate('$12.', 'money')) noError(await validate('$12.3', 'money')) }) test('validate custom validator', async () => { hasError(await validate('123', { custom: true })) noError(await validate('', { custom: true })) }) test('validate custom formats', async () => { hasError(await validate('aa asd', 'custom')) hasError(await validate('aa asd 中文', 'custom')) noError(await validate('中文', 'custom')) }) test('validate undefined format', async () => { expect( ( await validate('a', { required: false, pattern: '(\\d{3,4}-\\d{7,8}-\\d{4})|(4\\d{4,9})|(\\d{3,4}-\\d{7,8})', format: undefined, message: 'error', }) ).error ).toEqual(['error']) }) test('validator return boolean', async () => { hasError( await validate('123', { customBool: true, message: 'custom error', }), 'custom error' ) noError( await validate('123', { customBool2: true, message: 'custom error', }) ) }) test('language', async () => { setValidateLanguage('zh-CN') hasError( await validate('', { required: true, }), '该字段是必填字段' ) setValidateLanguage('en-US') hasError( await validate('', { required: true, }), 'The field value is required' ) }) test('validator template', async () => { registerValidateMessageTemplateEngine((message) => { if (typeof message !== 'string') return message return message.replace(/\<\<\s*([\w.]+)\s*\>\>/g, (_, $0) => { return { aa: 123 }[$0] }) }) hasError( await validate('', () => { return `<>=123` }), '123=123' ) }) test('validator template with format', async () => { registerValidateMessageTemplateEngine((message) => { if (typeof message !== 'string') return message return message.replace(/\<\<\s*([\w.]+)\s*\>\>/g, (_, $0) => { return { aa: 123 }[$0] }) }) hasError( await validate('', (value, rules, ctx, format) => { return `<>=123&${format('<>')}` }), '123=123&123' ) }) test('validator template with format and scope', async () => { registerValidateMessageTemplateEngine((message) => { if (typeof message !== 'string') return message return message.replace(/\<\<\s*([\w.]+)\s*\>\>/g, (_, $0) => { return { aa: 123 }[$0] }) }) const result = await validate( '', (value, rules, ctx, format) => { return `<>=123&${format('<>{{name}}')}` }, { context: { name: 'scopeName', }, } ) expect(result.error[0]).toEqual('123=123&123scopeName') }) test('validator order with format', async () => { hasError( await validate('', [ { required: true }, { format: 'url', }, ]), 'The field value is required' ) }) ================================================ FILE: packages/validator/src/formats.ts ================================================ export default { url: new RegExp( // protocol identifier '^(?:(?:(?:https?|ftp|rtmp):)?//)' + // user:pass authentication '(?:\\S+(?::\\S*)?@)?' + '(?:' + // IP address exclusion - private & local networks // Reference: https://www.arin.net/knowledge/address_filters.html // filter 10.*.*.* and 127.*.*.* addresses '(?!(?:10|127)(?:\\.\\d{1,3}){3})' + // filter 169.254.*.* and 192.168.*.* '(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})' + // filter 172.16.0.0 - 172.31.255.255 // TODO: add test to validate that it invalidates address in 16-31 range '(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' + // IP address dotted notation octets // excludes loopback network 0.0.0.0 // excludes reserved space >= 224.0.0.0 // excludes network & broadcast addresses // (first & last IP address of each class) // filter 1. part for 1-223 '(?:22[0-3]|2[01]\\d|[1-9]\\d?|1\\d\\d)' + // filter 2. and 3. part for 0-255 '(?:\\.(?:25[0-5]|2[0-4]\\d|1?\\d{1,2})){2}' + // filter 4. part for 1-254 '(?:\\.(?:25[0-4]|2[0-4]\\d|1\\d\\d|[1-9]\\d?))' + '|' + // host name '(?:(?:[a-z\\u00a1-\\uffff0-9_]-*)*[a-z\\u00a1-\\uffff0-9_]+)' + // domain name '(?:\\.(?:[a-z\\u00a1-\\uffff0-9_]-*)*[a-z\\u00a1-\\uffff0-9_]+)*' + // TLD identifier '(?:\\.(?:[a-z\\u00a1-\\uffff_]{2,}))' + ')' + // port number '(?::\\d{2,5})?' + // resource path '(?:/?\\S*)?$' ), email: /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/, ipv6: /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/, ipv4: /^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})$/, number: /^[+-]?\d+(\.\d+)?$/, integer: /^[+-]?\d+$/, qq: /^(\+?[1-9]\d*|0)$/, phone: /^\d{3}-\d{8}$|^\d{4}-\d{7}$|^\d{11}$/, idcard: /^\d{15}$|^\d{17}(\d|x|X)$/, money: /^([\u0024\u00A2\u00A3\u00A4\u20AC\u00A5\u20B1\u20B9\uFFE5]\s*)(\d+,?)+(\.\d+)?\s*$/, zh: /^[\u4e00-\u9fa5]+$/, date: /^[0-9]+[./-][0-9]+[./-][0-9]+\s*(?:[0-9]+\s*:\s*[0-9]+\s*:\s*[0-9]+)?$/, zip: /^[0-9]{6}$/, } ================================================ FILE: packages/validator/src/index.ts ================================================ export * from './validator' export * from './parser' export * from './registry' export * from './types' ================================================ FILE: packages/validator/src/locale.ts ================================================ export default { en: { pattern: 'This field is invalid', invalid: 'This field is invalid', required: 'The field value is required', number: 'The field value is not a number', integer: 'The field value is not an integer number', url: 'The field value is a invalid url', email: 'The field value is not a email format', ipv6: 'The field value is not a ipv6 format', ipv4: 'The field value is not a ipv4 format', idcard: 'The field value is not an idcard format', qq: 'The field value is not a qq number format', phone: 'The field value is not a phone number format', money: 'The field value is not a currency format', zh: 'The field value is not a chinese string', date: 'The field value is not a valid date format', zip: 'The field value is not a zip format', len: 'The length or number of entries must be {{len}}', min: 'The length or number of entries must be at least {{min}}', minLength: 'The length or number of entries must be at least {{minLength}}', minItems: 'The length or number of entries must be at least {{minItems}}', maximum: 'The field value cannot be greater than {{maximum}}', exclusiveMaximum: 'The field value must be less than {{exclusiveMaximum}}', minimum: 'The field value cannot be less than {{minimum}}', exclusiveMinimum: 'The field value must be greater than {{exclusiveMinimum}}', max: 'The field length or number of entries must be at most {{max}}', maxLength: 'The field length or number of entries must be at most {{maxLength}}', maxItems: 'The field length or number of entries must be at most {{maxItems}}', whitespace: 'This field value cannot be blank string.', enum: 'The field value must be one of {{enum}}', const: 'The field value must be equal to {{const}}', multipleOf: 'The field value must be divisible by {{multipleOf}}', maxProperties: 'The number of field properties cannot be greater than {{maxProperties}}', minProperties: 'The number of field properties cannot be less than {{maxProperties}}', uniqueItems: 'Array elements are not unique', }, zh: { pattern: '该字段不是一个合法的字段', invalid: '该字段不是一个合法的字段', required: '该字段是必填字段', number: '该字段不是合法的数字', integer: '该字段不是合法的整型数字', url: '该字段不是合法的url', email: '该字段不是合法的邮箱格式', ipv6: '该字段不是合法的ipv6格式', ipv4: '该字段不是合法的ipv4格式', idcard: '该字段不是合法的身份证格式', qq: '该字段不符合QQ号格式', phone: '该字段不是有效的手机号', money: '该字段不是有效货币格式', zh: '该字段不是合法的中文字符串', date: '该字段不是合法的日期格式', zip: '该字段不是合法的邮编格式', len: '长度或条目数必须为{{len}}', min: '长度或条目数不能小于{{min}}', minLength: '长度或条目数不能小于{{minLength}}', minItems: '长度或条目数不能小于{{minItems}}', max: '长度或条目数不能大于{{max}}', maxLength: '长度或条目数不能大于{{maxLength}}', maxItems: '长度或条目数不能大于{{maxItems}}', maximum: '数值不能大于{{maximum}}', exclusiveMaximum: '数值必须小于{{exclusiveMaximum}}', minimum: '数值不能小于{{minimum}}', exclusiveMinimum: '数值必须大于{{exclusiveMinimum}}', whitespace: '不能为纯空白字符串', enum: '字段值必须为{{enum}}其中一个', const: '字段值必须等于{{const}}', multipleOf: '字段值不能被{{multipleOf}}整除', maxProperties: '字段属性数量不能大于{{maxProperties}}', minProperties: '字段属性数量不能小于{{minProperties}}', uniqueItems: '数组元素不唯一', }, 'en-US': { pattern: 'This field is invalid', invalid: 'This field is invalid', required: 'The field value is required', number: 'The field value is not a number', integer: 'The field value is not an integer number', url: 'The field value is a invalid url', email: 'The field value is not a email format', ipv6: 'The field value is not a ipv6 format', ipv4: 'The field value is not a ipv4 format', idcard: 'The field value is not an idcard format', qq: 'The field value is not a qq number format', phone: 'The field value is not a phone number format', money: 'The field value is not a currency format', zh: 'The field value is not a chinese string', date: 'The field value is not a valid date format', zip: 'The field value is not a zip format', len: 'The length or number of entries must be {{len}}', min: 'The length or number of entries must be at least {{min}}', minLength: 'The length or number of entries must be at least {{minLength}}', minItems: 'The length or number of entries must be at least {{minItems}}', maximum: 'The field value cannot be greater than {{maximum}}', exclusiveMaximum: 'The field value must be less than {{exclusiveMaximum}}', minimum: 'The field value cannot be less than {{minimum}}', exclusiveMinimum: 'The field value must be greater than {{exclusiveMinimum}}', max: 'The field length or number of entries must be at most {{max}}', maxLength: 'The field length or number of entries must be at most {{maxLength}}', maxItems: 'The field length or number of entries must be at most {{maxItems}}', whitespace: 'This field value cannot be blank string.', enum: 'The field value must be one of {{enum}}', const: 'The field value must be equal to {{const}}', multipleOf: 'The field value must be divisible by {{multipleOf}}', maxProperties: 'The number of field properties cannot be greater than {{maxProperties}}', minProperties: 'The number of field properties cannot be less than {{maxProperties}}', uniqueItems: 'Array elements are not unique', }, 'zh-CN': { pattern: '该字段不是一个合法的字段', invalid: '该字段不是一个合法的字段', required: '该字段是必填字段', number: '该字段不是合法的数字', integer: '该字段不是合法的整型数字', url: '该字段不是合法的url', email: '该字段不是合法的邮箱格式', ipv6: '该字段不是合法的ipv6格式', ipv4: '该字段不是合法的ipv4格式', idcard: '该字段不是合法的身份证格式', qq: '该字段不符合QQ号格式', phone: '该字段不是有效的手机号', money: '该字段不是有效货币格式', zh: '该字段不是合法的中文字符串', date: '该字段不是合法的日期格式', zip: '该字段不是合法的邮编格式', len: '长度或条目数必须为{{len}}', min: '长度或条目数不能小于{{min}}', minLength: '长度或条目数不能小于{{minLength}}', minItems: '长度或条目数不能小于{{minItems}}', maxLength: '长度或条目数不能大于{{maxLength}}', maxItems: '长度或条目数不能大于{{maxItems}}', max: '长度或条目数不能大于{{max}}', maximum: '数值不能大于{{maximum}}', exclusiveMaximum: '数值必须小于{{exclusiveMaximum}}', minimum: '数值不能小于{{minimum}}', exclusiveMinimum: '数值必须大于{{exclusiveMinimum}}', whitespace: '不能为纯空白字符串', enum: '字段值必须为{{enum}}其中一个', const: '字段值必须等于{{const}}', multipleOf: '字段值不能被{{multipleOf}}整除', maxProperties: '字段属性数量不能大于{{maxProperties}}', minProperties: '字段属性数量不能小于{{minProperties}}', uniqueItems: '数组元素不唯一', }, 'zh-TW': { pattern: '該字段不是一個合法的字段', invalid: '該字段不是一個合法的字段', required: '該字段是必填字段', number: '該字段不是合法的數字', integer: '該字段不是合法的整型數字', url: '該字段不是合法的url', email: '該字段不是合法的郵箱格式', ipv6: '該字段不是合法的ipv6格式', ipv4: '該字段不是合法的ipv4格式', idcard: '該字段不是合法的身份證格式', qq: '該字段不符合QQ號格式', phone: '該字段不是有效的手機號', money: '該字段不是有效貨幣格式', zh: '該字段不是合法的中文字符串', date: '該字段不是合法的日期格式', zip: '該字段不是合法的郵編格式', len: '長度或條目數必須為{{len}}', min: '長度或條目數不能小於{{min}}', minItems: '長度或條目數不能小於{{minItems}}', minLength: '長度或條目數不能小於{{minLength}}', max: '長度或條目數不能大於{{max}}', maxItems: '長度或條目數不能大於{{maxItems}}', maxLength: '長度或條目數不能大於{{maxLength}}', maximum: '數值不能大於{{maximum}}', exclusiveMaximum: '數值必須小於{{exclusiveMaximum}}', minimum: '數值不能小於{{minimum}}', exclusiveMinimum: '數值必須大於{{exclusiveMinimum}}', whitespace: '不能為純空白字符串', enum: '字段值必須為{{enum}}其中一個', const: '字段值必須等於{{const}}', multipleOf: '字段值不能被{{multipleOf}}整除', maxProperties: '字段屬性數量不能大於{{maxProperties}}', minProperties: '字段屬性數量不能小於{{minProperties}}', uniqueItems: '數組元素不唯一', }, ja: { url: 'このフィールドは無効なURLです', whitespace: 'このフィールドを空の文字列にすることはできません。', zh: 'このフィールドは中国語の文字列ではありません', zip: 'このフィールドはzip形式ではありません', date: 'このフィールドは有効な日付形式ではありません', email: 'このフィールドはメール形式ではありません', exclusiveMaximum: '値は{{exclusiveMaximum}}未満である必要があります', exclusiveMinimum: '値は{{exclusiveMinimum}}より大きい必要があります', idcard: 'このフィールドはIDカード形式ではありません', integer: 'このフィールドは整数ではありません', ipv4: 'このフィールドはIPv4形式ではありません', ipv6: 'このフィールドはIPv6形式ではありません', len: 'エントリの長さまたは数は{{len}}でなければなりません', max: 'エントリの長さまたは数は最大{{max}}でなければなりません', maxItems: 'エントリの長さまたは数は最大{{maxItems}}でなければなりません', maxLength: 'エントリの長さまたは数は最大{{maxLength}}でなければなりません', maximum: '値は{{最大}}を超えることはできません', min: 'エントリの長さまたは数は、少なくとも{{min}}である必要があります', minItems: 'エントリの長さまたは数は、少なくとも{{minItems}}である必要があります', minLength: 'エントリの長さまたは数は、少なくとも{{minLength}}である必要があります', minimum: '値は{{minimum}}以上にする必要があります', money: 'このフィールドは通貨形式ではありません', number: 'このフィールドは数値ではありません', pattern: 'このフィールドはどのパターンとも一致しません', invalid: 'このフィールドはどのパターンとも一致しません', phone: 'このフィールドは電話番号の形式ではありません', qq: 'このフィールドはqq数値形式ではありません', required: 'この項目は必須です', enum: 'フィールド値は{{enum}}のいずれかである必要があります', cons: 'フィールド値は{{const}}と等しくなければなりません', multipleOf: 'フィールド値を{{multipleOf}}で割り切れない', maxProperties: 'フィールドプロパティの数は{{maxProperties}}を超えることはできません', minProperties: 'フィールドプロパティの数は{{minProperties}}未満にすることはできません', uniqueItems: '配列要素は一意ではありません', }, } ================================================ FILE: packages/validator/src/parser.ts ================================================ import { isArr, isBool, isFn, isStr } from '@formily/shared' import { ValidatorDescription, ValidatorFunction, ValidatorParsedFunction, Validator, IValidatorRules, isValidateResult, IValidatorOptions, } from './types' import { getValidateRules, getValidateLocale } from './registry' import { render } from './template' const getRuleMessage = (rule: IValidatorRules, type: string) => { if (rule.format) { return rule.message || getValidateLocale(rule.format) } return rule.message || getValidateLocale(type) } export const parseValidatorDescription = ( description: ValidatorDescription ): IValidatorRules => { if (!description) return {} let rules: IValidatorRules = {} if (isStr(description)) { rules.format = description } else if (isFn(description)) { rules.validator = description } else { rules = Object.assign(rules, description) } return rules } export const parseValidatorDescriptions = ( validator: Validator ): IValidatorRules[] => { if (!validator) return [] const array = isArr(validator) ? validator : [validator] return array.map((description) => { return parseValidatorDescription(description) }) } export const parseValidatorRules = ( rules: IValidatorRules = {} ): ValidatorParsedFunction[] => { const getRulesKeys = (): string[] => { const keys = [] if ('required' in rules) { keys.push('required') } for (let key in rules) { if (key === 'required' || key === 'validator') continue keys.push(key) } if ('validator' in rules) { keys.push('validator') } return keys } const getContext = (context: any, value: any) => { return { ...rules, ...context, value, } } const createValidate = (callback: ValidatorFunction, message: string) => async (value: any, context: any) => { const context_ = getContext(context, value) try { const results = await callback( value, { ...rules, message }, context_, (message: string, scope: any) => { return render( { type: 'error', message, }, Object.assign(context_, scope) )?.message } ) if (isBool(results)) { if (!results) { return render( { type: 'error', message, }, context_ ) } return { type: 'error', message: undefined, } } else if (results) { if (isValidateResult(results)) { return render(results, context_) } return render( { type: 'error', message: results, }, context_ ) } return { type: 'error', message: undefined, } } catch (e) { return { type: 'error', message: e?.message || e, } } } return getRulesKeys().reduce((buf, key) => { const callback = getValidateRules(key) if (callback) { const validator = createValidate(callback, getRuleMessage(rules, key)) return buf.concat(validator) } return buf }, []) } export const parseValidator = ( validator: Validator, options: IValidatorOptions = {} ) => { if (!validator) return [] const array = isArr(validator) ? validator : [validator] return array.reduce[]>( (buf, description) => { const rules = parseValidatorDescription(description) const triggerType = rules.triggerType ?? 'onInput' if (options?.triggerType && options.triggerType !== triggerType) return buf return rules ? buf.concat(parseValidatorRules(rules)) : buf }, [] ) } ================================================ FILE: packages/validator/src/registry.ts ================================================ import { FormPath, each, lowerCase, globalThisPolyfill, merge as deepmerge, isFn, isStr, } from '@formily/shared' import { ValidatorFunctionResponse, ValidatorFunction, IRegistryFormats, IRegistryLocaleMessages, IRegistryLocales, IRegistryRules, } from './types' const getIn = FormPath.getIn const self: any = globalThisPolyfill const defaultLanguage = 'en' const getBrowserlanguage = () => { /* istanbul ignore next */ if (!self.navigator) { return defaultLanguage } return ( self.navigator.browserlanguage || self.navigator.language || defaultLanguage ) } const registry = { locales: { messages: {}, language: getBrowserlanguage(), }, formats: {}, rules: {}, template: null, } const getISOCode = (language: string) => { let isoCode = registry.locales.language if (registry.locales.messages[language]) { return language } const lang = lowerCase(language) each( registry.locales.messages, (messages: IRegistryLocaleMessages, key: string) => { const target = lowerCase(key) if (target.indexOf(lang) > -1 || lang.indexOf(target) > -1) { isoCode = key return false } } ) return isoCode } export const getValidateLocaleIOSCode = getISOCode export const setValidateLanguage = (lang: string) => { registry.locales.language = lang || defaultLanguage } export const getValidateLanguage = () => registry.locales.language export const getLocaleByPath = ( path: string, lang: string = registry.locales.language ) => getIn(registry.locales.messages, `${getISOCode(lang)}.${path}`) export const getValidateLocale = (path: string) => { const message = getLocaleByPath(path) return ( message || getLocaleByPath('pattern') || getLocaleByPath('pattern', defaultLanguage) ) } export const getValidateMessageTemplateEngine = () => registry.template export const getValidateFormats = (key?: string) => key ? registry.formats[key] : registry.formats export const getValidateRules = ( key?: T ): T extends string ? ValidatorFunction : { [key: string]: ValidatorFunction } => key ? registry.rules[key as any] : registry.rules export const registerValidateLocale = (locale: IRegistryLocales) => { registry.locales.messages = deepmerge(registry.locales.messages, locale) } export const registerValidateRules = (rules: IRegistryRules) => { each(rules, (rule, key) => { if (isFn(rule)) { registry.rules[key] = rule } }) } export const registerValidateFormats = (formats: IRegistryFormats) => { each(formats, (pattern, key) => { if (isStr(pattern) || pattern instanceof RegExp) { registry.formats[key] = new RegExp(pattern) } }) } export const registerValidateMessageTemplateEngine = ( template: (message: ValidatorFunctionResponse, context: any) => any ) => { registry.template = template } ================================================ FILE: packages/validator/src/rules.ts ================================================ import { isEmpty, isValid, stringLength, isStr, isArr, isFn, toArr, isBool, isNum, isEqual, each, } from '@formily/shared' import { getValidateFormats } from './registry' import { IRegistryRules } from './types' const isValidateEmpty = (value: any) => { if (isArr(value)) { for (let i = 0; i < value.length; i++) { if (isValid(value[i])) return false } return true } else { //compat to draft-js if (value?.getCurrentContent) { /* istanbul ignore next */ return !value.getCurrentContent()?.hasText() } return isEmpty(value) } } const getLength = (value: any) => isStr(value) ? stringLength(value) : value ? value.length : 0 const extendSameRules = ( rules: IRegistryRules, names: Record ) => { each(names, (realName, name) => { rules[name] = (value, rule, ...args) => rules[realName](value, { ...rule, [realName]: rule[name] }, ...args) }) } const RULES: IRegistryRules = { format(value, rule) { if (isValidateEmpty(value)) return '' if (rule.format) { const format = getValidateFormats(rule.format) if (format) { return !new RegExp(format).test(value) ? rule.message : '' } } return '' }, required(value, rule) { if (rule.required !== true) return '' return isValidateEmpty(value) ? rule.message : '' }, max(value, rule) { if (isValidateEmpty(value)) return '' const length = isNum(value) ? value : getLength(value) const max = Number(rule.max) return length > max ? rule.message : '' }, min(value, rule) { if (isValidateEmpty(value)) return '' const length = isNum(value) ? value : getLength(value) const min = Number(rule.min) return length < min ? rule.message : '' }, exclusiveMaximum(value, rule) { if (isValidateEmpty(value)) return '' const length = isNum(value) ? value : getLength(value) const max = Number(rule.exclusiveMaximum) return length >= max ? rule.message : '' }, exclusiveMinimum(value, rule) { if (isValidateEmpty(value)) return '' const length = isNum(value) ? value : getLength(value) const min = Number(rule.exclusiveMinimum) return length <= min ? rule.message : '' }, len(value, rule) { if (isValidateEmpty(value)) return '' const length = getLength(value) const len = Number(rule.len) return length !== len ? rule.message : '' }, pattern(value, rule) { if (isValidateEmpty(value)) return '' return !new RegExp(rule.pattern).test(value) ? rule.message : '' }, async validator(value, rule, context, format) { if (isFn(rule.validator)) { const response = await Promise.resolve( rule.validator(value, rule, context, format) ) if (isBool(response)) { return !response ? rule.message : '' } else { return response } } /* istanbul ignore next */ throw new Error("The rule's validator property must be a function.") }, whitespace(value, rule) { if (isValidateEmpty(value)) return '' if (rule.whitespace) { return /^\s+$/.test(value) ? rule.message : '' } }, enum(value, rule) { if (isValidateEmpty(value)) return '' const enums = toArr(rule.enum) return enums.indexOf(value) === -1 ? rule.message : '' }, const(value, rule) { if (isValidateEmpty(value)) return '' return rule.const !== value ? rule.message : '' }, multipleOf(value, rule) { if (isValidateEmpty(value)) return '' return Number(value) % Number(rule.multipleOf) !== 0 ? rule.message : '' }, uniqueItems(value, rule) { if (isValidateEmpty(value)) return '' value = toArr(value) return value.some((item: any, index: number) => { for (let i = 0; i < value.length; i++) { if (i !== index && !isEqual(value[i], item)) { return false } } return true }) ? '' : rule.message }, maxProperties(value, rule) { if (isValidateEmpty(value)) return '' return Object.keys(value || {}).length <= Number(rule.maxProperties) ? '' : rule.message }, minProperties(value, rule) { if (isValidateEmpty(value)) return '' return Object.keys(value || {}).length >= Number(rule.minProperties) ? '' : rule.message }, } extendSameRules(RULES, { maximum: 'max', minimum: 'min', maxItems: 'max', minItems: 'min', maxLength: 'max', minLength: 'min', }) export default RULES ================================================ FILE: packages/validator/src/template.ts ================================================ import { isFn, isStr, FormPath } from '@formily/shared' import { IValidateResult, IValidatorRules } from './types' import { getValidateMessageTemplateEngine } from './registry' export const render = ( result: IValidateResult, rules: IValidatorRules ): IValidateResult => { const { message } = result if (isStr(message)) { const template = getValidateMessageTemplateEngine() if (isFn(template)) { result.message = template(message, rules) } result.message = result.message.replace( /\{\{\s*([\w.]+)\s*\}\}/g, (_, $0) => { return FormPath.getIn(rules, $0) } ) } return result } ================================================ FILE: packages/validator/src/types.ts ================================================ export type ValidatorFormats = | 'url' | 'email' | 'ipv6' | 'ipv4' | 'number' | 'integer' | 'idcard' | 'qq' | 'phone' | 'money' | 'zh' | 'date' | 'zip' | (string & {}) export interface IValidateResult { type: 'error' | 'warning' | 'success' | (string & {}) message: string } export interface IValidateResults { error?: string[] warning?: string[] success?: string[] } export const isValidateResult = (obj: any): obj is IValidateResult => !!obj['type'] && !!obj['message'] export type ValidatorFunctionResponse = | null | string | boolean | IValidateResult export type ValidatorFunction = ( value: any, rule: IValidatorRules, ctx: Context, render: (message: string, scope?: any) => string ) => ValidatorFunctionResponse | Promise | null export type ValidatorParsedFunction = ( value: any, ctx: Context ) => IValidateResult | Promise | null export type ValidatorTriggerType = | 'onInput' | 'onFocus' | 'onBlur' | (string & {}) export interface IValidatorRules { triggerType?: ValidatorTriggerType format?: ValidatorFormats validator?: ValidatorFunction required?: boolean pattern?: RegExp | string max?: number maximum?: number maxItems?: number minItems?: number maxLength?: number minLength?: number exclusiveMaximum?: number exclusiveMinimum?: number minimum?: number min?: number len?: number whitespace?: boolean enum?: any[] const?: any multipleOf?: number uniqueItems?: boolean maxProperties?: number minProperties?: number message?: string [key: string]: any } export interface IRegistryLocaleMessages { [key: string]: string | IRegistryLocaleMessages } export interface IRegistryLocales { [language: string]: IRegistryLocaleMessages } export interface IRegistryRules { [key: string]: ValidatorFunction } export interface IRegistryFormats { [key: string]: string | RegExp } export type ValidatorDescription = | ValidatorFormats | ValidatorFunction | IValidatorRules export type MultiValidator = ValidatorDescription[] export type Validator = | ValidatorDescription | MultiValidator export interface IValidatorOptions { validateFirst?: boolean triggerType?: ValidatorTriggerType context?: Context } ================================================ FILE: packages/validator/src/validator.ts ================================================ import { parseValidator } from './parser' import { IValidateResults, Validator, IValidatorOptions } from './types' import { registerValidateFormats, registerValidateLocale, registerValidateRules, } from './registry' import locales from './locale' import formats from './formats' import rules from './rules' registerValidateRules(rules) registerValidateLocale(locales) registerValidateFormats(formats) export const validate = async ( value: any, validator: Validator, options?: IValidatorOptions ): Promise => { const validates = parseValidator(validator, options) const results: IValidateResults = { error: [], success: [], warning: [], } for (let i = 0; i < validates.length; i++) { const result = await validates[i](value, options?.context) const { type, message } = result results[type] = results[type] || [] if (message) { results[type].push(message) if (options?.validateFirst) break } } return results } ================================================ FILE: packages/validator/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./lib", "paths": { "@formily/*": ["packages/*", "devtools/*"] }, "declaration": true } } ================================================ FILE: packages/validator/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["./src/**/*.ts", "./src/**/*.tsx"], "exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"] } ================================================ FILE: packages/vue/.npmignore ================================================ node_modules *.log build docs doc-site __tests__ .eslintrc jest.config.js vue.config.js tsconfig.json .umi src ================================================ FILE: packages/vue/README.md ================================================ ## Usage ### Requirement vue^2.6.0 + @vue/composition-api^1.0.0-beta.1 Or vue>=3.0.0-rc.0 ================================================ FILE: packages/vue/bin/formily-vue-fix.js ================================================ #!/usr/bin/env node 'use strict' require('../scripts/postinstall') ================================================ FILE: packages/vue/bin/formily-vue-switch.js ================================================ #!/usr/bin/env node 'use strict' require('../scripts/switch-cli') ================================================ FILE: packages/vue/docs/.vuepress/components/createCodeSandBox.js ================================================ import { getParameters } from 'codesandbox/lib/api/define' const CodeSandBoxHTML = '
' const CodeSandBoxJS = ` import Vue from 'vue' import Antd from 'ant-design-vue'; import 'ant-design-vue/dist/antd.css'; import App from './App.vue' Vue.config.productionTip = false; Vue.use(Antd); new Vue({ render: h => h(App), }).$mount('#app')` const createForm = ({ method, action, data }) => { const form = document.createElement('form') // 构造 form form.style.display = 'none' // 设置为不显示 form.target = '_blank' // 指向 iframe // 构造 formdata Object.keys(data).forEach((key) => { const input = document.createElement('input') // 创建 input input.name = key // 设置 name input.value = data[key] // 设置 value form.appendChild(input) }) form.method = method // 设置方法 form.action = action // 设置地址 document.body.appendChild(form) // 对该 form 执行提交 form.submit() document.body.removeChild(form) } export function createCodeSandBox(codeStr) { const parameters = getParameters({ files: { 'sandbox.config.json': { content: { template: 'node', infiniteLoopProtection: true, hardReloadOnChange: false, view: 'browser', container: { port: 8080, node: '14', }, }, }, 'package.json': { content: { scripts: { serve: 'vue-cli-service serve', build: 'vue-cli-service build', lint: 'vue-cli-service lint', }, dependencies: { '@formily/core': 'latest', '@formily/vue': 'latest', 'core-js': '^3.6.5', 'ant-design-vue': 'latest', 'vue-demi': 'latest', vue: '^2.6.11', }, devDependencies: { '@vue/cli-plugin-babel': '~4.5.0', '@vue/cli-service': '~4.5.0', '@vue/composition-api': 'latest', 'vue-template-compiler': '^2.6.11', }, babel: { presets: ['@vue/cli-plugin-babel/preset'], }, vue: { devServer: { host: '0.0.0.0', disableHostCheck: true, // 必须 }, }, }, }, 'src/App.vue': { content: codeStr, }, 'src/main.js': { content: CodeSandBoxJS, }, 'public/index.html': { content: CodeSandBoxHTML, }, }, }) createForm({ method: 'post', action: 'https://codesandbox.io/api/v1/sandboxes/define', data: { parameters, query: 'file=/src/App.vue', }, }) } ================================================ FILE: packages/vue/docs/.vuepress/components/dumi-previewer.vue ================================================ ================================================ FILE: packages/vue/docs/.vuepress/components/highlight.js ================================================ const prism = require('prismjs') const escapeHtml = require('escape-html') const loadLanguages = require('prismjs/components/index') function wrap(code, lang) { if (lang === 'text') { code = escapeHtml(code) } return `
${code}
` } function getLangCodeFromExtension(extension) { const extensionMap = { vue: 'markup', html: 'markup', md: 'markdown', rb: 'ruby', ts: 'typescript', py: 'python', sh: 'bash', yml: 'yaml', styl: 'stylus', kt: 'kotlin', rs: 'rust', } return extensionMap[extension] || extension } module.exports = (str, lang) => { if (!lang) { return wrap(str, 'text') } lang = lang.toLowerCase() const rawLang = lang lang = getLangCodeFromExtension(lang) if (!prism.languages[lang]) { try { loadLanguages([lang]) } catch (e) { console.warn( `[vuepress] Syntax highlight for language "${lang}" is not supported.` ) } } if (prism.languages[lang]) { const code = prism.highlight(str, prism.languages[lang], lang) return wrap(code, rawLang) } return wrap(str, 'text') } ================================================ FILE: packages/vue/docs/.vuepress/config.js ================================================ const path = require('path') module.exports = { title: 'Formily Vue', dest: './doc-site', theme: '@vuepress-dumi/dumi', head: [ [ 'link', { rel: 'icon', href: '//img.alicdn.com/imgextra/i3/O1CN01XtT3Tv1Wd1b5hNVKy_!!6000000002810-55-tps-360-360.svg', }, ], ], themeConfig: { logo: '//img.alicdn.com/imgextra/i2/O1CN01Kq3OHU1fph6LGqjIz_!!6000000004056-55-tps-1141-150.svg', nav: [ { text: '指南', link: '/guide/', }, { text: 'API', link: '/api/components/field', }, { text: 'Q&A', link: '/questions/', }, { text: '主站', link: 'https://formilyjs.org', }, { text: 'GITHUB', link: 'https://github.com/alibaba/formily', }, ], sidebar: { '/guide/': ['', 'architecture', 'concept'], '/api/': [ { title: 'Components', children: [ '/api/components/field', '/api/components/array-field', '/api/components/object-field', '/api/components/void-field', '/api/components/schema-field', '/api/components/schema-field-with-schema', '/api/components/recursion-field', '/api/components/recursion-field-with-component', '/api/components/form-provider', '/api/components/form-consumer', '/api/components/expression-scope', ], }, { title: 'Hooks', children: [ '/api/hooks/use-field', '/api/hooks/use-field-schema', '/api/hooks/use-form', '/api/hooks/use-form-effects', '/api/hooks/use-parent-form', ], }, { title: 'Shared', children: [ '/api/shared/connect', '/api/shared/injections', '/api/shared/map-props', '/api/shared/map-read-pretty', '/api/shared/observer', '/api/shared/schema', ], }, ], }, lastUpdated: 'Last Updated', smoothScroll: true, }, plugins: [ 'vuepress-plugin-typescript', '@vuepress/back-to-top', '@vuepress/last-updated', '@vuepress-dumi/dumi-previewer', [ '@vuepress/medium-zoom', { selector: '.content__default :not(a) > img', }, ], ], configureWebpack: (config, isServer) => { return { resolve: { alias: { '@formily/vue': path.resolve(__dirname, '../../src'), '@formily/json-schema': path.resolve( __dirname, '../../../json-schema/src' ), '@formily/path': path.resolve(__dirname, '../../../path/src'), '@formily/reactive-vue': path.resolve( __dirname, '../../../reactive-vue/src' ), '@formily/element': path.resolve(__dirname, '../../../element/src'), vue: path.resolve( __dirname, '../../../../node_modules/vue/dist/vue.runtime.esm.js' ), }, }, } }, } ================================================ FILE: packages/vue/docs/.vuepress/enhanceApp.js ================================================ import pageComponents from '@internal/page-components' export default ({ Vue }) => { for (const [name, component] of Object.entries(pageComponents)) { Vue.component(name, component) } } ================================================ FILE: packages/vue/docs/.vuepress/styles/index.styl ================================================ .navbar { padding: 0 28px !important; } .navbar .logo { height: auto !important; width: 150px !important; } .navbar .site-name { display: none; } .navbar .sidebar-button { padding: 0; } .home .feature { margin-bottom: 40px; text-align: center; } .theme-dumi-content:not(.custom) { max-width: 100%; } .page .page-nav { max-width: 100%; } .dumi-previewer .dumi-previewer-actions .dumi-previewer-actions__icon { padding: 0 !important; } .page .page-edit { max-width 100% } .sidebar-group .sidebar-heading { color: #454d64; font-size: 16px; } .sidebar-group a.sidebar-link { font-size: 0.9em; } .theme-dumi-content .custom-block.warning { padding: 10px 20px; border-color: #FFC11F; box-shadow: 0 6px 16px -2px rgba(0,0,0,.06); background: rgba(255,229,100,0.1); } .theme-dumi-content .custom-block.danger { padding: 10px 20px; p { margin: 0; } } .theme-dumi-content:not(.custom) > h1, .theme-dumi-content:not(.custom) > h2, .theme-dumi-content:not(.custom) > h3, .theme-dumi-content:not(.custom) > h4, .theme-dumi-content:not(.custom) > h5, .theme-dumi-content:not(.custom) > h6 { margin-bottom: 18px; } .theme-dumi-content p { margin: 1em 0; } .custom-block.warning p { margin: 0; } // .theme-dumi-content div[class*="language-"] { // background-color: #f9fafb; // } // .theme-dumi-content pre[class*="language-"] code { // color: #000; // } .dumi-previewer .dumi-previewer-source, .dumi-previewer .dumi-previewer-demo { overflow: auto; } @media (max-width: 719px) { .sidebar-button + .home-link { margin-left: 20px; } } @media (max-width: 419px) { .theme-dumi-content div[class*="language-"] { margin: 0; border-radius: 0; } } ================================================ FILE: packages/vue/docs/README.md ================================================ --- home: true heroText: Vue Library tagline: 阿里巴巴统一前端表单解决方案 actionText: 开发指南 actionLink: /guide/ features: - title: 超高性能 details: 依赖追踪,高效更新,按需渲染 - title: 开箱即用 details: 组件状态自动绑定,接入成本极低 - title: 协议驱动 details: 标准JSON-Schema - title: 场景复用 details: 基于协议驱动,抽象场景组件 - title: 调试友好 details: 天然对接Formily DevTools - title: 智能提示 details: 拥抱Typescript footer: Open-source MIT Licensed | Copyright © 2019-present --- ## 安装 vue3: ```bash $ npm install --save @formily/core @formily/vue ``` vue2: ```bash $ npm install --save @formily/core @formily/vue @vue/composition-api ``` ## 快速开始 ================================================ FILE: packages/vue/docs/api/components/array-field.md ================================================ --- order: 1 --- # ArrayField ## 描述 作为@formily/core 的 [createArrayField](https://core.formilyjs.org/api/models/form#createarrayfield) Vue 实现,它是专门用于将 ViewModel 与输入控件做绑定的桥接组件,ArrayField 组件属性参考[IFieldFactoryProps](https://core.formilyjs.org/api/models/form#ifieldfactoryprops) ::: warning 我们在使用 ArrayField 组件的时候,一定要记得传 name 属性。同时要使用 scoped slots 形式来组织子组件 ::: ## 签名 ```ts type ArrayField = Vue.Component ``` ## 用例 ================================================ FILE: packages/vue/docs/api/components/expression-scope.md ================================================ --- order: 8 --- # ExpressionScope ## 描述 用于自定义组件内部给 json-schema 表达式传递局部作用域 ## 签名 ```ts interface IExpressionScopeProps { value?: any } type ExpressionScope = Vue.Component ``` ## 用例 ================================================ FILE: packages/vue/docs/api/components/field.md ================================================ --- order: 0 --- # Field ## 描述 作为@formily/core 的 [createField](https://core.formilyjs.org/api/models/form#createfield) Vue 实现,它是专门用于将 ViewModel 与输入控件做绑定的桥接组件,Field 组件属性参考[IFieldFactoryProps](https://core.formilyjs.org/api/models/form#ifieldfactoryprops) ::: warning 我们在使用 Field 组件的时候,一定要记得传 name 属性。 ::: ## 签名 ```ts type Field = Vue.Component ``` ## 用例 ================================================ FILE: packages/vue/docs/api/components/form-consumer.md ================================================ --- order: 7 --- # FormConsumer ## 描述 表单响应消费者,专门用于监听表单模型数据变化而实现各种 UI 响应的组件,使用方式为 scoped slot. 当回调函数内依赖的数据发生变化时就会重新渲染回调函数 Form 参考[Form](https://core.formilyjs.org/api/models/form) ## 用例 ================================================ FILE: packages/vue/docs/api/components/form-provider.md ================================================ --- order: 6 --- # FormProvider ## 描述 入口组件,用于下发表单上下文给字段组件,负责整个表单状态的通讯,它相当于是一个通讯枢纽。 ## 签名 ```ts type FormProvider = Vue.Component< any, any, any, { form: Form //通过createForm创建的form实例 } > ``` Form 参考[Form](https://core.formilyjs.org/api/models/form) ## 用例 ================================================ FILE: packages/vue/docs/api/components/object-field.md ================================================ --- order: 2 --- # ObjectField ## 描述 作为@formily/core 的 [createObjectField](https://core.formilyjs.org/api/models/form#createobjectfield) Vue 实现,它是专门用于将 ViewModel 与输入控件做绑定的桥接组件,ObjectField 组件属性参考[IFieldFactoryProps](https://core.formilyjs.org/api/models/form#ifieldfactoryprops) ::: warning 我们在使用 ObjectField 组件的时候,一定要记得传 name 属性。同时要使用 scoped slot 形式来组织子组件 ::: ## 签名 ```ts type ObjectField = Vue.Component ``` ## 用例 ================================================ FILE: packages/vue/docs/api/components/recursion-field-with-component.md ================================================ --- order: 5 --- # RecursionField (自增列表递归) ## 用例 使用[useField](/api/hooks/use-field)和[useFieldSchema](/api/shared/use-field-schema)来获取当前字段上下文中的字段实例和字段 schema ================================================ FILE: packages/vue/docs/api/components/recursion-field.md ================================================ --- order: 5 --- # RecursionField (简易递归) ## 描述 递归渲染组件,主要基于[JSON-Schema](/api/shared/schema)做递归渲染,它是[SchemaField](/api/components/schema-field)组件内部的核心渲染组件,当然,它是可以独立于 SchemaField 单独使用的,我们使用的时候主要是在自定义组件中使用,用于实现具有递归渲染能力的自定义组件 ## 签名 ```ts interface IRecursionFieldProps { schema: Schema //schema对象 name?: string //路径名称 basePath?: FormPathPattern //基础路径 onlyRenderProperties?: boolean //是否只渲染properties onlyRenderSelf?: boolean //是否只渲染自身,不渲染properties mapProperties?: (schema: Schema, name: string) => Schema //schema properties映射器,主要用于改写schema filterProperties?: (schema: Schema, name: string) => boolean //schema properties过滤器,被过滤掉的schema节点不会被渲染 } type RecursionField = Vue.Component ``` ## 用例 我们可以从组件属性中读取独立的 schema 对象,传给 RecursionField 渲染 ================================================ FILE: packages/vue/docs/api/components/schema-field-with-schema.md ================================================ --- order: 4 --- # SchemaField (JSON Schema) ## 描述 SchemaField 支持直接传入 [JSON-Schema](/api/shared/schema) 对象渲染表单。 ## 用例 ================================================ FILE: packages/vue/docs/api/components/schema-field.md ================================================ --- order: 4 --- # SchemaField (Markup Schema) ## 描述 SchemaField 组件是专门用于解析[JSON-Schema](/api/shared/schema)动态渲染表单的组件。 在使用 SchemaField 组件的时候,需要通过 createSchemaField 工厂函数创建一个 SchemaField 组件。 ## 签名 ```ts type ComposeSchemaField = { SchemaField: Vue.Component SchemaMarkupField: Vue.Component SchemaStringField: Vue.Component> SchemaObjectField: Vue.Component> SchemaArrayField: Vue.Component> SchemaBooleanField: Vue.Component> SchemaDateField: Vue.Component> SchemaDateTimeField: Vue.Component> SchemaVoidField: Vue.Component> SchemaNumberField: Vue.Component> } //工厂函数参数属性 interface ISchemaFieldFactoryProps { components?: { [key: string]: Vue.Component //组件列表 } scope?: any //全局作用域,用于实现协议表达式变量注入 } //SchemaField属性 interface ISchemaFieldProps extends IFieldFactoryProps { schema?: ISchema //字段schema scope?: any //协议表达式作用域 name?: string //字段名称 } //工厂函数 interface createSchemaField { (props: ISchemaFieldFactoryProps): ComposeSchemaField } ``` IFieldFactoryProps 参考 [IFieldFactoryProps](https://core.formilyjs.org/api/models/form#ifieldfactoryprops) ISchema 参考 [ISchema](/api/shared/schema#ischema) ## 用例 ================================================ FILE: packages/vue/docs/api/components/void-field.md ================================================ --- order: 3 --- # VoidField ## 描述 作为@formily/core 的 [createVoidField](https://core.formilyjs.org/api/models/form#createvoidfield) Vue 实现,它是专门用于将 ViewModel 与虚拟布局控件做绑定的桥接组件,可以用来控制数据型字段的显示隐藏,交互模式等,VoidField 组件属性参考[IVoidFieldFactoryProps](https://core.formilyjs.org/api/models/form#ivoidfieldfactoryprops) ::: warning 我们在使用 VoidField 组件的时候,一定要记得传 name 属性。 ::: ## 签名 ```ts type VoidField = Vue.Component ``` ## 用例 该例子演示了如何用 VoidField 控制子节点显示隐藏,注意观察,VoidField 隐藏的时候,子节点的数据会同时被清空,因为 visible 为 false 代表 display 为 none,这种隐藏是不会保留字段值的。 但是再次显示的时候,又会恢复现场,这里是 Formily Core 内部的特性,支持完全恢复现场的能力。 ================================================ FILE: packages/vue/docs/api/hooks/use-field-schema.md ================================================ # useFieldSchema ## 描述 主要在自定义组件中读取当前字段的 Schema 信息,该 hook 只能用在 SchemaField 或者 RecursionField 的子树中使用 ## 签名 ```ts interface useFieldSchema { (): Ref } ``` Schema 参考[Schema](/api/shared/schema) ## 用例 ================================================ FILE: packages/vue/docs/api/hooks/use-field.md ================================================ # useField ## 描述 主要用在自定义组件内读取当前字段属性,操作字段状态等,在所有 Field 组件的子树内都能使用,注意,拿到的是[GeneralField](https://core.formilyjs.org/api/models/field#generalfield),如果需要对不同类型的字段做处理,请使用[Type Checker](https://core.formilyjs.org/api/entry/form-checker) ::: warning 注意:如果要在自定义组件内使用 useField,并响应字段模型变化,需要使用 [observer](/api/shared/observer) 包裹自定义组件 ::: ## 签名 ```ts interface useField { (): Ref } ``` ## 用例 ================================================ FILE: packages/vue/docs/api/hooks/use-form-effects.md ================================================ # useFormEffects ## 描述 主要在自定义组件中往当前[Form](https://core.formilyjs.org/api/models/form)实例注入副作用逻辑,用于实现一些较为复杂的场景化组件 ## 签名 ```ts interface useFormEffects { (form: Form): void } ``` ## 用例 ================================================ FILE: packages/vue/docs/api/hooks/use-form.md ================================================ # useForm ## 描述 主要在自定义组件中读取当前[Form](https://core.formilyjs.org/api/models/form)实例,用于实现一些副作用依赖,比如依赖 Form 的 errors 信息之类的,用于实现一些较为复杂的场景化组件 ## 签名 ```ts interface useForm { (): Form } ``` ## 用例 ================================================ FILE: packages/vue/docs/api/hooks/use-parent-form.md ================================================ # useParentForm ## 描述 用于读取最近的 Form 或者 ObjectField 实例,主要方便于调用子表单的 submit/validate ## 签名 ```ts interface useParentForm { (): Form | ObjectField } ``` ## 用例 ================================================ FILE: packages/vue/docs/api/shared/connect.md ================================================ # connect ## 描述 主要用于对第三方组件库的无侵入接入 Formily ## 签名 ```ts interface IComponentMapper { (target: T): Vue.Component } interface connect { (target: T, ...args: IComponentMapper[]): Vue.Component } ``` 入参传入第一个参数是要接入的组件,后面的参数都是组件映射器,每个映射器都是一个函数,通常我们会使用内置的[mapProps](/api/shared/map-props)和[mapReadPretty](/api/shared/map-read-pretty)映射器 ## 用例 ================================================ FILE: packages/vue/docs/api/shared/injections.md ================================================ # injections ## 描述 @formily/vue 的所有 injections,方便用户做更复杂的个性化定制,我们可以通过 inject 来消费这些上下文 ## FormContext #### 描述 Form 上下文,可以获取当前 Form 实例 #### 签名 ```ts import { Form } from '@formily/core' const FormContext = inject(FormSymbol) ``` ## FieldContext #### 描述 字段上下文,可以获取当前字段实例 #### 签名 ```ts import { GeneralField } from '@formily/core' const FieldContext = inject(FieldSymbol) ``` ## SchemaMarkupContext #### 描述 Schema 标签上下文,主要用于收集 JSX Markup 写法的 Schema 标签,然后转换成标准 JSON Schema #### 签名 ```ts SchemaMarkupContext = inject(SchemaMarkupSymbol) ``` ## SchemaContext #### 描述 字段 Schema 上下文,主要用于获取当前字段的 Schema 信息 #### 签名 ```ts const SchemaContext = inject(SchemaSymbol) ``` ## SchemaExpressionScopeContext #### 描述 Schema 表达式作用域上下文 #### 签名 ```ts const SchemaExpressionScopeContext = inject(SchemaExpressionScopeSymbol) ``` ## SchemaOptionsContext #### 描述 Schema 全局参数上下文,主要用于获取从 createSchemaField 传入的参数 #### 签名 ```ts const SchemaOptionsContext = inject(SchemaOptionsSymbol) ``` ================================================ FILE: packages/vue/docs/api/shared/map-props.md ================================================ # mapProps ## 描述 将[Field](https://core.formilyjs.org/api/models/field)属性与组件属性映射的适配器函数,主要与 connect 函数搭配使用 ## 签名 ```ts import { Field, GeneralField } from '@formily/core' type IStateMapper = | { [key in keyof Field]?: keyof Props | boolean } | ((props: Props, field: GeneralField) => Props) interface mapProps { (...args: IStateMapper>[]): Vue.Component } ``` - 参数可以传对象(key 是 field 的属性,value 是组件的属性,如果 value 为 true,代表映射的属性名相同) - 参数可以传函数,函数可以直接对属性做更复杂的映射 ## 用例 ================================================ FILE: packages/vue/docs/api/shared/map-read-pretty.md ================================================ # mapReadPretty ## 描述 因为大多数第三方组件都不支持阅读态,如果想要快速支持阅读态的话,即可使用 mapReadPretty 函数来映射一个阅读态组件 ## 签名 ```ts interface mapReadPretty { (component: Vue.Component): Vue.Component } ``` ## 用例 ================================================ FILE: packages/vue/docs/api/shared/observer.md ================================================ # observer ## 描述 observer 是从 [@formily/reactive-vue](https://reactive.formilyjs.org/api/vue/observer) 中导出的,API 完全一致,使用 observer 主要是将组件支持响应式更新能力。 ## 用例 ================================================ FILE: packages/vue/docs/api/shared/schema.md ================================================ # Schema ## 描述 @formily/vue 协议驱动最核心的部分,Schema 在其中是一个通用 Class,用户可以自行使用,同时在 SchemaField 和 RecursionField 中都有依赖它,它主要有几个核心能力: - 解析 json-schema 的能力 - 将 json-schema 转换成 Field Model 的能力 - 编译 json-schema 表达式的能力 从@formily/vue 中可以导出 Schema 这个 Class,但是如果你不希望使用@formily/vue,你可以单独依赖@formily/json-schema 这个包 ## 构造器 ```ts class Schema { constructor(json: ISchema, parent?: ISchema) } ``` 基于一份 json schema 数据创建一棵 Schema Tree,保证每个 schema 节点都是包含对应方法的 ## 属性 | 属性 | 描述 | 类型 | 字段模型映射 | | -------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | type | 类型 | [SchemaTypes](#schematypes) | [GeneralField](https://core.formilyjs.org/api/models/field#generalfield) | | title | 标题 | React.ReactNode | `title` | | description | 描述 | React.ReactNode | `description` | | default | 默认值 | Any | `initialValue` | | readOnly | 是否只读 | Boolean | `readOnly` | | writeOnly | 是否只写 | Boolean | `editable` | | enum | 枚举 | [SchemaEnum](#schemaenum) | `dataSource` | | const | 校验字段值是否与 const 的值相等 | Any | `validator` | | multipleOf | 校验字段值是否可被 multipleOf 的值整除 | Number | `validator` | | maximum | 校验最大值(大于) | Number | `validator` | | exclusiveMaximum | 校验最大值(大于等于 | Number | `validator` | | minimum | 校验最小值(小于) | Number | `validator` | | exclusiveMinimum | 最小值(小于等于) | Number | `validator` | | maxLength | 校验最大长度 | Number | `validator` | | minLength | 校验最小长度 | Number | `validator` | | pattern | 正则校验规则 | RegExpString | `validator` | | maxItems | 最大条目数 | Number | `validator` | | minItems | 最小条目数 | Number | `validator` | | uniqueItems | 是否校验重复 | Boolean | `validator` | | maxProperties | 最大属性数量 | Number | `validator` | | minProperties | 最小属性数量 | Number | `validator` | | required | 必填 | Boolean | `validator` | | format | 正则校验格式 | [ValidatorFormats](https://core.formilyjs.org/api/models/field#fieldvalidator) | `validator` | | properties | 属性描述 | [SchemaProperties](#schemaproperties) | - | | items | 数组描述 | [SchemaItems](#schemaitems) | - | | additionalItems | 额外数组元素描述 | Schema | - | | patternProperties | 动态匹配对象的某个属性的 Schema | [SchemaProperties](#schemaproperties) | - | | additionalProperties | 匹配对象额外属性的 Schema | Schema | - | | x-index | UI 展示顺序 | Number | - | | x-pattern | UI 交互模式 | [FieldPatternTypes](https://core.formilyjs.org/api/models/field#fieldpatterntypes) | `pattern` | | x-display | UI 展示 | [FieldDisplayTypes](https://core.formilyjs.org/api/models/field#fielddisplaytypes) | `display` | | x-validator | 字段校验器 | [FieldValidator](https://core.formilyjs.org/api/models/field#fieldvalidator) | `validator` | | x-decorator | 字段 UI 包装器组件 | `String \| React.FC` | `decorator` | | x-decorator-props | 字段 UI 包装器组件属性 | Any | `decorator` | | x-component | 字段 UI 组件 | `String \| React.FC` | `component` | | x-component-props | 字段 UI 组件属性 | Any | `component` | | x-reactions | 字段联动协议 | [SchemaReactions](#schemareactions) | `reactions` | | x-content | 字段内容,用来传入某个组件的子节点 | React.ReactNode | ReactChildren | | x-visible | 字段显示隐藏 | Boolean | `visible` | | x-hidden | 字段 UI 隐藏(保留数据) | Boolean | `hidden` | | x-disabled | 字段禁用 | Boolean | `disabled` | | x-editable | 字段可编辑 | Boolean | `editable` | | x-read-only | 字段只读 | Boolean | `readOnly` | | x-read-pretty | 字段阅读态 | Boolean | `readPretty` | | definitions | Schema 预定义 | [SchemaProperties](#schemaproperties) | - | | $ref | 从 Schema 预定义中读取 Schema 并合并至当前 Schema | String | - | | x-data | 扩展属性 | Object | data | #### 详细说明 - x-component 的组件标识与[createSchemaField](/api/components/schema-field#签名)传入的组件集合的 Key 匹配 - x-decorator 的组件标识与[createSchemaField](/api/components/schema-field#签名)传入的组件集合的 Key 匹配 - Schema 的每个属性都能使用字符串表达式{{expression}},表达式变量可以从 createSchemaField 中传入,也可以从 SchemaField 组件中传入 - $ref 指定 Schema 预定义的格式必须是#/definitions/address这种格式,不支持加载远程 JSON Schema ## 方法 ### addProperty #### 描述 添加属性描述 #### 签名 ```ts interface addProperty { (key: string | number, schema: ISchema): Schema //返回添加后的Schema对象 } ``` ### removeProperty #### 描述 移除属性描述 #### 签名 ```ts interface removeProperty { (key: string | number): Schema //返回被移除的Schema对象 } ``` ### setProperties #### 描述 覆盖式更新属性描述 #### 签名 ```ts interface setProperties { (properties: SchemaProperties): Schema //返回当前Schema对象 } ``` SchemaProperties 参考 [SchemaProperties](#schemaproperties) ### addPatternProperty #### 描述 添加正则属性描述 #### 签名 ```ts interface addPatternProperty { (regexp: string, schema: ISchema): Schema //返回添加后的Schema对象 } ``` ### removePatternProperty #### 描述 移除正则属性描述 #### 签名 ```ts interface removePatternProperty { (regexp: string): Schema //返回移除后的Schema对象 } ``` ### setPatternProperties #### 描述 覆盖式更新正则属性描述 #### 签名 ```ts interface setPatternProperties { (properties: SchemaProperties): Schema //返回当前Schema对象 } ``` SchemaProperties 参考 [SchemaProperties](#schemaproperties) ### setAdditionalProperties #### 描述 覆盖式更新扩展属性描述 #### 签名 ```ts interface setAdditionalProperties { (properties: ISchema): Schema //返回扩展属性Schema对象 } ``` ### setItems #### 描述 覆盖式更新数组项描述 #### 签名 ```ts interface setItems { (items: SchemaItems): SchemaItems //返回更新后的SchemaItems对象 } ``` SchemaItems 参考 [SchemaItems](#schemaitems) ### setAdditionalItems #### 描述 覆盖式更新数组扩展项描述 #### 签名 ```ts interface setAdditionalItems { (items: ISchema): Schema //返回更新后的Schema对象 } ``` SchemaItems 参考 [SchemaItems](#schemaitems) ### mapProperties #### 描述 遍历并映射当前 Schema 的 properties 属性,同时会基于 x-index 顺序来遍历 #### 签名 ```ts interface mapProperties { (mapper: (property: Schema, key: string | number) => T): T[] } ``` ### mapPatternProperties #### 描述 遍历并映射当前 Schema 的 patternProperties 属性,同时会基于 x-index 顺序来遍历 #### 签名 ```ts interface mapPatternProperties { (mapper: (property: Schema, key: string | number) => T): T[] } ``` ### reduceProperties #### 描述 reduce 当前 Schema 的 properties 属性,同时会基于 x-index 顺序来遍历 #### 签名 ```ts interface reduceProperties { ( reducer: (value: T, property: Schema, key: string | number) => T, initialValue?: T ): T } ``` ### reducePatternProperties #### 描述 reduce 当前 Schema 的 patternProperties 属性,同时会基于 x-index 顺序来遍历 #### 签名 ```ts interface reducePatternProperties { ( reducer: (value: T, property: Schema, key: string | number) => T, initialValue?: T ): T } ``` ### compile #### 描述 深度递归当前 Schema 对象中的表达式片段,编译表达式,并返回 Schema,我们可以传入作用域对象,在表达式中即可消费作用域变量 表达式片段约定:以`{{`开头`}}`结尾的字符串代表一个表达式片段 #### 签名 ```ts interface compile { (scope: any): Schema } ``` ### fromJSON #### 描述 将普通 json 数据转换成 Schema 对象 #### 签名 ```ts interface fromJSON { (json: ISchema): Schema } ``` ### toJSON #### 描述 将当前 Schema 对象转换成普通 json 数据 #### 签名 ```ts interface toJSON { (): ISchema } ``` ### toFieldProps #### 描述 将当前 Schema 对象转换成 Formily 字段模型属性,映射关系参考 [属性](#属性) #### 签名 ```ts import { IFieldFactoryProps } from '@formily/core' interface toFieldProps { (): IFieldFactoryProps } ``` IFieldFactoryProps 参考 [IFieldFactoryProps](https://core.formilyjs.org/api/models/form#ifieldfactoryprops) ## 静态方法 ### getOrderProperties #### 描述 从 Schema 中获取排序后的 properties #### 签名 ```ts interface getOrderProperties { (schema: ISchema = {}, propertiesName: keyof ISchema = 'properties'): ISchema } ``` ### compile #### 描述 深度遍历任意对象中的表达式片段,表达式片段约定:以`{{`开头`}}`结尾的字符串代表一个表达式片段 #### 签名 ```ts interface compile { (target: any, scope: any): any } ``` ### shallowCompile #### 描述 浅层遍历任意对象中的表达式片段,表达式片段约定:以`{{`开头`}}`结尾的字符串代表一个表达式片段 #### 签名 ```ts interface shallowCompile { (target: any, scope: any): any } ``` ### silent #### 描述 是否静默编译,如果是,则表达式报错不会有任何提醒 #### 签名 ```ts interface silent { (value?: boolean): void } ``` ### isSchemaInstance #### 描述 判断某个对象是否为 Schema Class 的实例对象 #### 签名 ```ts interface isSchemaInstance { (target: any): target is Schema } ``` ### registerCompiler #### 描述 注册表达式编译器 #### 签名 ```ts interface registerCompiler { (compiler: (expression: string, scope: any) => any): void } ``` ### registerPatches #### 描述 注册 Schema 补丁,方便做不同版本的 Schema 协议兼容 #### 签名 ```ts type SchemaPatch = (schema: ISchema) => ISchema interface registerPatches { (...args: SchemaPatch[]): void } ``` ### registerVoidComponents #### 描述 给字段组件打上标识,标识该组件是虚拟组件,与 formily1.x 做兼容 #### 签名 ```ts interface registerVoidComponents { (components: string[]): void } ``` #### 用例 ```ts import { Schema } from '@formily/react' Schema.registerVoidComponents(['card', 'tab', 'step']) ``` ::: warning

注意,该 api 需要配合 enablePolyfills(['1.0']) 使用

::: ### registerTypeDefaultComponents #### 描述 给 Schema 类型标识默认组件类型 #### 签名 ```ts interface registerTypeDefaultComponents { (maps: Record): void } ``` #### 用例 ```ts import { Schema } from '@formily/vue' Schema.registerTypeDefaultComponents({ string: 'Input', number: 'NumberPicker', array: 'ArrayTable', }) ``` 注意,该 api 需要配合 enablePolyfills(['1.0']) 使用 ### registerPolyfills #### 描述 注册协议兼容垫片 #### 签名 ```ts type SchemaPatch = (schema: ISchema) => ISchema interface registerPolyfills { (version: string, patch: SchemaPatch): void } ``` #### 用例 ```ts import { Schema } from '@formily/react' Schema.registerPolyfills('1.0', (schema) => { schema['x-decorator'] = 'FormItem' return schema }) ``` ### enablePolyfills #### 描述 开启协议垫片,默认内置 1.0 版本协议兼容垫片,主要兼容特性: - x-decorator 不声明,自动作为 FormItem - x-linkages 转换为 x-reactions - x-props 自动转换为 x-decorator-props - x-rules 转换为 x-validator - editable 转换为 x-editable - visible 转换为 x-visible - x-component 为 card/block/grid-row/grid-col/grid/layout/step/tab/text-box 自动转换 VoidField, #### 签名 ```ts interface enablePolyfills { (versions: string[]): void } ``` #### 用例 ```ts import { Schema } from '@formily/vue' Schema.enablePolyfills(['1.0']) ``` ## 类型 ### ISchema #### 描述 ISchema 就是一份普通 JSON 数据,同时它是遵循 Schema [属性](#属性) 规范的 JSON 数据 ### SchemaTypes #### 描述 Schema 描述的类型 #### 签名 ```ts type SchemaTypes = | 'string' | 'object' | 'array' | 'number' | 'boolean' | 'void' | 'date' | 'datetime' | (string & {}) ``` ### SchemaProperties #### 描述 Schema 属性描述 #### 签名 ```ts type SchemaProperties = Record ``` ### SchemaItems #### 描述 Schema 数组项描述 #### 签名 ```ts type SchemaItems = ISchema | ISchema[] ``` ### SchemaEnum #### 描述 Schema 枚举 #### 签名 ```ts type SchemaEnum = Array< | string | number | { label: Message; value: any; [key: string]: any } | { key: any; title: Message; [key: string]: any } > ``` ### SchemaReactions #### 描述 Schema 联动协议,如果 reaction 对象里包含 target,则代表主动联动模式,否则代表被动联动模式 如果想实现更复杂的联动,可以通过作用域传入 reaction 响应器函数进行处理 FormPathPattern 路径语法文档看[这里](https://core.formilyjs.org/zh-CN/api/entry/form-path#formpathpattern) #### 签名 ```ts import { IGeneralFieldState } from '@formily/core' type SchemaReactionEffect = | 'onFieldInit' | 'onFieldMount' | 'onFieldUnmount' | 'onFieldValueChange' | 'onFieldInputValueChange' | 'onFieldInitialValueChange' | 'onFieldValidateStart' | 'onFieldValidateEnd' | 'onFieldValidateFailed' | 'onFieldValidateSuccess' type SchemaReaction = | { dependencies?: //依赖的字段路径列表,支持FormPathPattern数据路径语法, 只能以点路径描述依赖,支持相对路径 | Array< | string //如果数组里是string,那么读的时候也是数组格式 | { //如果数组里是对象, 那么读的时候通过name从$deps获取 name?: string //从$deps读取时的别名 type?: string //字段类型 source?: string //字段路径 property?: string //依赖属性, 默认为value } > | Record //如果是对象格式,读的时候也是对象格式,只是对象的key相当于别名 when?: string | boolean //联动条件 target?: string //要操作的字段路径,支持FormPathPattern匹配路径语法,注意:不支持相对路径!! effects?: SchemaReactionEffect[] //主动模式下的独立生命周期钩子 fulfill?: { //满足条件 state?: IGeneralFieldState //更新状态 schema?: ISchema //更新Schema run?: string //执行语句 } otherwise?: { //不满足条件 state?: IGeneralFieldState //更新状态 schema?: ISchema //更新Schema run?: string //执行语句 } } | ((field: Field) => void) //支持函数, 可以复杂联动 type SchemaReactions = | SchemaReaction | SchemaReaction[] //支持传入数组 ``` #### 用例 **主动联动** 写法一,标准主动联动 ```json { "type": "object", "properties": { "source": { "type": "string", "x-component": "Input", "x-reactions": { "target": "target", "when": "{{$self.value === '123'}}", "fulfill": { "state": { "visible": false } }, "otherwise": { "state": { "visible": true } } } }, "target": { "type": "string", "x-component": "Input" } } } ``` 写法二,局部表达式分发联动 ```json { "type": "object", "properties": { "source": { "type": "string", "x-component": "Input", "x-reactions": { "target": "target", "fulfill": { "state": { "visible": "{{$self.value === '123'}}" //任意层次属性都支持表达式 } } } }, "target": { "type": "string", "x-component": "Input" } } } ``` 写法三,相邻元素联动 ```json { "type": "array", "x-component": "ArrayTable", "items": { "type": "object", "properties": { "source": { "type": "string", "x-component": "Input", "x-reactions": { "target": ".target", "fulfill": { "state": { "visible": "{{$self.value === '123'}}" //任意层次属性都支持表达式 } } } }, "target": { "type": "string", "x-component": "Input" } } } } ``` 写法四,基于 Schema 协议联动 ```json { "type": "object", "properties": { "source": { "type": "string", "x-component": "Input", "x-reactions": { "target": "target", "fulfill": { "schema": { "x-visible": "{{$self.value === '123'}}" //任意层次属性都支持表达式 } } } }, "target": { "type": "string", "x-component": "Input" } } } ``` 写法五,基于 run 语句联动 ```json { "type": "object", "properties": { "source": { "type": "string", "x-component": "Input", "x-reactions": { "fulfill": { "run": "$form.setFieldState('target',state=>{state.visible = $self.value === '123'})" } } }, "target": { "type": "string", "x-component": "Input" } } } ``` 写法六,基于生命周期钩子联动 ```json { "type": "object", "properties": { "source": { "type": "string", "x-component": "Input", "x-reactions": { "target": "target", "effects": ["onFieldInputValueChange"], "fulfill": { "state": { "visible": "{{$self.value === '123'}}" //任意层次属性都支持表达式 } } } }, "target": { "type": "string", "x-component": "Input" } } } ``` **被动联动** ```json { "type": "object", "properties": { "source": { "type": "string", "x-component": "Input" }, "target": { "type": "string", "x-component": "Input", "x-reactions": { "dependencies": ["source"], //依赖路径写法默认是取value,如果依赖的是字段的其他属性,可以使用 source#modified,用#分割取详细属性 // "dependencies":{ aliasName:"source" }, //别名形式 "fulfill": { "schema": { "x-visible": "{{$deps[0] === '123'}}" //任意层次属性都支持表达式 } } } } } } ``` **复杂联动** ```json { "type": "object", "properties": { "source": { "type": "string", "x-component": "Input" }, "target": { "type": "string", "x-component": "Input", "x-reactions": "{{myReaction}}" //外部传入的函数,在函数内可以实现更复杂的联动 } } } ``` **组件属性联动** 写法一,操作状态 ```json { "type": "object", "properties": { "source": { "type": "string", "x-component": "Input", "x-reactions": { "target": "target", "fulfill": { "state": { "component[1].style.color": "{{$self.value === '123' ? 'red' : 'blue'}}" //任意层次属性都支持表达式,同时key是支持路径表达式的,可以实现精确操作属性 } } } }, "target": { "type": "string", "x-component": "Input" } } } ``` 写法二,操作 Schema 协议 ```json { "type": "object", "properties": { "source": { "type": "string", "x-component": "Input", "x-reactions": { "target": "target", "fulfill": { "schema": { "x-component-props.style.color": "{{$self.value === '123' ? 'red' : 'blue'}}" //任意层次属性都支持表达式,同时key是支持路径表达式的,可以实现精确操作属性 } } } }, "target": { "type": "string", "x-component": "Input" } } } ``` ## 内置表达式作用域 内置表达式作用域主要用于在表达式中实现各种联动关系 ### $self 代表当前字段实例,可以在普通属性表达式中使用,也能在 x-reactions 中使用 ### $values 代表顶层表单数据,可以在普通属性表达式中使用,也能在 x-reactions 中使用 ### $form 代表当前 Form 实例,可以在普通属性表达式中使用,也能在 x-reactions 中使用 ### $observable 用于创建响应式对象,使用方式与 observable 一致 ### $memo 用于创建持久引用数据,使用方式与 autorun.memo 一致 ### $effect 用于响应 autorun 第一次执行的下一个微任务时机与响应 autorun 的 dispose,使用方式与 autorun.effect 一致 ### $dependencies 只能在 x-reactions 中的表达式消费,与 x-reactions 定义的 dependencies 对应,数组顺序一致 ### $deps 只能在 x-reactions 中的表达式消费,与 x-reactions 定义的 dependencies 对应,数组顺序一致 ### $target 只能在 x-reactions 中的表达式消费,代表主动模式的 target 字段 ================================================ FILE: packages/vue/docs/demos/api/components/array-field.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/components/expression-scope.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/components/field.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/components/form-consumer.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/components/form-provider.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/components/object-field.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/components/recursion-field-with-component.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/components/recursion-field.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/components/schema-field-with-schema.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/components/schema-field.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/components/void-field.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/hooks/use-field-schema.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/hooks/use-field.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/hooks/use-form-effects.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/hooks/use-form.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/hooks/use-parent-form.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/shared/connect.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/shared/map-props.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/shared/map-read-pretty.vue ================================================ ================================================ FILE: packages/vue/docs/demos/api/shared/observer.vue ================================================ ================================================ FILE: packages/vue/docs/demos/index.vue ================================================ ================================================ FILE: packages/vue/docs/demos/questions/default-slot.vue ================================================ ================================================ FILE: packages/vue/docs/demos/questions/events.vue ================================================ ================================================ FILE: packages/vue/docs/demos/questions/named-slot.vue ================================================ ================================================ FILE: packages/vue/docs/demos/questions/scoped-slot.vue ================================================ ================================================ FILE: packages/vue/docs/guide/README.md ================================================ # 介绍 @formily/vue 的核心定位是将 ViewModel([@formily/core](//core.formilyjs.org))与组件实现一个状态绑定关系,它不负责管理表单数据,表单校验,它仅仅是一个渲染胶水层,但是这样一层胶水,并不脏,它会把很多脏逻辑优雅的解耦,变得可维护。 ## 超高性能 借助 [@formily/core](//core.formilyjs.org) 的响应式模型,@formily/vue 无需做任何优化即可获得超高的性能优势,依赖追踪,精确更新,按需渲染,让我们的表单真正做到了只需关注业务逻辑,无需考虑性能问题。 ## 开箱即用 @formily/vue 提供了一系列的 Vue 组件,比如 Field/ArrayField/ObjectField/VoidField,用户在使用的时候,只需要给 Field 组件传入 component 属性(支持 value/@change 这样的双向绑定约定)即可快速接入@formily/vue,接入成本极低。 ## 协议驱动 @formily/vue 提供了 SchemaField 这样的协议驱动组件,同时是基于标准 JSON-Schema 的驱动,让表单开发可以变得更加动态化,可配置化,更甚,我们可以做到一份协议,让多端渲染表单。 ## 场景复用 借助协议驱动的能力,我们可以将一个携带业务逻辑的协议片段抽象成一个场景组件,帮助用户在某些场景上高效开发,比如 FormTab、FormStep 这类场景组件。 ## 智能提示 因为 formily 是完全的 Typescript 项目,所以用户在 VSCode 或 WebStorm 等上开发可以获得最大化的智能提示体验 ![img](https://img.alicdn.com/imgextra/i2/O1CN01yiREHk1X95KJPPz1c_!!6000000002880-2-tps-2014-868.png) ## 状态可观测 安装 [FormilyDevtools](https://chrome.google.com/webstore/detail/formily-devtools/kkocalmbfnplecdmbadaapgapdioecfm?hl=zh-CN) 可以实时观测模型状态变化,排查问题 ![img](https://img.alicdn.com/imgextra/i4/O1CN01DSci5h1rAGfRafpXw_!!6000000005590-2-tps-2882-1642.png) ================================================ FILE: packages/vue/docs/guide/architecture.md ================================================ # 核心架构 @formily/vue 的架构相比于@formily/core 并不复杂,先看架构图: ![](https://img.alicdn.com/imgextra/i1/O1CN013jbRfk1l5n6N7jYH8_!!6000000004768-55-tps-2200-1637.svg) 从这张架构图中我们可以看到,@formily/vue 支持了两类用户,一类就是纯源码开发用户,它们只需要使用 Field/ArrayField/ObjectField/VoidField 组件。另一类就是基于 JSON-Schema 做动态开发的用户,它们依赖的主要是 SchemaField 组件,但是,这两类用户都需要使用一个 FormProvider 的组件来统一下发上下文。然后是 SchemaField 组件,它内部其实是依赖的 Field/ArrayField/ObjectField/VoidField 组件。 ================================================ FILE: packages/vue/docs/guide/concept.md ================================================ # 核心概念 @formily/vue 本身架构不复杂,因为它只是提供了一系列的组件和 Hooks 给用户使用,但是我们还是需要理解以下几个概念: - 表单上下文 - 字段上下文 - 协议上下文 - 模型绑定 - 协议驱动 - 三种开发模式 ## 表单上下文 从[架构图](/guide/architecture)中我们可以看到 FormProvider 是作为表单统一上下文而存在,它的地位非常重要,主要用于将@formily/core 创建出来的[Form](//core.formilyjs.org/api/models/form)实例下发到所有子组件中,不管是在内置组件还是用户扩展的组件,都能通过[useForm](/api/hooks/use-form)读取到[Form](//core.formilyjs.org/api/models/form)实例 ## 字段上下文 从[架构图](/guide/architecture)中我们可以看到不管是 Field/ArrayField/ObjectField/VoidField,会给子树下发一个 FieldContext,我们可以在自定义组件中读取到当前字段模型,主要是使用[useField](/api/hooks/use-field)来读取,这样非常方便于做模型映射 ## 协议上下文 从[架构图](/guide/architecture)中我们可以看到[RecursionField](/api/components/recursion-field)会给子树下发一个 FieldSchemaContext,我们可以在自定义组件中读取到当前字段的 Schema 描述,主要是使用[useFieldSchema](/api/hooks/use-field-schema)来读取。注意,该 Hook 只能用在[SchemaField](/api/components/schema-field)和[RecursionField](/api/components/recursion-field)子树中使用 ## 模型绑定 想要理解模型绑定,需要先理解什么是[MVVM](//core.formilyjs.org/guide/mvvm),理解了之后我们再看看这张图: ![](https://img.alicdn.com/imgextra/i1/O1CN01A03C191KwT1raxnDg_!!6000000001228-55-tps-2200-869.svg) 在 Formily 中,@formily/core 就是 ViewModel,Component 和 Decorator 就是 View,@formily/vue 就是将 ViewModel 和 View 绑定起来的胶水层,ViewModel 和 View 的绑定就叫做模型绑定,实现模型绑定的手段主要有[useField](/api/hooks/use-field),也能使用[connect](/api/shared/connect)和[mapProps](/api/shared/map-props),需要注意的是,Component 只需要支持 value/onChange 属性即可自动实现数据层的双向绑定。 ## 协议驱动 协议驱动渲染算是 @formily/vue 中学习成本最高的部分了,但是学会了之后,它给业务带来的收益也是很高,总共需要理解 4 个核心概念: - Schema - 递归渲染 - 协议绑定 - 三种开发模式 ### Schema formily 的协议驱动主要是基于标准 JSON Schema 来进行驱动渲染的,同时我们在标准之上又扩展了一些`x-*`属性来表达 UI,使得整个协议可以具备完整描述一个复杂表单的能力,具体 Schema 协议,参考[Schema](/api/shared/schema) API 文档 ### 递归渲染 何为递归渲染?递归渲染就是组件 A 在某些条件下会继续用组件 A 来渲染内容,看看以下伪代码: ```json { <---- RecursionField(条件:object;渲染权:RecursionField) "type":"object", "properties":{ "username":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input" }, "phone":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input", "x-validator":"phone" }, "email":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input", "x-validator":"email" }, "contacts":{ <---- RecursionField(条件:array;渲染权:RecursionField) "type":"array", "x-component":"ArrayTable", "items":{ <---- RecursionField(条件:object;渲染权:ArrayTable组件) "type":"object", "properties":{ "username":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input" }, "phone":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input", "x-validator":"phone" }, "email":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input", "x-validator":"email" }, } } } } } ``` @formily/vue 递归渲染的入口是[SchemaField](/api/components/schema-field),但它内部实际是使用 [RecursionField](/api/components/recursion-field) 来渲染的,因为 JSON-Schema 就是一个递归型结构,所以 [RecursionField](/api/components/recursion-field) 在渲染的时候会从顶层 Schema 节点解析,如果是非 object 和 array 类型则直接渲染具体组件,如果是 object,则会遍历 properties 继续用 [RecursionField](/api/components/recursion-field) 渲染子级 Schema 节点。 这里有点特殊的情况是 array 类型的自增列表渲染,需要用户在自定义组件内使用[RecursionField](/api/components/recursion-field)进行递归渲染,因为自增列表的 UI 个性化定制程度很高,所以就把递归渲染权交给用户来渲染了,这样设计也能让协议驱动渲染变得更加灵活。 那 SchemaField 和 RecursionField 有啥差别呢?主要有两点: - SchemaField 是支持 Markup 语法的,它会提前解析 Markup 语法生成[JSON Schema](/api/shared/schema)移交给 RecursionField 渲染,所以 RecursionField 只能基于 [JSON Schema](/api/shared/schema) 渲染 - SchemaField 渲染的是整体的 Schema 协议,而 RecursionField 渲染的是局部 Schema 协议 ### 协议绑定 前面讲了模型绑定,而协议绑定则是将 Schema 协议转换成模型绑定的过程,因为 JSON-Schema 协议是 JSON 字符串,可离线存储的,而模型绑定则是内存间的绑定关系,是 Runtime 层的,比如`x-component`在 Schema 中是组件的字符串标识,但是在模型中的 component 则是需要组件引用,所以 JSON 字符串与 Runtime 层是需要转换的。然后我们就可以继续完善一下以上模型绑定的图: ![](https://img.alicdn.com/imgextra/i3/O1CN01jLCRxH1aa3V0x6nw4_!!6000000003345-55-tps-2200-1147.svg) 总结下来,在 @formily/vue 中,主要有 2 层绑定关系,Schema 绑定模型,模型绑定组件,实现绑定的胶水层就是 @formily/vue,需要注意的是,Schema 绑定字段模型之后,字段模型中是感知不到 Schema 的,比如要修改`enum`,就是修改字段模型中的`dataSource`属性了,总之,想要更新字段模型,参考[Field](//core.formilyjs.org/models/field),想要理解 Schema 与字段模型的映射关系可以参考[Schema](/api/shared/schema)文档 ## 三种开发模式 从[架构图](/guide/architecture)中我们可以看到整个 @formily/vue 是有三种开发模式的,对应不同用户: - Template 开发模式 - JSON Schema 开发模式 - Markup Schema 开发模式 #### Template 开发模式 该模式主要是使用 Field/ArrayField/ObjectField/VoidField 组件 ```html ``` #### JSON Schema 开发模式 该模式是给 SchemaField 的 schema 属性传递 JSON Schema 即可 ```html ``` #### Markup Schema 开发模式 该模式算是一个对源码开发比较友好的 Schema 开发模式,同样是使用 SchemaField 相关组件。 Markup Schema 模式主要有以下几个特点: - 主要依赖 SchemaStringField/SchemaArrayField/SchemaObjectField...这类描述标签来表达 Schema - 每个描述标签都代表一个 Schema 节点,与 JSON-Schema 等价 - SchemaField 子节点不能随意插 UI 元素,因为 SchemaField 只会解析子节点的所有 Schema 描述标签,然后转换成 JSON Schema,最终交给[RecursionField](/api/components/recursion-field)渲染,如果想要插入 UI 元素,可以在 SchemaVoidField 上传`x-content`属性来插入 UI 元素 ```html ``` ================================================ FILE: packages/vue/docs/questions/README.md ================================================ --- sidebar: auto --- # 常见问题 ## 如何添加事件? `x-component-props` 中可以用 `@` 来标识事件,同时也支持 `onXxx` 这种方式来标识事件。两者区别在于使用 `@` 标识的内容不会再作为 prop 传入组件,而 `onXxx` 这种会。这是为了兼容某些组件具有 `onXxx` 的 prop,如 ElementUI 中的 [upload 组件](https://element.eleme.cn/#/zh-CN/component/upload#attribute)。 ::: warning 事件名冲突时,`@` 的优先级更高。例如同时设置了 `@change` 和 `onChange`,只有 `@change` 会生效。 ::: ## 如何使用插槽? 使用 `x-content` 可以在组件的 `default` 插槽中插入内容。可以传入文本或组件。 ## 如何使用具名插槽? `x-content` 中以键名来表示插槽名。 ::: danger 注意键名不可包含 `template`、`render`、`setup` 三个关键字,否则整个 `x-content` 会被当做 vue 组件进行渲染。 ::: ## 如何使用作用域插槽? `x-content` 使用函数式组件时, 渲染函数增加第二个参数,通过其 `props` 成员访问作用域插槽传入属性,支持 observer() 和 connect() 接入组件。 ================================================ FILE: packages/vue/package.json ================================================ { "name": "@formily/vue", "version": "2.3.7", "license": "MIT", "main": "lib", "module": "esm", "umd:main": "dist/formily.vue.umd.production.js", "unpkg": "dist/formily.vue.umd.production.js", "jsdelivr": "dist/formily.vue.umd.production.js", "jsnext:main": "esm", "types": "type-artefacts/cur/index.d.ts", "engines": { "npm": ">=3.0.0" }, "scripts": { "postinstall": "node ./scripts/postinstall.js", "start": "vuepress dev docs", "build": "rimraf -rf lib esm dist type-artefacts && npm run build:cjs && npm run build:esm && npm run build:umd && npm run build:types", "build:cjs": "tsc --project tsconfig.build.json", "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm", "build:umd": "rollup --config", "build:types": "npm run build:types-vue2 && npm run build:types-vue3 && rimraf type-artefacts/**/*.js type-artefacts/**/**/*.js", "build:types-vue2": "tsc --project tsconfig.types.json --outDir type-artefacts/v2", "build:types-vue3": "npx vue-demi-switch 3 vue3 && tsc --project tsconfig.types.json --outDir type-artefacts/v3 && tsc --project tsconfig.types.json --outDir type-artefacts/cur && npx vue-demi-switch 2", "build:docs": "vuepress build docs" }, "bin": { "formily-vue-fix": "bin/formily-vue-fix.js", "formily-vue-switch": "bin/formily-vue-switch.js" }, "devDependencies": { "@ant-design/icons": "^2.1.1", "@ant-design/icons-vue": "^2.0.0", "@vue/composition-api": "^1.0.0-rc.7", "@vuepress-dumi/vuepress-plugin-dumi-previewer": "0.3.3", "@vuepress-dumi/vuepress-theme-dumi": "0.3.3", "@vuepress/plugin-back-to-top": "^1.8.2", "@vuepress/plugin-medium-zoom": "^1.8.2", "ant-design-vue": "^1.7.3", "codesandbox": "^2.2.3", "core-js": "^2.4.0", "vue": "^2.6.12", "vue3": "npm:vue@3", "vuepress": "^1.8.2", "vuepress-plugin-typescript": "^0.3.1" }, "dependencies": { "@formily/core": "2.3.7", "@formily/json-schema": "2.3.7", "@formily/reactive": "2.3.7", "@formily/reactive-vue": "2.3.7", "@formily/shared": "2.3.7", "@formily/validator": "2.3.7", "fs-extra": "^10.0.0", "vue-demi": ">=0.13.6", "vue-frag": "^1.1.4" }, "peerDependencies": { "@vue/composition-api": "^1.0.0-beta.1", "vue": "^2.6.0 || >=3.0.0-rc.0" }, "peerDependenciesMeta": { "@vue/composition-api": { "optional": true } }, "publishConfig": { "access": "public" }, "gitHead": "ac79c196ae9324889aca5e0501146f9b37b04283" } ================================================ FILE: packages/vue/rollup.config.js ================================================ import baseConfig from '../../scripts/rollup.base' export default baseConfig('formily.vue', 'Formily.Vue') ================================================ FILE: packages/vue/scripts/postinstall.js ================================================ const { switchVersion, loadModule } = require('./utils.js') const Vue = loadModule('vue') try { if (Vue.version.startsWith('2.')) { switchVersion(2) } else if (Vue.version.startsWith('3.')) { switchVersion(3) } } catch (err) { // nothing to do } ================================================ FILE: packages/vue/scripts/switch-cli.js ================================================ const { switchVersion } = require('./utils.js') const { exec } = require('child_process') const version = process.argv[2] const vueEntry = process.argv[3] || 'vue' if (version == '2') { switchVersion(2) console.log(`[formily-vue] Switched types for Vue 2`) exec(`npx vue-demi-switch 2 ${vueEntry}`) console.log(`[vue-demi] Switched for Vue 2 (entry: "${vueEntry}")`) } else if (version == '3') { switchVersion(3) console.log(`[formily-vue] Switched types for Vue 3`) exec(`npx vue-demi-switch 3 ${vueEntry}`) console.log(`[vue-demi] Switched for Vue 3 (entry: "${vueEntry}")`) } else { console.warn( `[formily-vue] expecting version "2" or "3" but got "${version}"` ) process.exit(1) } ================================================ FILE: packages/vue/scripts/utils.js ================================================ const fs = require('fs-extra') const path = require('path') const dir = path.resolve(__dirname, '..', 'type-artefacts') function switchVersion(version) { fs.emptyDirSync(`${dir}/cur`) fs.copySync(`${dir}/v${version}`, `${dir}/cur`) } function loadModule(name) { try { return require(name) } catch (e) { return undefined } } module.exports.loadModule = loadModule module.exports.switchVersion = switchVersion ================================================ FILE: packages/vue/src/__tests__/expression.scope.spec.ts ================================================ import { render } from '@testing-library/vue' import { createForm } from '@formily/core' import { FormProvider, ExpressionScope, createSchemaField, h } from '..' import { defineComponent } from '@vue/composition-api' test('expression scope', async () => { const Container = defineComponent({ setup(_props, { slots }) { return () => h( ExpressionScope, { props: { value: { $innerScope: 'inner scope value' } }, }, slots ) }, }) const Input = defineComponent({ props: ['text'], setup(props) { return () => h( 'div', { attrs: { 'data-testid': 'test-input' } }, { default: () => props.text } ) }, }) const SchemaField = createSchemaField({ components: { Container, Input }, }) const form = createForm() const { getByTestId } = render({ components: { ...SchemaField, FormProvider }, data() { return { form } }, template: ` `, }) expect(getByTestId('test-input').textContent).toBe( 'inner scope valueouter scope value' ) }) ================================================ FILE: packages/vue/src/__tests__/field.spec.ts ================================================ import Vue, { FunctionalComponentOptions } from 'vue' import { render, fireEvent, waitFor } from '@testing-library/vue' import { defineComponent, h, ref } from '@vue/composition-api' import { createForm, Field as FieldType, isField, isVoidField, onFieldChange, } from '@formily/core' import { useField, useFormEffects, connect, mapProps, mapReadPretty } from '../' import { FormProvider, ArrayField, ObjectField, VoidField, Field, } from '../vue2-components' import ReactiveField from '../components/ReactiveField' // import { expectThrowError } from './shared' Vue.component('FormProvider', FormProvider) Vue.component('ArrayField', ArrayField) Vue.component('ObjectField', ObjectField) Vue.component('VoidField', VoidField) Vue.component('Field', Field) Vue.component('ReactiveField', ReactiveField as unknown as Vue) const Decorator = defineComponent({ props: ['label'], render(h) { return h( 'div', { attrs: this.$attrs, }, [this.label, this.$slots.default] ) }, }) const Input = defineComponent({ props: ['value'], setup(props, { attrs, listeners }) { const fieldRef = useField() return () => { const field = fieldRef.value return h('input', { class: 'test-input', attrs: { ...attrs, value: props.value, 'data-testid': field.path.toString(), }, on: { ...listeners, input: listeners.change, }, }) } }, }) const Normal: FunctionalComponentOptions = { functional: true, render(h) { return h('div') }, } test('render field', async () => { const form = createForm() const onChange = jest.fn() const atChange = jest.fn() const atBlur = jest.fn() const atFocus = jest.fn() const { getByTestId, queryByTestId, queryByText } = render( defineComponent({ name: 'TestComponent', setup() { return { form, Normal, Input, Decorator, onChange, atChange, atFocus, atBlur, } }, template: `
`, }) ) expect(form.mounted).toBeTruthy() expect(form.query('aa').take().mounted).toBeTruthy() expect(form.query('bb').take().mounted).toBeTruthy() expect(form.query('cc').take().mounted).toBeTruthy() expect(form.query('dd').take().mounted).toBeTruthy() await fireEvent.update(getByTestId('aa'), '123') await fireEvent.update(getByTestId('kk'), '123') await fireEvent.focus(getByTestId('ll')) await fireEvent.blur(getByTestId('ll')) await fireEvent.update(getByTestId('ll'), '123') expect(onChange).toBeCalledTimes(1) expect(atChange).toBeCalledTimes(1) expect(atFocus).toBeCalledTimes(1) expect(atBlur).toBeCalledTimes(1) expect(getByTestId('bb-children')).not.toBeUndefined() expect(getByTestId('dd-children')).not.toBeUndefined() expect(queryByTestId('ee')).toBeNull() expect(form.query('aa').get('value')).toEqual('123') expect(form.query('kk').get('value')).toEqual('123') expect(getByTestId('mm-children')).not.toBeUndefined() expect(queryByText('aa-decorator')).not.toBeNull() }) const InputWithSlot = defineComponent({ props: ['value'], setup(props, { attrs, listeners, slots }) { const fieldRef = useField() return () => { const field = fieldRef.value return h('div', {}, [ h('input', { class: 'test-input', attrs: { ...attrs, value: props.value, 'data-testid': field.path.toString(), }, on: { ...listeners, input: listeners.change, }, }), [slots['append']?.({ path: field.path.toString() })], ]) } }, }) test('render in nesting slots with (ObjectField/ArrayField) no decorator', async () => { const form = createForm() const { getByTestId } = render( defineComponent({ name: 'TestComponent', setup() { return { form, Normal, InputWithSlot, Decorator, } }, template: ` `, }) ) expect(getByTestId('oo')).not.toBeUndefined() expect(getByTestId('cc.mm')).not.toBeUndefined() expect(getByTestId('slot-prop-cc.mm')).not.toBeUndefined() }) test('render field with html attrs', async () => { const form = createForm() const { getByTestId, container } = render( defineComponent({ name: 'TestComponent', setup() { return { form, Input, Decorator, } }, template: ` `, }) ) expect(form.mounted).toBeTruthy() expect(form.query('aa').take().mounted).toBeTruthy() expect(getByTestId('aa').className.indexOf('test-input') !== -1).toBeTruthy() expect(getByTestId('aa').className.indexOf('test-class') !== -1).toBeTruthy() expect(getByTestId('aa').style.marginLeft).toEqual('10px') expect( getByTestId('decorator').className.indexOf('test-class') !== -1 ).toBeTruthy() expect(getByTestId('decorator').style.marginRight).toEqual('10px') }) test('ReactiveField', () => { render({ template: ``, }) render({ template: `
`, }) }) test('useAttch', async () => { const form1 = createForm() const MyComponent = defineComponent({ props: ['form', 'name1', 'name2', 'name3', 'name4'], data() { return { Input, Decorator } }, template: ` `, }) const { updateProps } = render(MyComponent, { props: { form: form1, name1: 'aa', name2: 'bb', name3: 'cc', name4: 'dd', }, }) expect(form1.mounted).toBeTruthy() expect(form1.query('aa').take().mounted).toBeTruthy() expect(form1.query('bb').take().mounted).toBeTruthy() expect(form1.query('cc').take().mounted).toBeTruthy() expect(form1.query('dd').take().mounted).toBeTruthy() await updateProps({ name1: 'aaa', name2: 'bbb', name3: 'ccc', name4: 'ddd', }) await Vue.nextTick() expect(form1.query('aa').take().mounted).toBeFalsy() expect(form1.query('bb').take().mounted).toBeFalsy() expect(form1.query('cc').take().mounted).toBeFalsy() expect(form1.query('dd').take().mounted).toBeFalsy() expect(form1.query('aaa').take().mounted).toBeTruthy() expect(form1.query('bbb').take().mounted).toBeTruthy() expect(form1.query('ccc').take().mounted).toBeTruthy() expect(form1.query('ddd').take().mounted).toBeTruthy() const form2 = createForm() await updateProps({ form: form2, }) await Vue.nextTick() expect(form1.unmounted).toBeTruthy() expect(form2.mounted).toBeTruthy() }) test('useFormEffects', async () => { const form = createForm() const CustomField = defineComponent({ props: ['value'], setup(props) { const fieldRef = useField() useFormEffects(() => { onFieldChange('aa', ['value'], (target) => { if (isVoidField(target)) return fieldRef.value.setValue(target.value) }) }) return () => { return h('div', { attrs: { 'data-testid': 'custom-value' } }, [ props.value, ]) } }, }) const { queryByTestId } = render({ data() { return { form, Decorator, Input, CustomField } }, template: ` `, }) expect(queryByTestId('custom-value').textContent).toEqual('') form.query('aa').take((aa) => { if (isField(aa)) { const value = '123' as any aa.setValue(value) } }) await waitFor(() => { expect(queryByTestId('custom-value').textContent).toEqual('123') }) }) test('useFormEffects: should be reregister when formRef change', async () => { const CustomField = defineComponent({ setup() { const reactiveText = ref() useFormEffects(() => { onFieldChange('aa', ['value'], (target) => { if (isVoidField(target)) return reactiveText.value = target.value }) }) return () => h('div', { attrs: { 'data-testid': 'custom-value' } }, [ reactiveText.value, ]) }, }) const { queryByTestId } = render({ setup() { const formRef = ref(createForm()) return { formRef, Input, CustomField, changeForm() { // form change formRef.value = createForm() formRef.value.setValues({ aa: 'text' }) }, } }, template: ` `, }) expect(queryByTestId('custom-value').textContent).toEqual('') queryByTestId('btn').click() await waitFor(() => { expect(queryByTestId('custom-value').textContent).toEqual('text') }) }) test('connect', async () => { const CustomField = connect( { functional: true, props: ['list'], render(h, context) { return h('div', [context.props.list]) }, }, mapProps({ value: 'list', loading: true }, (props, field) => { return { ...props, mounted: field.mounted ? 1 : 2, } }), mapReadPretty({ render(h) { return h('div', 'read pretty') }, }) ) const BaseComponent = { functional: true, name: 'BaseComponent', render(h, context) { return h('div', [context.props.value]) }, } as FunctionalComponentOptions const CustomField2 = connect( BaseComponent, mapProps({ value: true, loading: true }), mapReadPretty({ render(h) { return h('div', 'read pretty') }, }) ) const CustomField3 = connect( Input, mapProps(), mapReadPretty({ render(h) { return h('div', 'read pretty') }, }) ) const CustomFormItem = connect( { functional: true, render(h, context) { return h('div', context.data, context.children) }, }, mapProps(), mapReadPretty({ render(h) { return h('div', 'read pretty') }, }) ) const form = createForm() const { queryByText, getByTestId } = render({ data() { return { form, Decorator, CustomField, CustomField2, CustomField3, CustomFormItem, } }, template: ` dd `, }) form.query('aa').take((field) => { field.setState((state) => { state.value = '123' }) }) expect(queryByText('dd')).toBeVisible() await waitFor(() => { expect(queryByText('123')).toBeVisible() }) fireEvent.update(getByTestId('cc'), '123') expect(queryByText('123')).toBeVisible() expect(form.query('cc').get('value')).toEqual('123') form.query('aa').take((field) => { if (!isField(field)) return field.readPretty = true }) await waitFor(() => { expect(queryByText('123')).toBeNull() expect(queryByText('read pretty')).toBeVisible() }) }) ================================================ FILE: packages/vue/src/__tests__/form.spec.ts ================================================ import Vue from 'vue' import { render, fireEvent } from '@testing-library/vue' import { mount } from '@vue/test-utils' import { createForm } from '@formily/core' import { FormProvider, FormConsumer, Field, ObjectField, VoidField, } from '../vue2-components' import { defineComponent } from 'vue-demi' import { useParentForm, useField } from '../hooks' import { h } from 'vue-demi' Vue.component('FormProvider', FormProvider) Vue.component('FormConsumer', FormConsumer) Vue.component('ObjectField', ObjectField) Vue.component('VoidField', VoidField) Vue.component('Field', Field) const Input = defineComponent({ props: ['value'], setup(props, { attrs, listeners }) { const fieldRef = useField() return () => { const field = fieldRef.value return h('input', { class: 'test-input', attrs: { ...attrs, value: props.value, 'data-testid': field.path.toString(), }, on: { ...listeners, input: listeners.change, }, }) } }, }) test('render form', () => { const form = createForm() render({ data() { return { form } }, template: ` `, }) expect(form.mounted).toBeTruthy() }) const DisplayParentForm = defineComponent({ setup() { const form = useParentForm() return () => h('div', [form.value.displayName]) }, }) test('useParentForm', () => { const { queryByTestId } = render({ components: { DisplayParentForm, }, data() { const form = createForm() return { form } }, template: ` `, }) expect(queryByTestId('111').textContent).toBe('ObjectField') expect(queryByTestId('222').textContent).toBe('Form') expect(queryByTestId('333').textContent).toBe('Form') }) test('useInjectionCleaner', async () => { const form = createForm() const { getByTestId } = render({ name: 'TestComponent', setup() { return { form, Input, } }, template: ` `, }) expect(form.mounted).toBeTruthy() expect(form.query('inner').take().mounted).toBeTruthy() expect(form.query('parent.outer').take().mounted).toBeTruthy() await fireEvent.update(getByTestId('parent.outer'), '123') expect(form.getValuesIn('parent.outer')).toBe('123') await fireEvent.update(getByTestId('inner'), '123') expect(form.getValuesIn('inner')).toBe('123') }) test('FormConsumer', async () => { const form = createForm({ values: { a: 'abc', }, }) const wrapper = mount({ data() { return { form, Input } }, template: ` `, }) expect(form.getValuesIn('a')).toBe('abc') expect(wrapper.find('.consumer').text()).toBe('{"a":"abc"}') form.setDisplay('none') expect(form.getValuesIn('a')).toBeUndefined() const $consumer = wrapper.vm.$refs.consumer as Vue $consumer.$forceUpdate() expect(wrapper.find('.consumer').text()).toBe('{}') }) ================================================ FILE: packages/vue/src/__tests__/schema.json.spec.ts ================================================ import { createForm, Field } from '@formily/core' import { observer } from '@formily/reactive-vue' import { Schema } from '@formily/json-schema' import { fireEvent, render, waitFor } from '@testing-library/vue' import { mount } from '@vue/test-utils' import Vue, { FunctionalComponentOptions } from 'vue' import { FormProvider, createSchemaField, RecursionField, } from '../vue2-components' import { connect, mapProps, mapReadPretty, useField, useFieldSchema } from '../' import { defineComponent, h } from 'vue-demi' Vue.component('FormProvider', FormProvider) const Input: FunctionalComponentOptions = { functional: true, render(h, context) { return h('input', { class: 'input', attrs: { value: context.props.value, 'data-testid': 'input', }, on: { input: context.listeners.change, }, }) }, } const Input2: FunctionalComponentOptions = { functional: true, render(h, context) { return h('input', { class: 'input2', attrs: { value: context.props.value, 'data-testid': 'input2', }, on: { input: context.listeners.change, }, }) }, } const FormItem: FunctionalComponentOptions = { functional: true, render(h, { props, slots, data }) { return h( 'div', { ...data, style: { width: '300px', height: '30px', background: 'yellow', }, attrs: { 'data-testid': 'formitem', ...data.attrs, }, }, [props.label || 'unknown ', slots().default] ) }, } const ArrayItems = observer( defineComponent({ setup() { const fieldRef = useField() const schemaRef = useFieldSchema() return () => { const field = fieldRef.value const schema = schemaRef.value const items = field.value?.map?.((item, index) => { return h(RecursionField, { props: { schema: schema.items, name: index }, }) }) return h('div', { attrs: { 'data-testid': 'array-items' } }, [items]) } }, }) ) const Previewer: FunctionalComponentOptions = { functional: true, render(h, context) { return h( 'div', { attrs: { 'data-testid': 'previewer', }, }, context.children ) }, } const Previewer2: FunctionalComponentOptions = { functional: true, render(h, context) { return h( 'div', { attrs: { 'data-testid': 'previewer2', }, }, [context.scopedSlots.content({})] ) }, } const Previewer3: FunctionalComponentOptions = { functional: true, render(h, context) { return h( 'div', { attrs: { 'data-testid': 'previewer3', }, }, [ context.scopedSlots.default({ slotProp: '123', }), ] ) }, } const Previewer4: FunctionalComponentOptions = { functional: true, render(h, context) { return h( 'div', { attrs: { 'data-testid': 'previewer4', }, }, [ context.scopedSlots.content({ slotProp: '123', }), ] ) }, } const Previewer5: FunctionalComponentOptions = { functional: true, render(h, context) { return h( 'div', { attrs: { 'data-testid': 'previewer5', }, }, context.slots()?.append ) }, } describe('json schema field', () => { test('string field', () => { const form = createForm() const { SchemaField } = createSchemaField({ components: { Input, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'string', default: '123', 'x-component': 'Input', }), } }, template: ` `, }) expect(queryByTestId('input')).toBeVisible() expect(queryByTestId('input').getAttribute('value')).toEqual('123') }) test('object field', () => { const form = createForm() const { SchemaField } = createSchemaField({ components: { Input, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'object', properties: { string: { type: 'string', 'x-component': 'Input', }, }, }), } }, template: ` `, }) expect(queryByTestId('input')).toBeVisible() }) }) describe('x-content', () => { test('default slot', () => { const form = createForm() const { SchemaField } = createSchemaField({ components: { Previewer, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'string', 'x-component': 'Previewer', 'x-content': '123', }), } }, template: ` `, }) expect(queryByTestId('previewer')).toBeVisible() expect(queryByTestId('previewer').textContent).toEqual('123') }) test('default slot with component', () => { const form = createForm() const Content = { render(h) { return h('span', '123') }, } const { SchemaField } = createSchemaField({ components: { Previewer, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'string', 'x-component': 'Previewer', 'x-content': Content, }), } }, template: ` `, }) expect(queryByTestId('previewer')).toBeVisible() expect(queryByTestId('previewer').textContent).toEqual('123') }) test('default slot with name default', () => { const form = createForm() const { SchemaField } = createSchemaField({ components: { Previewer, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'string', 'x-component': 'Previewer', 'x-content': { default: '123', }, }), } }, template: ` `, }) expect(queryByTestId('previewer')).toBeVisible() expect(queryByTestId('previewer').textContent).toEqual('123') }) test('default slot with name default and component', () => { const form = createForm() const Content = { render(h) { return h('span', '123') }, } const { SchemaField } = createSchemaField({ components: { Previewer, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'string', 'x-component': 'Previewer', 'x-content': { default: Content, }, }), } }, template: ` `, }) expect(queryByTestId('previewer')).toBeVisible() expect(queryByTestId('previewer').textContent).toEqual('123') }) test('named slot', () => { const form = createForm() const Content = { render(h) { return h('span', '123') }, } const { SchemaField } = createSchemaField({ components: { Previewer2, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'string', 'x-component': 'Previewer2', 'x-content': { content: Content, }, }), } }, template: ` `, }) expect(queryByTestId('previewer2')).toBeVisible() expect(queryByTestId('previewer2').textContent).toEqual('123') }) test('named slot with scope', () => { const form = createForm() const Content = { render(h) { return h('span', '123') }, } const { SchemaField } = createSchemaField({ components: { Previewer2, }, scope: { Content, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'string', 'x-component': 'Previewer2', 'x-content': { content: '{{Content}}', }, }), } }, template: ` `, }) expect(queryByTestId('previewer2')).toBeVisible() expect(queryByTestId('previewer2').textContent).toEqual('123') }) test('named slot in void field', () => { const form = createForm() const Content = { render(h) { return h('span', '123') }, } const { SchemaField } = createSchemaField({ components: { Previewer2, }, scope: { Content, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'void', 'x-component': 'Previewer2', 'x-content': { content: '{{Content}}', }, }), } }, template: ` `, }) expect(queryByTestId('previewer2')).toBeVisible() expect(queryByTestId('previewer2').textContent).toEqual('123') }) test('scoped slot', () => { const form = createForm() const Content = { functional: true, render(h, context) { return h('span', context.props.slotProp) }, } const { SchemaField } = createSchemaField({ components: { Previewer3, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'string', 'x-component': 'Previewer3', 'x-content': Content, }), } }, template: ` `, }) expect(queryByTestId('previewer3')).toBeVisible() expect(queryByTestId('previewer3').textContent).toEqual('123') }) test('scoped slot with scope', () => { const form = createForm() const Content = { functional: true, render(h, context) { return h('span', context.props.slotProp) }, } const { SchemaField } = createSchemaField({ components: { Previewer3, }, scope: { Content, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'string', 'x-component': 'Previewer3', 'x-content': '{{Content}}', }), } }, template: ` `, }) expect(queryByTestId('previewer3')).toBeVisible() expect(queryByTestId('previewer3').textContent).toEqual('123') }) test('scoped slot with name default', () => { const form = createForm() const Content = { functional: true, render(h, context) { return h('span', context.props.slotProp) }, } const { SchemaField } = createSchemaField({ components: { Previewer3, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'string', 'x-component': 'Previewer3', 'x-content': { default: Content, }, }), } }, template: ` `, }) expect(queryByTestId('previewer3')).toBeVisible() expect(queryByTestId('previewer3').textContent).toEqual('123') }) test('scoped slot with name other', () => { const form = createForm() const Content = { functional: true, render(h, context) { return h('span', context.props.slotProp) }, } const { SchemaField } = createSchemaField({ components: { Previewer4, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'string', 'x-component': 'Previewer4', 'x-content': { content: Content, }, }), } }, template: ` `, }) expect(queryByTestId('previewer4')).toBeVisible() expect(queryByTestId('previewer4').textContent).toEqual('123') }) test('scoped slot with connect', () => { const form = createForm() const ConnectedComponent = connect( defineComponent({ render(h) { return h( 'div', { attrs: { 'data-testid': 'ConnectedComponent', }, }, [ this.$scopedSlots.default({ slotProp: '123', }), ] ) }, }), mapProps((props, field) => { return { ...props, } }) ) const scopeSlotComponent = { functional: true, render(h, context) { return h('span', context.props.slotProp) }, } const { SchemaField } = createSchemaField({ components: { ConnectedComponent, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'string', name: 'ConnectedComponent', 'x-component': 'ConnectedComponent', 'x-content': { default: scopeSlotComponent, }, }), } }, template: ` `, }) expect(queryByTestId('ConnectedComponent')).toBeVisible() expect(queryByTestId('ConnectedComponent').textContent).toEqual('123') }) test('scoped slot with connect and readPretty', () => { const form = createForm() const ConnectedWithMapReadPretty = connect( defineComponent({ render(h) { return h( 'div', { attrs: { 'data-testid': 'ConnectedWithMapReadPretty', }, }, [ this.$scopedSlots.withMapReadPretty({ slotProp: '123', }), ] ) }, }), mapProps((props, field) => { return { ...props, } }), mapReadPretty({ render(h) { return h('div', 'read pretty') }, }) ) const scopeSlotComponent = { functional: true, render(h, context) { return h('span', context.props.slotProp) }, } const { SchemaField } = createSchemaField({ components: { ConnectedWithMapReadPretty, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'string', name: 'ConnectedWithMapReadPretty', 'x-component': 'ConnectedWithMapReadPretty', 'x-content': { withMapReadPretty: scopeSlotComponent, }, }), } }, template: ` `, }) expect(queryByTestId('ConnectedWithMapReadPretty')).toBeVisible() expect(queryByTestId('ConnectedWithMapReadPretty').textContent).toEqual( '123' ) }) test('slot compitible', () => { const form = createForm() const { SchemaField } = createSchemaField({ components: { Previewer5, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'string', 'x-component': 'Previewer5', 'x-content': { append: '123', }, }), } }, template: ` `, }) expect(queryByTestId('previewer5')).toBeVisible() expect(queryByTestId('previewer5').textContent).toEqual('123') }) test('wrong x-content will be ignore', () => { const form = createForm() const { SchemaField } = createSchemaField({ components: { Previewer, }, }) const { queryAllByTestId, container } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'object', properties: { input1: { type: 'string', 'x-component': 'Previewer', 'x-content': { default: { someAttr: '123', }, }, }, input2: { type: 'string', 'x-component': 'Previewer', 'x-content': { default: null, }, }, }, }), } }, template: ` `, }) queryAllByTestId('previewer').forEach((el) => expect(el).toBeVisible()) queryAllByTestId('previewer').forEach((el) => expect(el.textContent).toEqual('') ) }) }) describe('x-slot', () => { test('x-slot works in void field properties', () => { const form = createForm() const Content = { render(h) { return h('span', '123') }, } const { SchemaField } = createSchemaField({ components: { Previewer4, Content, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'void', 'x-component': 'Previewer4', properties: { content: { type: 'void', 'x-component': 'Content', 'x-slot': 'content', }, }, }), } }, template: ` `, }) expect(queryByTestId('previewer4')).toBeVisible() expect(queryByTestId('previewer4').textContent).toEqual('123') }) test('x-slot works in object field properties', () => { const form = createForm() const Content = { render(h) { return h('span', '123') }, } const { SchemaField } = createSchemaField({ components: { Previewer4, Content, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'object', 'x-component': 'Previewer4', properties: { content: { type: 'void', 'x-component': 'Content', 'x-slot': 'content', }, }, }), } }, template: ` `, }) expect(queryByTestId('previewer4')).toBeVisible() expect(queryByTestId('previewer4').textContent).toEqual('123') }) }) describe('scope', () => { test('scope in prop', async () => { const form = createForm() const { SchemaField } = createSchemaField({ components: { Input, Input2, Previewer, }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: { type: 'object', properties: { input1: { type: 'string', 'x-component': 'Input', 'x-reactions': { target: 'input2', fulfill: { state: { value: '{{ test }}', }, }, }, }, input2: { type: 'string', 'x-component': 'Input2', }, }, }, } }, template: ` `, }) expect(queryByTestId('input2').getAttribute('value')).toEqual('123') }) test('scope in options of createSchemaField', async () => { const form = createForm() const { SchemaField } = createSchemaField({ components: { Input, Input2, Previewer, }, scope: { test: '123', }, }) const { queryByTestId } = render({ components: { SchemaField }, data() { return { form, schema: { type: 'object', properties: { input1: { type: 'string', 'x-component': 'Input', 'x-reactions': { target: 'input2', fulfill: { state: { value: '{{ test }}', }, }, }, }, input2: { type: 'string', 'x-component': 'Input2', }, }, }, } }, template: ` `, }) expect(queryByTestId('input2').getAttribute('value')).toEqual('123') }) }) describe('expression', () => { test('expression x-visible', async () => { const form = createForm() const { SchemaField } = createSchemaField({ components: { Input, Input2, Previewer, }, }) const wrapper = mount( { components: { SchemaField }, data() { return { form, schema: { type: 'object', properties: { input: { type: 'string', 'x-component': 'Input', }, input2: { type: 'string', 'x-component': 'Input2', 'x-visible': '{{$form.values.input === "123"}}', }, }, }, } }, template: ` `, }, { attachToDocument: true, } ) expect(wrapper.find('.input').exists()).toBeTruthy() expect(wrapper.find('.input2').exists()).not.toBeTruthy() form.values.input = '123' await waitFor(() => expect(wrapper.find('.input2').exists()).toBeTruthy()) wrapper.destroy() }) test('expression x-value', async () => { const form = createForm({ values: { input: 1, }, }) const { SchemaField } = createSchemaField({ components: { Input, Input2, Previewer, }, }) const wrapper = mount( { components: { SchemaField }, data() { return { form, schema: { type: 'object', properties: { input: { type: 'string', 'x-component': 'Input', }, input2: { type: 'string', 'x-component': 'Input2', 'x-value': '{{$form.values.input * 10}}', }, }, }, } }, template: ` `, }, { attachToDocument: true } ) expect(wrapper.find('.input2').attributes().value).toEqual('10') form.values.input = 10 await waitFor(() => expect(wrapper.find('.input2').attributes().value).toEqual('100') ) wrapper.destroy() }) }) describe('schema controlled', () => { test('view update correctly when schema changed', async () => { const form = createForm({}) const { SchemaField } = createSchemaField({ components: { Input, Input2, }, }) const component = defineComponent({ components: { SchemaField }, data() { return { form, schema: { type: 'object', properties: { input: { type: 'string', 'x-component': 'Input', }, input2: { type: 'string', 'x-component': 'Input2', }, }, }, } }, methods: { changeSchema() { this.schema = { type: 'object', properties: { input2: { type: 'string', 'x-component': 'Input2', }, }, } }, }, template: ` `, }) const { queryByTestId, getByText } = render(component) expect(queryByTestId('input')).toBeVisible() expect(queryByTestId('input2')).toBeVisible() getByText('changeSchema').click() await waitFor(() => { expect(queryByTestId('input2')).toBeVisible() expect(queryByTestId('input')).toBeNull() }) }) test('view updated correctly with schema fragment changed', async () => { const form = createForm({}) const { SchemaField } = createSchemaField({ components: { Input, Input2, ArrayItems, }, }) const frag1 = { type: 'object', properties: { input1: { type: 'string', 'x-component': 'Input', }, }, } const frag2 = { type: 'array', 'x-component': 'ArrayItems', items: { type: 'object', properties: { input2: { type: 'string', 'x-component': 'Input2', }, }, }, } const component = defineComponent({ components: { SchemaField }, data() { return { form, schema: { type: 'object', properties: { input: frag1, }, }, } }, methods: { changeSchema() { this.form.clearFormGraph('input') this.form.deleteValuesIn('input') this.schema = { type: 'object', properties: { input: frag2, }, } }, }, template: ` `, }) const { queryByTestId, getByText } = render(component) expect(queryByTestId('input')).toBeVisible() expect(queryByTestId('array-items')).toBeNull() getByText('changeSchema').click() await waitFor(() => { expect(queryByTestId('input')).toBeNull() expect(queryByTestId('array-items')).toBeVisible() }) }) }) describe('x-decorator', () => { test('x-decorator-props', async () => { const form = createForm() const { SchemaField } = createSchemaField({ components: { Input, FormItem, }, }) const atBlurFn = jest.fn() const onClickFn = jest.fn() const atClickFn = jest.fn() const { queryByTestId, getByText } = render({ components: { SchemaField }, data() { return { form, schema: new Schema({ type: 'string', 'x-component': 'Input', 'x-component-props': { '@blur': function atBlur() { atBlurFn() }, }, 'x-decorator': 'FormItem', 'x-decorator-props': { label: 'Label ', onClick: function onClick() { onClickFn() }, '@click': function atClick() { atClickFn() }, }, }), } }, template: ` `, }) expect(queryByTestId('formitem')).toBeVisible() await fireEvent.click(getByText('Label')) expect(atClickFn).toBeCalledTimes(1) expect(onClickFn).toBeCalledTimes(0) }) }) ================================================ FILE: packages/vue/src/__tests__/schema.markup.spec.ts ================================================ import { createForm } from '@formily/core' import { useFieldSchema, useField, Schema } from '../' import { FormProvider, RecursionField, createSchemaField, } from '../vue2-components' import { render } from '@testing-library/vue' import { mount, createLocalVue } from '@vue/test-utils' import Vue, { CreateElement } from 'vue' import { defineComponent, h } from '@vue/composition-api' Vue.component('FormProvider', FormProvider) Vue.component('RecursionField', RecursionField) const Input = defineComponent({ props: ['value'], setup(props, { attrs, listeners }) { return () => { return h('input', { attrs: { ...attrs, value: props.value, 'data-testid': 'input', }, on: { ...listeners, input: listeners.change, }, }) } }, }) describe('markup schema field', () => { test('string', () => { const form = createForm() const { SchemaField, SchemaStringField } = createSchemaField({ components: { Input, }, }) const { queryByTestId } = render({ components: { SchemaField, SchemaStringField }, data() { return { form, } }, template: ` `, }) expect(queryByTestId('input')).toBeVisible() }) test('boolean', () => { const form = createForm() const { SchemaField, SchemaBooleanField } = createSchemaField({ components: { Input, }, }) const { queryByTestId } = render({ components: { SchemaField, SchemaBooleanField }, data() { return { form, } }, template: ` `, }) expect(queryByTestId('input')).toBeVisible() }) test('number', () => { const form = createForm() const { SchemaField, SchemaNumberField } = createSchemaField({ components: { Input, }, }) const { queryByTestId } = render({ components: { SchemaField, SchemaNumberField }, data() { return { form, } }, template: ` `, }) expect(queryByTestId('input')).toBeVisible() }) test('date', () => { const form = createForm() const { SchemaField, SchemaDateField } = createSchemaField({ components: { Input, }, }) const { queryByTestId } = render({ components: { SchemaField, SchemaDateField }, data() { return { form, } }, template: ` `, }) expect(queryByTestId('input')).toBeVisible() }) test('datetime', () => { const form = createForm() const { SchemaField, SchemaDateTimeField } = createSchemaField({ components: { Input, }, }) const { queryByTestId } = render({ components: { SchemaField, SchemaDateTimeField }, data() { return { form, } }, template: ` `, }) expect(queryByTestId('input')).toBeVisible() }) test('void', () => { const form = createForm() const VoidComponent = { render(h: CreateElement) { return h( 'div', { attrs: { 'data-testid': 'void-component' } }, this.$slots.default ) }, } const { SchemaField, SchemaVoidField } = createSchemaField({ components: { VoidComponent, }, }) const { queryByTestId } = render({ components: { SchemaField, SchemaVoidField }, data() { return { form, } }, template: ` `, }) expect(queryByTestId('void-component')).toBeVisible() }) test('array', () => { const form = createForm() const components = createSchemaField({ components: { Input, }, }) render({ components: { ...components }, data() { return { form, } }, template: ` `, }) }) test('other', () => { const form = createForm() const components = createSchemaField({ components: { Input, }, }) render({ components: { ...components }, data() { return { form, } }, template: ` `, }) }) test('no parent', () => { const form = createForm() const components = createSchemaField({ components: { Input, }, }) render({ components: { ...components }, data() { return { form, } }, template: ` `, }) }) }) describe('recursion field', () => { test('onlyRenderProperties', () => { const form = createForm() const CustomObject = defineComponent({ setup() { const schemaRef = useFieldSchema() return () => { return h('div', { attrs: { 'data-testid': 'object' } }, [ h('RecursionField', { props: { schema: schemaRef.value } }), ]) } }, }) const CustomObject2 = defineComponent({ setup() { const fieldRef = useField() const schemaRef = useFieldSchema() return () => { const schema = schemaRef.value const field = fieldRef.value return h('div', { attrs: { 'data-testid': 'only-properties' } }, [ h('RecursionField', { props: { name: schema.name, basePath: field.address, schema, onlyRenderProperties: true, }, }), ]) } }, }) const components = createSchemaField({ components: { Input, CustomObject, CustomObject2, }, }) const { queryAllByTestId } = render({ components: components, data() { return { form, } }, template: ` `, }) expect(queryAllByTestId('input').length).toEqual(3) expect(queryAllByTestId('object').length).toEqual(1) expect(queryAllByTestId('only-properties').length).toEqual(2) }) test('mapProperties', () => { const form = createForm() const CustomObject = defineComponent({ setup() { const schemaRef = useFieldSchema() return () => { return h('div', { attrs: { 'data-testid': 'object' } }, [ h('RecursionField', { props: { schema: schemaRef.value, mapProperties: (schema) => { schema.default = '123' return schema }, }, }), ]) } }, }) const CustomObject2 = defineComponent({ setup() { const schemaRef = useFieldSchema() return () => { const schema = schemaRef.value return h('div', { attrs: { 'data-testid': 'object' } }, [ h('RecursionField', { props: { schema, mapProperties: () => { return null }, }, }), ]) } }, }) const components = createSchemaField({ components: { Input, CustomObject, CustomObject2, }, }) const { queryAllByTestId } = render({ components: components, data() { return { form, } }, template: ` `, }) expect(queryAllByTestId('input').length).toEqual(2) expect(queryAllByTestId('input')[0].getAttribute('value')).toEqual('123') expect(queryAllByTestId('input')[1].getAttribute('value')).toBeFalsy() }) test('filterProperties', () => { const form = createForm() const CustomObject = defineComponent({ setup() { const schemaRef = useFieldSchema() return () => { return h('div', { attrs: { 'data-testid': 'object' } }, [ h('RecursionField', { props: { schema: schemaRef.value, filterProperties: (schema: Schema) => { if (schema['x-component'] === 'Input') return false return true }, }, }), ]) } }, }) const CustomObject2 = defineComponent({ setup() { const schemaRef = useFieldSchema() return () => { return h('div', { attrs: { 'data-testid': 'object' } }, [ h('RecursionField', { props: { schema: schemaRef.value, filterProperties: (schema: Schema) => { if (schema['x-component'] === 'Input') return return true }, }, }), ]) } }, }) const components = createSchemaField({ components: { Input, CustomObject, CustomObject2, }, }) const { queryAllByTestId } = render({ components: components, data() { return { form, } }, template: ` `, }) expect(queryAllByTestId('input').length).toEqual(1) expect(queryAllByTestId('object').length).toEqual(2) }) test('onlyRenderSelf', () => { const form = createForm() const CustomObject = defineComponent({ setup() { const schemaRef = useFieldSchema() return () => { return h('div', { attrs: { 'data-testid': 'object' } }, [ h('RecursionField', { props: { schema: schemaRef.value, onlyRenderSelf: true, }, }), ]) } }, }) const components = createSchemaField({ components: { Input, CustomObject, }, }) const { queryAllByTestId } = render({ components: components, data() { return { form, } }, template: ` `, }) expect(queryAllByTestId('input').length).toEqual(0) expect(queryAllByTestId('object').length).toEqual(1) }) test('illegal schema', () => { const form = createForm() const CustomObject = defineComponent({ setup() { return () => { return h('div', { attrs: { 'data-testid': 'object' } }, [ h('RecursionField', { props: { schema: null, }, }), ]) } }, }) const CustomObject2 = defineComponent({ setup() { return () => { return h('div', { attrs: { 'data-testid': 'object' } }, [ h('RecursionField', { props: { schema: {}, }, }), ]) } }, }) const components = createSchemaField({ components: { Input, CustomObject, CustomObject2, }, }) const { queryByTestId } = render({ components: components, data() { return { form, } }, template: ` `, }) expect(queryByTestId('input')).toBeNull() }) test('schema reactions', async () => { const div = document.createElement('div') document.body.appendChild(div) const form = createForm() const components = createSchemaField({ components: { Input, }, }) const localVue = createLocalVue() localVue.component('FormProvider', FormProvider) const TestComponent = { components: components, data() { return { form, reactions: [ { when: '{{$form.values.aaa === "123"}}', fulfill: { state: { visible: true, }, }, otherwise: { state: { visible: false, }, }, }, { when: '{{$self.value === "123"}}', target: 'ccc', fulfill: { schema: { 'x-visible': true, }, }, otherwise: { schema: { 'x-visible': false, }, }, }, ], } }, template: ` `, } as any const wrapper = mount(TestComponent, { // attachTo: div, attachToDocument: true, localVue, }) expect(wrapper.find('.bbb').exists()).toBeFalsy() wrapper.find('.aaa').setValue('123') expect(form.query('aaa').get('value')).toEqual('123') await wrapper.vm.$forceUpdate() expect(wrapper.find('.bbb').exists()).toBeTruthy() expect(wrapper.find('.ccc').exists()).toBeFalsy() wrapper.find('.bbb').setValue('123') expect(form.query('bbb').get('value')).toEqual('123') await wrapper.vm.$forceUpdate() expect(wrapper.find('.ccc').exists()).toBeTruthy() wrapper.destroy() }) test('void field children', () => { const form = createForm() const VoidComponent = { render(h: CreateElement) { return h('div', this.$slots.default || 'placeholder') }, } const { SchemaField, SchemaVoidField } = createSchemaField({ components: { VoidComponent, }, }) const { queryByTestId } = render({ components: { SchemaField, SchemaVoidField }, data() { return { form, } }, template: ` `, }) expect(queryByTestId('void-component-1').textContent).toBe('placeholder') expect(queryByTestId('void-component-2').textContent).toBe('content') }) }) ================================================ FILE: packages/vue/src/__tests__/shared.spec.ts ================================================ import { createForm } from '../' import { isRaw } from '@vue/composition-api' test('createForm returns an un reactive form instance.', () => { const form = createForm() expect(isRaw(form)).toBeTruthy() }) ================================================ FILE: packages/vue/src/__tests__/utils.spec.ts ================================================ import { formatVue3VNodeData } from '../utils/formatVNodeData' test('valid formatVNodeData', () => { const onClick = () => {} const ondblclick = () => {} const vNodeData = { class: [{ bar: false }, { 'test-component': true }], style: { border: '4px solid red', padding: '20px', borderRadius: '10px', }, attrs: { id: 'foo', }, props: { value: 'leader', user: { name: '张三', age: 18, sex: 1, }, }, domProps: { innerHTML: 'innerHTML - baz', }, on: { click: onClick, }, nativeOn: { dblclick: ondblclick, }, } const vue3VNodeData = { class: [{ bar: false }, { 'test-component': true }], style: { border: '4px solid red', padding: '20px', borderRadius: '10px', }, id: 'foo', value: 'leader', user: { name: '张三', age: 18, sex: 1, }, innerHTML: 'innerHTML - baz', onClick, ondblclick, } expect(formatVue3VNodeData(vNodeData)).toEqual(vue3VNodeData) }) ================================================ FILE: packages/vue/src/components/ArrayField.ts ================================================ import { isVue2, h as _h } from 'vue-demi' import ReactiveField from './ReactiveField' import { getRawComponent } from '../utils/getRawComponent' import type { IArrayFieldProps, DefineComponent } from '../types' import { getFieldProps } from '../utils/getFieldProps' let ArrayField: DefineComponent /* istanbul ignore else */ if (isVue2) { ArrayField = { functional: true, name: 'ArrayField', props: getFieldProps(), render(h, context) { const props = context.props as IArrayFieldProps const attrs = context.data.attrs const componentData = { ...context.data, props: { fieldType: 'ArrayField', fieldProps: { ...attrs, ...props, ...getRawComponent(props), }, }, } return _h(ReactiveField, componentData, context.children) }, } as unknown as DefineComponent } else { ArrayField = { name: 'ArrayField', props: getFieldProps(), setup(props: IArrayFieldProps, context) { return () => { const componentData = { fieldType: 'ArrayField', fieldProps: { ...props, ...getRawComponent(props), }, } as Record return _h(ReactiveField, componentData, context.slots) } }, } as unknown as DefineComponent } export default ArrayField ================================================ FILE: packages/vue/src/components/ExpressionScope.ts ================================================ import { lazyMerge } from '@formily/shared' import { computed, defineComponent, inject, provide, Ref } from 'vue-demi' import { SchemaExpressionScopeSymbol, Fragment, h } from '../shared' import { IExpressionScopeProps } from '../types' export const ExpressionScope = defineComponent({ name: 'ExpressionScope', props: ['value'], setup(props: IExpressionScopeProps, { slots }) { const scopeRef = inject(SchemaExpressionScopeSymbol) const expressionScopeRef = computed(() => lazyMerge(scopeRef.value, props.value) ) provide(SchemaExpressionScopeSymbol, expressionScopeRef) return () => h(Fragment, {}, slots) }, }) ================================================ FILE: packages/vue/src/components/Field.ts ================================================ import { isVue2, h as _h } from 'vue-demi' import ReactiveField from './ReactiveField' import { getRawComponent } from '../utils/getRawComponent' import type { IFieldProps, DefineComponent } from '../types' import { getFieldProps } from '../utils/getFieldProps' let Field: DefineComponent /* istanbul ignore else */ if (isVue2) { Field = { functional: true, name: 'Field', props: getFieldProps(), render(h, context) { const props = context.props as IFieldProps const attrs = context.data.attrs const componentData = { ...context.data, props: { fieldType: 'Field', fieldProps: { ...attrs, ...props, ...getRawComponent(props), }, }, } return _h(ReactiveField, componentData, context.children) }, } as unknown as DefineComponent } else { Field = { name: 'Field', props: getFieldProps(), setup(props: IFieldProps, context) { return () => { const componentData = { fieldType: 'Field', fieldProps: { ...props, ...getRawComponent(props), }, } as Record return _h(ReactiveField, componentData, context.slots) } }, } as unknown as DefineComponent } export default Field ================================================ FILE: packages/vue/src/components/FormConsumer.ts ================================================ import { defineComponent } from 'vue-demi' import { observer } from '@formily/reactive-vue' import { useForm } from '../hooks' import h from '../shared/h' export default observer( defineComponent({ name: 'FormConsumer', inheritAttrs: false, setup(props, { slots }) { const formRef = useForm() return () => { // just like return h( 'div', { style: { display: 'contents' } }, { default: () => slots.default?.({ form: formRef.value, }), } ) } }, }), { // make sure observables updated scheduler: /* istanbul ignore next */ (update) => Promise.resolve().then(update), } ) ================================================ FILE: packages/vue/src/components/FormProvider.ts ================================================ import { provide, defineComponent, toRef } from 'vue-demi' import { FormSymbol, FieldSymbol, SchemaMarkupSymbol, SchemaSymbol, SchemaExpressionScopeSymbol, SchemaOptionsSymbol, } from '../shared/context' import { IProviderProps, DefineComponent } from '../types' import { useAttach } from '../hooks/useAttach' import { useInjectionCleaner } from '../hooks/useInjectionCleaner' import h from '../shared/h' import { Fragment } from '../shared/fragment' export default defineComponent({ name: 'FormProvider', inheritAttrs: false, props: ['form'], setup(props: IProviderProps, { slots }) { const formRef = useAttach(toRef(props, 'form')) provide(FormSymbol, formRef) useInjectionCleaner([ FieldSymbol, SchemaMarkupSymbol, SchemaSymbol, SchemaExpressionScopeSymbol, SchemaOptionsSymbol, ]) return () => h(Fragment, {}, slots) }, }) as DefineComponent ================================================ FILE: packages/vue/src/components/ObjectField.ts ================================================ import { isVue2, h as _h } from 'vue-demi' import ReactiveField from './ReactiveField' import { getRawComponent } from '../utils/getRawComponent' import type { IObjectFieldProps, DefineComponent } from '../types' import { getFieldProps } from '../utils/getFieldProps' let ObjectField: DefineComponent /* istanbul ignore else */ if (isVue2) { ObjectField = { functional: true, name: 'ObjectField', props: getFieldProps(), render(h, context) { const props = context.props as IObjectFieldProps const attrs = context.data.attrs const componentData = { ...context.data, props: { fieldType: 'ObjectField', fieldProps: { ...attrs, ...props, ...getRawComponent(props), }, }, } return _h(ReactiveField, componentData, context.children) }, } as unknown as DefineComponent } else { ObjectField = { name: 'ObjectField', props: getFieldProps(), setup(props: IObjectFieldProps, context) { return () => { const componentData = { fieldType: 'ObjectField', fieldProps: { ...props, ...getRawComponent(props), }, } as Record return _h(ReactiveField, componentData, context.slots) } }, } as unknown as DefineComponent } export default ObjectField ================================================ FILE: packages/vue/src/components/ReactiveField.ts ================================================ import { inject, provide, Ref, ref, shallowRef, watch, isVue2 } from 'vue-demi' import { GeneralField, isVoidField } from '@formily/core' import { each, FormPath } from '@formily/shared' import { observer } from '@formily/reactive-vue' import { toJS, reaction } from '@formily/reactive' import { SchemaOptionsSymbol, FieldSymbol, h, Fragment } from '../shared' import { useAttach } from '../hooks/useAttach' import { useField, useForm } from '../hooks' import type { IReactiveFieldProps, VueComponentProps, DefineComponent, } from '../types' import type { VNode } from 'vue' function isVueOptions(options: Record) { return ( typeof options.template === 'string' || typeof options.render === 'function' || typeof options.setup === 'function' ) } const wrapFragment = (childNodes: VNode[] | VNode): VNode => { if (!Array.isArray(childNodes)) { return childNodes } if (childNodes.length > 1) { return h(Fragment, {}, { default: () => childNodes }) } return childNodes[0] } const resolveComponent = (render: () => unknown[], extra?: any) => { if (extra === undefined || extra === null) { return render } if (typeof extra === 'string') { return () => [...render(), extra] } // not component if (!isVueOptions(extra) && typeof extra !== 'function') { return render } // for scoped slot if (extra.length > 1 || extra?.render?.length > 1) { return (scopedProps: VueComponentProps) => [ ...render(), h(extra, { props: scopedProps }, {}), ] } return () => [...render(), h(extra, {}, {})] } const mergeSlots = ( field: GeneralField, slots: Record, content: any ): Record any[]> => { const slotNames = Object.keys(slots) if (!slotNames.length) { if (!content) { return {} } if (typeof content === 'string') { return { default: resolveComponent(() => [], content), } } } const patchSlot = (slotName: string) => (...originArgs) => slots[slotName]?.({ field, form: field.form, ...originArgs[0] }) ?? [] const patchedSlots: Record unknown[]> = {} slotNames.forEach((name) => { patchedSlots[name] = patchSlot(name) }) // for named slots if (content && typeof content === 'object' && !isVueOptions(content)) { Object.keys(content).forEach((key) => { const child = content[key] const slot = patchedSlots[key] ?? (() => []) patchedSlots[key] = resolveComponent(slot, child) }) return patchedSlots } // maybe default slot is empty patchedSlots['default'] = resolveComponent( patchedSlots['default'] ?? (() => []), content ) return patchedSlots } const createFieldInVue2 = (innerCreateField) => { return () => { let res: GeneralField const disposer = reaction(() => { res = innerCreateField() }) disposer() return res } } export default observer({ name: 'ReactiveField', props: { fieldType: { type: String, default: 'Field', }, fieldProps: { type: Object, default: () => ({}), }, }, setup(props: IReactiveFieldProps, { slots }) { const formRef = useForm() const parentRef = useField() const optionsRef = inject(SchemaOptionsSymbol, ref(null)) let createField = () => formRef?.value?.[`create${props.fieldType}`]?.({ ...props.fieldProps, basePath: props.fieldProps?.basePath ?? parentRef.value?.address, }) if (isVue2) { createField = createFieldInVue2(createField) } const fieldRef = shallowRef(createField()) as Ref watch( () => props.fieldProps, () => (fieldRef.value = createField()) ) useAttach(fieldRef) provide(FieldSymbol, fieldRef) return () => { const field = fieldRef.value const options = optionsRef.value if (!field) { return slots.default?.() } if (field.display !== 'visible') { return h('template', {}, {}) } const mergedSlots = mergeSlots(field, slots, field.content) const renderDecorator = (childNodes: any[]) => { if (!field.decoratorType) { return wrapFragment(childNodes) } const finalComponent = FormPath.getIn(options?.components, field.decoratorType as string) ?? field.decoratorType const componentAttrs = toJS(field.decorator[1]) || {} const events: Record = {} each(componentAttrs, (value, eventKey) => { const onEvent = eventKey.startsWith('on') const atEvent = eventKey.startsWith('@') if (!onEvent && !atEvent) return if (onEvent) { const eventName = `${eventKey[2].toLowerCase()}${eventKey.slice(3)}` // '@xxx' has higher priority events[eventName] = events[eventName] || value } else if (atEvent) { const eventName = eventKey.slice(1) events[eventName] = value delete componentAttrs[eventKey] } }) const componentData = { attrs: componentAttrs, style: componentAttrs?.style, class: componentAttrs?.class, on: events, } delete componentData.attrs.style delete componentData.attrs.class return h(finalComponent, componentData, { default: () => childNodes, }) } const renderComponent = () => { if (!field.componentType) return wrapFragment(mergedSlots?.default?.()) const component = FormPath.getIn(options?.components, field.componentType as string) ?? field.componentType const originData = toJS(field.component[1]) || {} const events = {} as Record const originChange = originData['@change'] || originData['onChange'] const originFocus = originData['@focus'] || originData['onFocus'] const originBlur = originData['@blur'] || originData['onBlur'] each(originData, (value, eventKey) => { const onEvent = eventKey.startsWith('on') const atEvent = eventKey.startsWith('@') if (!onEvent && !atEvent) return if (onEvent) { const eventName = `${eventKey[2].toLowerCase()}${eventKey.slice(3)}` // '@xxx' has higher priority events[eventName] = events[eventName] || value } else if (atEvent) { const eventName = eventKey.slice(1) events[eventName] = value delete originData[eventKey] } }) events.change = (...args: any[]) => { if (!isVoidField(field)) field.onInput(...args) originChange?.(...args) } events.focus = (...args: any[]) => { if (!isVoidField(field)) field.onFocus(...args) originFocus?.(...args) } events.blur = (...args: any[]) => { if (!isVoidField(field)) field.onBlur(...args) originBlur?.(...args) } const componentData = { attrs: { disabled: !isVoidField(field) ? field.pattern === 'disabled' || field.pattern === 'readPretty' : undefined, readOnly: !isVoidField(field) ? field.pattern === 'readOnly' : undefined, ...originData, value: !isVoidField(field) ? field.value : undefined, }, style: originData?.style, class: originData?.class, on: events, } delete componentData.attrs.style delete componentData.attrs.class return h(component, componentData, mergedSlots) } return renderDecorator([renderComponent()]) } }, } as unknown as DefineComponent) ================================================ FILE: packages/vue/src/components/RecursionField.ts ================================================ import { inject, provide, watch, shallowRef, computed, markRaw } from 'vue-demi' import { GeneralField } from '@formily/core' import { isFn, isValid, lazyMerge } from '@formily/shared' import { Schema } from '@formily/json-schema' import { SchemaSymbol, SchemaOptionsSymbol, SchemaExpressionScopeSymbol, } from '../shared' import { useField } from '../hooks' import ObjectField from './ObjectField' import ArrayField from './ArrayField' import Field from './Field' import VoidField from './VoidField' import { h } from '../shared/h' import type { IRecursionFieldProps, DefineComponent } from '../types' const resolveEmptySlot = (slots: Record any[]>) => { return Object.keys(slots).length ? h('div', { style: 'display:contents;' }, slots) : undefined } const RecursionField = { name: 'RecursionField', inheritAttrs: false, props: { schema: { required: true, }, name: [String, Number], basePath: {}, onlyRenderProperties: { type: Boolean, default: undefined, }, onlyRenderSelf: { type: Boolean, default: undefined, }, mapProperties: {}, filterProperties: {}, }, setup(props: IRecursionFieldProps) { const parentRef = useField() const optionsRef = inject(SchemaOptionsSymbol) const scopeRef = inject(SchemaExpressionScopeSymbol) const createSchema = (schemaProp: IRecursionFieldProps['schema']) => markRaw(new Schema(schemaProp)) const fieldSchemaRef = computed(() => createSchema(props.schema)) const getPropsFromSchema = (schema: Schema) => schema?.toFieldProps?.({ ...optionsRef.value, get scope() { return lazyMerge(optionsRef.value.scope, scopeRef.value) }, }) const fieldPropsRef = shallowRef(getPropsFromSchema(fieldSchemaRef.value)) watch([fieldSchemaRef, optionsRef], () => { fieldPropsRef.value = getPropsFromSchema(fieldSchemaRef.value) }) const getBasePath = () => { if (props.onlyRenderProperties) { return props.basePath ?? parentRef?.value?.address.concat(props.name) } return props.basePath ?? parentRef?.value?.address } provide(SchemaSymbol, fieldSchemaRef) return () => { const basePath = getBasePath() const fieldProps = fieldPropsRef.value const generateSlotsByProperties = (scoped = false) => { if (props.onlyRenderSelf) return {} const properties = Schema.getOrderProperties(fieldSchemaRef.value) if (!properties.length) return {} const renderMap: Record unknown)[]> = {} const setRender = ( key: string, value: (field?: GeneralField) => unknown ) => { if (!renderMap[key]) { renderMap[key] = [] } renderMap[key].push(value) } properties.forEach(({ schema: item, key: name }, index) => { let schema: Schema = item if (isFn(props.mapProperties)) { const mapped = props.mapProperties(item, name) if (mapped) { schema = mapped } } if (isFn(props.filterProperties)) { if (props.filterProperties(schema, name) === false) { return null } } setRender(schema['x-slot'] ?? 'default', (field?: GeneralField) => h( RecursionField, { key: `${index}-${name}`, attrs: { schema, name, basePath: field?.address ?? basePath, }, slot: schema['x-slot'], }, {} ) ) }) const slots = {} Object.keys(renderMap).forEach((key) => { const renderFns = renderMap[key] slots[key] = scoped ? ({ field }) => renderFns.map((fn) => fn(field)) : () => renderFns.map((fn) => fn()) }) return slots } const render = () => { if (!isValid(props.name)) return resolveEmptySlot(generateSlotsByProperties()) if (fieldSchemaRef.value.type === 'object') { if (props.onlyRenderProperties) return resolveEmptySlot(generateSlotsByProperties()) return h( ObjectField, { attrs: { ...fieldProps, name: props.name, basePath: basePath, }, }, generateSlotsByProperties(true) ) } else if (fieldSchemaRef.value.type === 'array') { return h( ArrayField, { attrs: { ...fieldProps, name: props.name, basePath: basePath, }, }, {} ) } else if (fieldSchemaRef.value.type === 'void') { if (props.onlyRenderProperties) return resolveEmptySlot(generateSlotsByProperties()) const slots = generateSlotsByProperties(true) return h( VoidField, { attrs: { ...fieldProps, name: props.name, basePath: basePath, }, }, slots ) } return h( Field, { attrs: { ...fieldProps, name: props.name, basePath: basePath, }, }, {} ) } if (!fieldSchemaRef.value) return return render() } }, } as unknown as DefineComponent export default RecursionField ================================================ FILE: packages/vue/src/components/SchemaField.ts ================================================ import { inject, provide, computed, shallowRef, watch } from 'vue-demi' import { ISchema, Schema, SchemaTypes } from '@formily/json-schema' import { RecursionField } from '../components' import { SchemaMarkupSymbol, SchemaExpressionScopeSymbol, SchemaOptionsSymbol, } from '../shared' import { ISchemaFieldVueFactoryOptions, SchemaVueComponents, ISchemaFieldProps, ISchemaMarkupFieldProps, ISchemaTypeFieldProps, } from '../types' import { resolveSchemaProps } from '../utils/resolveSchemaProps' import { h } from '../shared/h' import { Fragment } from '../shared/fragment' import type { DefineComponent } from '../types' import { lazyMerge } from '@formily/shared' type SchemaFieldComponents = { SchemaField: DefineComponent SchemaMarkupField: DefineComponent SchemaStringField: DefineComponent SchemaObjectField: DefineComponent SchemaArrayField: DefineComponent SchemaBooleanField: DefineComponent SchemaDateField: DefineComponent SchemaDateTimeField: DefineComponent SchemaVoidField: DefineComponent SchemaNumberField: DefineComponent } const env = { nonameId: 0, } const getRandomName = () => { return `NO_NAME_FIELD_$${env.nonameId++}` } const markupProps = { version: String, name: [String, Number], title: {}, description: {}, default: {}, readOnly: { type: Boolean, default: undefined, }, writeOnly: { type: Boolean, default: undefined, }, enum: {}, const: {}, multipleOf: Number, maximum: Number, exclusiveMaximum: Number, minimum: Number, exclusiveMinimum: Number, maxLength: Number, minLength: Number, pattern: {}, maxItems: Number, minItems: Number, uniqueItems: { type: Boolean, default: undefined, }, maxProperties: Number, minProperties: Number, required: { type: [Boolean, Array, String], default: undefined, }, format: String, properties: {}, items: {}, additionalItems: {}, patternProperties: {}, additionalProperties: {}, xIndex: Number, xPattern: {}, xDisplay: {}, xValidator: {}, xDecorator: {}, xDecoratorProps: {}, xComponent: {}, xComponentProps: {}, xReactions: {}, xContent: {}, xVisible: { type: Boolean, default: undefined, }, xHidden: { type: Boolean, default: undefined, }, xDisabled: { type: Boolean, default: undefined, }, xEditable: { type: Boolean, default: undefined, }, xReadOnly: { type: Boolean, default: undefined, }, xReadPretty: { type: Boolean, default: undefined, }, } export function createSchemaField< Components extends SchemaVueComponents = SchemaVueComponents >(options: ISchemaFieldVueFactoryOptions = {}): SchemaFieldComponents { const SchemaField = { name: 'SchemaField', inheritAttrs: false, props: { schema: {}, scope: {}, components: {}, name: [String, Number], basePath: {}, onlyRenderProperties: { type: Boolean, default: undefined }, onlyRenderSelf: { type: Boolean, default: undefined }, mapProperties: {}, filterProperties: {}, }, setup(props: ISchemaFieldProps, { slots }) { const schemaRef = computed(() => Schema.isSchemaInstance(props.schema) ? props.schema : new Schema({ type: 'object', ...props.schema, }) ) const scopeRef = computed(() => lazyMerge(options.scope, props.scope)) const optionsRef = computed(() => ({ ...options, components: { ...options.components, ...props.components, }, })) provide(SchemaMarkupSymbol, schemaRef) provide(SchemaOptionsSymbol, optionsRef) provide(SchemaExpressionScopeSymbol, scopeRef) return () => { env.nonameId = 0 return h( Fragment, {}, { default: () => { const children = [] if (slots.default) { children.push( h( 'template', {}, { default: () => slots.default(), } ) ) } children.push( h( RecursionField, { attrs: { ...props, schema: schemaRef.value, }, }, {} ) ) return children }, } ) } }, } const MarkupField = { name: 'MarkupField', props: { type: String, ...markupProps, }, setup(props: ISchemaMarkupFieldProps, { slots }) { const parentRef = inject(SchemaMarkupSymbol, null) if (!parentRef || !parentRef.value) return () => h('template', {}, {}) const name = props.name || getRandomName() const appendArraySchema = (schema: ISchema) => { if (parentRef.value.items) { return parentRef.value.addProperty(name, schema) } else { return parentRef.value.setItems(resolveSchemaProps(props)) } } const schemaRef = shallowRef(null) watch( parentRef, () => { if ( parentRef.value.type === 'object' || parentRef.value.type === 'void' ) { schemaRef.value = parentRef.value.addProperty( name, resolveSchemaProps(props) ) } else if (parentRef.value.type === 'array') { const schema = appendArraySchema(resolveSchemaProps(props)) schemaRef.value = Array.isArray(schema) ? schema[0] : schema } }, { immediate: true } ) provide(SchemaMarkupSymbol, schemaRef) return () => { return h('div', { style: 'display: none;' }, slots) } }, } const SchemaFieldFactory = (type: SchemaTypes, name: string) => { return { name: name, props: { ...markupProps }, setup(props: ISchemaTypeFieldProps, { slots }) { return () => h( MarkupField, { attrs: { ...props, type: type, }, }, slots ) }, } } return { SchemaField, SchemaMarkupField: MarkupField, SchemaStringField: SchemaFieldFactory('string', 'SchemaStringField'), SchemaObjectField: SchemaFieldFactory('object', 'SchemaObjectField'), SchemaArrayField: SchemaFieldFactory('array', 'SchemaArrayField'), SchemaBooleanField: SchemaFieldFactory('boolean', 'SchemaBooleanField'), SchemaDateField: SchemaFieldFactory('date', 'SchemaDateField'), SchemaDateTimeField: SchemaFieldFactory('datetime', 'SchemaDatetimeField'), SchemaVoidField: SchemaFieldFactory('void', 'SchemaVoidField'), SchemaNumberField: SchemaFieldFactory('number', 'SchemaNumberField'), } as unknown as SchemaFieldComponents } ================================================ FILE: packages/vue/src/components/VoidField.ts ================================================ import { isVue2, h as _h } from 'vue-demi' import ReactiveField from './ReactiveField' import { getRawComponent } from '../utils/getRawComponent' import type { IVoidFieldProps, DefineComponent } from '../types' import { getVoidFieldProps } from '../utils/getFieldProps' let VoidField: DefineComponent /* istanbul ignore else */ if (isVue2) { VoidField = { functional: true, name: 'VoidField', props: getVoidFieldProps(), render(h, context) { const props = context.props as IVoidFieldProps const attrs = context.data.attrs const componentData = { ...context.data, props: { fieldType: 'VoidField', fieldProps: { ...attrs, ...props, ...getRawComponent(props), }, }, } return _h(ReactiveField, componentData, context.children) }, } as unknown as DefineComponent } else { VoidField = { name: 'VoidField', props: getVoidFieldProps(), setup(props: IVoidFieldProps, context) { return () => { const componentData = { fieldType: 'VoidField', fieldProps: { ...props, ...getRawComponent(props), }, } as Record return _h(ReactiveField, componentData, context.slots) } }, } as unknown as DefineComponent } export default VoidField ================================================ FILE: packages/vue/src/components/index.ts ================================================ export { default as FormProvider } from './FormProvider' export { default as FormConsumer } from './FormConsumer' export { default as ArrayField } from './ArrayField' export { default as ObjectField } from './ObjectField' export { default as VoidField } from './VoidField' export { default as RecursionField } from './RecursionField' export { default as Field } from './Field' export { createSchemaField } from './SchemaField' export { ExpressionScope } from './ExpressionScope' ================================================ FILE: packages/vue/src/global.d.ts ================================================ /// /// import * as Types from './types' declare global { namespace Formily.Vue { export { Types } } } ================================================ FILE: packages/vue/src/hooks/index.ts ================================================ export * from './useForm' export * from './useField' export * from './useFormEffects' export * from './useFieldSchema' export * from './useParentForm' ================================================ FILE: packages/vue/src/hooks/useAttach.ts ================================================ import { onMounted, watch, Ref, onUnmounted, nextTick } from 'vue-demi' interface IRecycleTarget { onMount: () => void onUnmount: () => void } export const useAttach = (target: Ref): Ref => { watch(target, (v, old, onInvalidate) => { if (v && v !== old) { old?.onUnmount() nextTick(() => v.onMount()) onInvalidate(() => v.onUnmount()) } }) onMounted(() => { target.value?.onMount() }) onUnmounted(() => { target.value?.onUnmount() }) return target } ================================================ FILE: packages/vue/src/hooks/useField.ts ================================================ import { inject, Ref, ref } from 'vue-demi' import { GeneralField } from '@formily/core' import { FieldSymbol } from '../shared/context' export const useField = (): Ref => { return inject(FieldSymbol, ref()) as any } ================================================ FILE: packages/vue/src/hooks/useFieldSchema.ts ================================================ import { inject, ref } from 'vue-demi' import { SchemaSymbol } from '../shared/context' export const useFieldSchema = () => { return inject(SchemaSymbol, ref()) } ================================================ FILE: packages/vue/src/hooks/useForm.ts ================================================ import { inject, Ref, ref } from 'vue-demi' import { Form } from '@formily/core' import { FormSymbol } from '../shared/context' export const useForm = (): Ref => { const form = inject(FormSymbol, ref()) return form } ================================================ FILE: packages/vue/src/hooks/useFormEffects.ts ================================================ import { onBeforeUnmount, watchEffect } from 'vue-demi' import { Form } from '@formily/core' import { uid } from '@formily/shared' import { useForm } from './useForm' export const useFormEffects = (effects?: (form: Form) => void): void => { const formRef = useForm() const stop = watchEffect((onCleanup) => { const id = uid() formRef.value.addEffects(id, effects) onCleanup(() => { formRef.value.removeEffects(id) }) }) onBeforeUnmount(() => stop()) } ================================================ FILE: packages/vue/src/hooks/useInjectionCleaner.ts ================================================ import { InjectionKey, provide, Ref, ref } from 'vue-demi' export const useInjectionCleaner = ( injectionKeys: InjectionKey>[] ) => { injectionKeys.forEach((key) => provide(key, ref())) } ================================================ FILE: packages/vue/src/hooks/useParentForm.ts ================================================ import { isObjectField, GeneralField, Form, ObjectField } from '@formily/core' import { computed, Ref } from 'vue-demi' import { useField } from './useField' import { useForm } from './useForm' export const useParentForm = (): Ref => { const field = useField() const form = useForm() const findObjectParent = (field: GeneralField) => { if (!field) return form.value if (isObjectField(field)) return field return findObjectParent(field?.parent) } return computed(() => findObjectParent(field.value)) } ================================================ FILE: packages/vue/src/index.ts ================================================ export * from '@formily/json-schema' export * from './components' export * from './shared' export * from './hooks' export * from './types' export * as Vue2Components from './vue2-components' ================================================ FILE: packages/vue/src/shared/connect.ts ================================================ import { isVue2, markRaw, defineComponent, getCurrentInstance } from 'vue-demi' import { isFn, isStr, FormPath, each, isValid } from '@formily/shared' import { isVoidField, GeneralField } from '@formily/core' import { observer } from '@formily/reactive-vue' import { useField } from '../hooks/useField' import h from './h' import type { VueComponent, IComponentMapper, IStateMapper, VueComponentProps, } from '../types' export function mapProps( ...args: IStateMapper>[] ) { const transform = (input: VueComponentProps, field: GeneralField) => args.reduce((props, mapper) => { if (isFn(mapper)) { props = Object.assign(props, mapper(props, field)) } else { each(mapper, (to, extract) => { const extractValue = FormPath.getIn(field, extract) const targetValue = isStr(to) ? to : extract const originalValue = FormPath.getIn(props, targetValue) if (extract === 'value') { if (to !== extract) { delete props['value'] } } if (isValid(originalValue) && !isValid(extractValue)) return FormPath.setIn(props, targetValue, extractValue) }) } return props }, input) return (target: T) => { return observer( defineComponent({ name: target.name ? `Connected${target.name}` : `ConnectedComponent`, setup(props, { attrs, slots, listeners }: any) { const fieldRef = useField() return () => { const newAttrs = fieldRef.value ? transform({ ...attrs } as VueComponentProps, fieldRef.value) : { ...attrs } return h( target, { attrs: newAttrs, on: listeners, }, slots ) } }, }) ) } } export function mapReadPretty( component: C, readPrettyProps?: Record ) { return (target: T) => { return observer( defineComponent({ name: target.name ? `Read${target.name}` : `ReadComponent`, setup(props, { attrs, slots, listeners }: Record) { const fieldRef = useField() return () => { const field = fieldRef.value return h( field && !isVoidField(field) && field.pattern === 'readPretty' ? component : target, { attrs: { ...readPrettyProps, ...attrs, }, on: listeners, }, slots ) } }, }) ) } } export function connect( target: T, ...args: IComponentMapper[] ): T { const Component = args.reduce((target: VueComponent, mapper) => { return mapper(target) }, target) /* istanbul ignore else */ if (isVue2) { const functionalComponent = defineComponent({ functional: true, name: target.name, render(h, context) { return h(Component, context.data, context.children) }, }) return markRaw(functionalComponent) as T } else { const functionalComponent = defineComponent({ name: target.name, setup(props, { attrs, slots }) { return () => { return h(Component, { props, attrs }, slots) } }, }) return markRaw(functionalComponent) as T } } ================================================ FILE: packages/vue/src/shared/context.ts ================================================ import { InjectionKey, Ref } from 'vue-demi' import { Form, GeneralField } from '@formily/core' import { Schema } from '@formily/json-schema' import { ISchemaFieldVueFactoryOptions } from '../types' export const FormSymbol: InjectionKey> = Symbol('form') export const FieldSymbol: InjectionKey> = Symbol('field') export const SchemaMarkupSymbol: InjectionKey> = Symbol('schemaMarkup') export const SchemaSymbol: InjectionKey> = Symbol('schema') export const SchemaExpressionScopeSymbol: InjectionKey< Ref> > = Symbol('schemaExpression') export const SchemaOptionsSymbol: InjectionKey< Ref > = Symbol('schemaOptions') ================================================ FILE: packages/vue/src/shared/createForm.ts ================================================ import { createForm } from '@formily/core' import { markRaw } from 'vue-demi' const createRawForm = (...args: Parameters) => { const form = createForm(...args) return markRaw(form) } export { createRawForm as createForm } ================================================ FILE: packages/vue/src/shared/fragment.ts ================================================ import { Fragment as FragmentV2 } from 'vue-frag' import { DefineComponent } from '../types' import { isVue2, defineComponent } from 'vue-demi' export const Fragment = '#fragment' let FragmentComponent: DefineComponent<{}> if (isVue2) { FragmentComponent = { name: 'Fragment', ...FragmentV2, } as unknown as DefineComponent<{}> } else { /* istanbul ignore next */ FragmentComponent = defineComponent({ name: 'Fragment', render() { return this.$slots.default?.() }, }) } export { FragmentComponent } ================================================ FILE: packages/vue/src/shared/h.ts ================================================ import { h, isVue2 } from 'vue-demi' import { Fragment, FragmentComponent } from './fragment' import { formatVue3VNodeData } from '../utils/formatVNodeData' type RenderChildren = { [key in string]?: (...args: any[]) => (VNode | string)[] } type Tag = any type VNodeData = Record type VNode = any type VNodeChildren = any const compatibleCreateElement = ( tag: Tag, data: VNodeData, components: RenderChildren ): any => { /* istanbul ignore else */ if (isVue2) { const hInVue2 = h as ( tag: Tag, data?: VNodeData, components?: VNodeChildren ) => VNode const scopedSlots = components // 默认全部作为 scopedSlots 处理 const children = [] /** * scopedSlots 不会映射为slots,所以这里手动映射一遍 * 主要为了解决 slots.x 问题 */ Object.keys(components).forEach((key) => { const func = components[key] // 转换为 slots 传递 if (typeof func === 'function' && func.length === 0) { /** * func 参数为0的判断不准确,因为composition-api包了一层,导致全部为0 * try catch 解决scoped slots 转换参数异常问题 * */ try { const child = func() children.push( key === 'default' ? child : hInVue2(FragmentComponent, { slot: key }, [child]) ) } catch (error) {} } }) const newData = Object.assign({}, data) if (Object.keys(scopedSlots).length > 0) { if (!newData.scopedSlots) { newData.scopedSlots = scopedSlots } else { newData.scopedSlots = { ...newData.scopedSlots, ...scopedSlots, } } } if (tag === Fragment) { // sometimes we needn't to use Fragment component. if (children.length === 1) { if (!Array.isArray(children[0])) { return children[0] } else if (children[0].length === 1) { if (!Array.isArray(children[0][0])) { return children[0][0] } else if (children[0][0].length === 1) { return children[0][0][0] } } } tag = FragmentComponent } return hInVue2(tag, newData, children) } else { if (tag === Fragment) { tag = FragmentComponent } const hInVue3 = h as ( tag: Tag, data?: VNodeData, components?: RenderChildren ) => VNode return hInVue3(tag, formatVue3VNodeData(data), components) } } export default compatibleCreateElement export { compatibleCreateElement as h } ================================================ FILE: packages/vue/src/shared/index.ts ================================================ export * from './context' export * from './connect' export * from './h' export * from './fragment' export * from './createForm' ================================================ FILE: packages/vue/src/types/index.ts ================================================ import { Component } from 'vue' import * as VueDemi from 'vue-demi' import { Form, IFieldFactoryProps, IVoidFieldFactoryProps, GeneralField, Field, ObjectField, FormPatternTypes, FieldDisplayTypes, FieldValidator, } from '@formily/core' import type { FormPathPattern } from '@formily/shared' import type { ISchema, Schema, SchemaKey } from '@formily/json-schema' class Helper { Return = VueDemi.defineComponent({} as { props: Record }) } export type DefineComponent = Helper['Return'] export type VueComponent = Component export type VueComponentOptionsWithProps = { props: unknown } export type VueComponentProps = T extends VueComponentOptionsWithProps ? T['props'] : T export interface IProviderProps { form: Form } export type IFieldProps< D extends VueComponent = VueComponent, C extends VueComponent = VueComponent > = IFieldFactoryProps export type IVoidFieldProps< D extends VueComponent = VueComponent, C extends VueComponent = VueComponent > = IVoidFieldFactoryProps export type IArrayFieldProps = IFieldProps export type IObjectFieldProps = IFieldProps export interface IReactiveFieldProps { fieldType: 'Field' | 'ArrayField' | 'ObjectField' | 'VoidField' fieldProps: IFieldProps | IVoidFieldProps } export interface IComponentMapper { (target: T): VueComponent } export type IStateMapper = | { [key in keyof Field]?: keyof Props | boolean } | ((props: Props, field: GeneralField) => Props) export type SchemaVueComponents = Record export interface ISchemaFieldVueFactoryOptions< Components extends SchemaVueComponents = any > { components?: Components scope?: any } export interface ISchemaFieldProps extends Omit { schema?: ISchema components?: { [key: string]: VueComponent } scope?: any name?: SchemaKey } export interface ISchemaMapper { (schema: Schema, name: SchemaKey): Schema } export interface ISchemaFilter { (schema: Schema, name: SchemaKey): boolean } export interface IRecursionFieldProps { schema: Schema name?: SchemaKey basePath?: FormPathPattern onlyRenderProperties?: boolean onlyRenderSelf?: boolean mapProperties?: ISchemaMapper filterProperties?: ISchemaFilter } export type ObjectKey = string | number | boolean | symbol export type KeyOfComponents = keyof T export type ComponentPath< T, Key extends KeyOfComponents = KeyOfComponents > = Key extends string ? Key : never export type ComponentPropsByPathValue< T extends SchemaVueComponents, P extends ComponentPath > = P extends keyof T ? VueComponentProps : never export type ISchemaMarkupFieldProps< Components extends SchemaVueComponents = SchemaVueComponents, Decorator extends ComponentPath = ComponentPath, Component extends ComponentPath = ComponentPath > = ISchema< Decorator, Component, ComponentPropsByPathValue, ComponentPropsByPathValue, FormPatternTypes, FieldDisplayTypes, FieldValidator, string, GeneralField > export type ISchemaTypeFieldProps< Components extends SchemaVueComponents = SchemaVueComponents, Decorator extends ComponentPath = ComponentPath, Component extends ComponentPath = ComponentPath > = Omit, 'type'> export type IExpressionScopeProps = { value: any } ================================================ FILE: packages/vue/src/utils/formatVNodeData.ts ================================================ import { each } from '@formily/shared' type VNodeData = Record export const formatVue3VNodeData = (data: VNodeData) => { const newData = {} each(data, (value, key) => { if (key === 'on' || key === 'nativeOn') { if (value) { each(value, (func, name) => { const eventName = `on${ key === 'on' ? name[0].toUpperCase() : name[0] }${name.slice(1)}` newData[eventName] = func }) } } else if (key === 'attrs' || key === 'props' || key === 'domProps') { Object.assign(newData, value) } else { newData[key] = value } }) return newData } ================================================ FILE: packages/vue/src/utils/getFieldProps.ts ================================================ export const getFieldProps = () => ({ name: {}, title: {}, description: {}, value: {}, initialValue: {}, basePath: {}, decorator: Array, component: Array, display: String, pattern: String, required: { type: Boolean, default: undefined }, validateFirst: { type: Boolean, default: undefined }, hidden: { type: Boolean, default: undefined }, visible: { type: Boolean, default: undefined }, editable: { type: Boolean, default: undefined }, disabled: { type: Boolean, default: undefined }, readOnly: { type: Boolean, default: undefined }, readPretty: { type: Boolean, default: undefined }, dataSource: {}, validator: {}, reactions: [Array, Function], }) export const getVoidFieldProps = () => ({ name: {}, title: {}, description: {}, basePath: {}, decorator: Array, component: Array, display: String, pattern: String, hidden: { type: Boolean, default: undefined }, visible: { type: Boolean, default: undefined }, editable: { type: Boolean, default: undefined }, disabled: { type: Boolean, default: undefined }, readOnly: { type: Boolean, default: undefined }, readPretty: { type: Boolean, default: undefined }, reactions: [Array, Function], }) ================================================ FILE: packages/vue/src/utils/getRawComponent.ts ================================================ import { IFieldProps, VueComponent } from '../types' import { toRaw } from 'vue-demi' export const getRawComponent = ( props: IFieldProps ) => { const { component, decorator } = props let newComponent: typeof props.component let newDecorator: typeof props.component if (Array.isArray(component)) { newComponent = [toRaw(component[0]), component[1]] } if (Array.isArray(decorator)) { newDecorator = [toRaw(decorator[0]), decorator[1]] } return { component: newComponent, decorator: newDecorator } } ================================================ FILE: packages/vue/src/utils/resolveSchemaProps.ts ================================================ import { paramCase } from '@formily/shared' export const resolveSchemaProps = (props: Record) => { const newProps = {} Object.keys(props).forEach((key) => { if (key.indexOf('x') === 0 && key.indexOf('x-') === -1) { newProps[paramCase(key)] = props[key] } else { newProps[key] = props[key] } }) return newProps } ================================================ FILE: packages/vue/src/vue2-components.ts ================================================ // This file just converts types import * as components from './components' import type Vue from 'vue' import type { VueConstructor } from 'vue' import type { IVoidFieldProps, IArrayFieldProps, IObjectFieldProps, IFieldProps, IRecursionFieldProps, IProviderProps, ISchemaMarkupFieldProps, ISchemaFieldProps, ISchemaFieldVueFactoryOptions, ISchemaTypeFieldProps, SchemaVueComponents, } from './types' const { Field: _Field, ArrayField: _ArrayField, FormConsumer: _FormConsumer, FormProvider: _FormProvider, ObjectField: _ObjectField, RecursionField: _RecursionField, VoidField: _VoidField, createSchemaField: _createSchemaField, } = components type DefineComponent = Vue & VueConstructor & Props type SchemaFieldComponents = { SchemaField: DefineComponent> SchemaMarkupField: DefineComponent SchemaStringField: DefineComponent SchemaObjectField: DefineComponent SchemaArrayField: DefineComponent SchemaBooleanField: DefineComponent SchemaDateField: DefineComponent SchemaDateTimeField: DefineComponent SchemaVoidField: DefineComponent SchemaNumberField: DefineComponent } type CreateSchemaField< Components extends SchemaVueComponents = SchemaVueComponents > = ( options: ISchemaFieldVueFactoryOptions ) => SchemaFieldComponents const Field = _Field as unknown as DefineComponent> const ArrayField = _ArrayField as unknown as DefineComponent< Omit > const ObjectField = _ObjectField as unknown as DefineComponent< Omit > const VoidField = _VoidField as unknown as DefineComponent< Omit > const RecursionField = _RecursionField as unknown as DefineComponent< Omit > const FormConsumer = _FormConsumer as unknown as DefineComponent<{}> const FormProvider = _FormProvider as unknown as DefineComponent const createSchemaField = _createSchemaField as unknown as CreateSchemaField export { Field, ArrayField, ObjectField, VoidField, RecursionField, FormConsumer, FormProvider, createSchemaField, } ================================================ FILE: packages/vue/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./lib", "paths": { "@formily/*": ["packages/*", "devtools/*"] }, "declaration": false } } ================================================ FILE: packages/vue/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "skipLibCheck": true }, "include": ["./src/**/*.ts", "./src/**/*.tsx"], "exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"] } ================================================ FILE: packages/vue/tsconfig.types.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./type-artefacts", "paths": { "@formily/*": ["packages/*", "devtools/*"] }, "declaration": true, "sourceMap": false, "inlineSources": false } } ================================================ FILE: scripts/build-style/buildAllStyles.ts ================================================ import typescript from 'rollup-plugin-typescript2' import { build, getRollupBasePlugin } from './helper' // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export const buildAllStyles = async (outputFile: string) => { await build({ input: 'src/style.ts', output: { file: outputFile, }, plugins: [ typescript({ tsconfig: './tsconfig.json', tsconfigOverride: { compilerOptions: { module: 'ESNext', declaration: false, }, }, }), ...getRollupBasePlugin(), ], }) } ================================================ FILE: scripts/build-style/copy.ts ================================================ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { copy, readFile, writeFile, existsSync } from 'fs-extra' import glob from 'glob' export type CopyBaseOptions = Record<'esStr' | 'libStr', string> const importLibToEs = async ({ libStr, esStr, filename, }: CopyBaseOptions & { filename: string }) => { if (!existsSync(filename)) { return Promise.resolve() } const fileContent: string = (await readFile(filename)).toString() return writeFile( filename, fileContent.replace(new RegExp(libStr, 'g'), esStr) ) } export const runCopy = ({ resolveForItem, ...lastOpts }: CopyBaseOptions & { resolveForItem?: (filename: string) => unknown }) => { return new Promise((resolve, reject) => { glob(`./src/**/*`, (err, files) => { if (err) { return reject(err) } const all = [] as Promise[] for (let i = 0; i < files.length; i += 1) { const filename = files[i] resolveForItem?.(filename) if (/\.(less|scss)$/.test(filename)) { all.push(copy(filename, filename.replace(/src\//, 'esm/'))) all.push(copy(filename, filename.replace(/src\//, 'lib/'))) continue } if (/\/style.ts$/.test(filename)) { importLibToEs({ ...lastOpts, filename: filename.replace(/src\//, 'esm/').replace(/\.ts$/, '.js'), }) continue } } }) }) } ================================================ FILE: scripts/build-style/helper.ts ================================================ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { OutputOptions, rollup, RollupOptions } from 'rollup' import postcss from 'rollup-plugin-postcss' import NpmImport from 'less-plugin-npm-import' import resolve from 'rollup-plugin-node-resolve' export const getRollupBasePlugin = () => [ resolve(), postcss({ extract: true, minimize: true, sourceMap: true, // extensions: ['.css', '.less', '.sass'], use: { less: { plugins: [new NpmImport({ prefix: '~' })], javascriptEnabled: true, }, sass: {}, stylus: {}, }, }), ] export const build = async ( rollupConfig: Omit & { output: OutputOptions } ) => { const { output, ...input } = rollupConfig const bundle = await rollup(input) return bundle.write(output as OutputOptions) } ================================================ FILE: scripts/build-style/index.ts ================================================ import { runCopy, CopyBaseOptions } from './copy' import { buildAllStyles } from './buildAllStyles' // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function build({ allStylesOutputFile, ...opts }: CopyBaseOptions & { allStylesOutputFile: string }) { return Promise.all([buildAllStyles(allStylesOutputFile), runCopy(opts)]) } export { runCopy } ================================================ FILE: scripts/rollup.base.js ================================================ import path from 'path' import typescript from 'rollup-plugin-typescript2' import resolve from 'rollup-plugin-node-resolve' import commonjs from '@rollup/plugin-commonjs' import externalGlobals from 'rollup-plugin-external-globals' import injectProcessEnv from 'rollup-plugin-inject-process-env' import dts from 'rollup-plugin-dts' import { terser } from 'rollup-plugin-terser' const presets = () => { const externals = { antd: 'antd', vue: 'Vue', react: 'React', moment: 'moment', 'react-is': 'ReactIs', '@alifd/next': 'Next', 'mobx-react-lite': 'mobxReactLite', 'react-dom': 'ReactDOM', 'element-ui': 'Element', '@ant-design/icons': 'icons', '@vue/composition-api': 'VueCompositionAPI', '@formily/reactive-react': 'Formily.ReactiveReact', '@formily/reactive-vue': 'Formily.ReactiveVue', '@formily/reactive': 'Formily.Reactive', '@formily/path': 'Formily.Path', '@formily/shared': 'Formily.Shared', '@formily/validator': 'Formily.Validator', '@formily/core': 'Formily.Core', '@formily/json-schema': 'Formily.JSONSchema', '@formily/react': 'Formily.React', '@formily/vue': 'Formily.Vue', 'vue-demi': 'VueDemi' } return [ typescript({ tsconfig: './tsconfig.build.json', tsconfigOverride: { compilerOptions: { module: 'ESNext', declaration: false, }, }, }), resolve(), commonjs(), externalGlobals(externals, { exclude: ['**/*.{less,sass,scss}'], }), ] } const createEnvPlugin = (env) => { return injectProcessEnv( { NODE_ENV: env, }, { exclude: '**/*.{css,less,sass,scss}', verbose: false, } ) } const inputFilePath = path.join(process.cwd(), 'src/index.ts') const noUIDtsPackages = [ 'formily.core', 'formily.validator', 'formily.shared', 'formily.path', 'formily.json-schema', 'formily.reactive', ] export const removeImportStyleFromInputFilePlugin = () => ({ name: 'remove-import-style-from-input-file', transform(code, id) { // 样式由 build:style 进行打包,所以要删除入口文件上的 `import './style'` if (inputFilePath === id) { return code.replace(`import './style';`, '') } return code }, }) export default (filename, targetName, ...plugins) => { const base = [ { input: 'src/index.ts', output: { format: 'umd', file: `dist/${filename}.umd.development.js`, name: targetName, sourcemap: true, amd: { id: filename, }, globals: { '@formily/json-schema': 'Formily.JSONSchema', }, }, external: ['react', 'react-dom', 'react-is', '@formily/json-schema'], plugins: [...presets(), ...plugins, createEnvPlugin('development')], }, { input: 'src/index.ts', output: { format: 'umd', file: `dist/${filename}.umd.production.js`, name: targetName, sourcemap: true, amd: { id: filename, }, globals: { '@formily/json-schema': 'Formily.JSONSchema', }, }, external: ['react', 'react-dom', 'react-is', '@formily/json-schema'], plugins: [ ...presets(), terser(), ...plugins, createEnvPlugin('production'), ], }, ] if (noUIDtsPackages.includes(filename)) { base.push({ input: 'esm/index.d.ts', output: { format: 'es', file: `dist/${filename}.d.ts`, }, plugins: [dts(), ...plugins], }) base.push({ input: 'esm/index.d.ts', output: { format: 'es', file: `dist/${filename}.all.d.ts`, }, plugins: [ dts({ respectExternal: true, }), ...plugins, ], }) } return base } ================================================ FILE: tsconfig.build.json ================================================ { "compilerOptions": { "esModuleInterop": true, "moduleResolution": "node", "allowJs": true, "module": "commonjs", "target": "es5", } } ================================================ FILE: tsconfig.jest.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "jsx": "react", "esModuleInterop": true, "moduleResolution": "node", "allowJs": true, "module": "commonjs", "target": "es5", "paths": { "@formily/*": ["./packages/*/src"] } }, "exclude": ["./packages/*/esm", "./packages/*/lib"] } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "esModuleInterop": true, "moduleResolution": "node", "jsx": "react", "module": "commonjs", "target": "es5", "allowJs": false, "noUnusedLocals": false, "preserveConstEnums": true, "skipLibCheck": true, "sourceMap": true, "inlineSources": true, "declaration": true, "experimentalDecorators": true, "downlevelIteration": true, "baseUrl": ".", "paths": { "@formily/*": ["packages/*/src", "devtools/*/src"] }, "lib": ["ESNext"] } }