This is page 18 of 35. Use http://codebase.md/alibaba/formily?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .all-contributorsrc ├── .codecov.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ └── config.yml │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows │ ├── check-pr-title.yml │ ├── ci.yml │ ├── commitlint.yml │ ├── issue-open-check.yml │ ├── package-size.yml │ └── pr-welcome.yml ├── .gitignore ├── .prettierrc.js ├── .umirc.js ├── .vscode │ └── cspell.json ├── .yarnrc ├── CHANGELOG.md ├── commitlint.config.js ├── devtools │ ├── .eslintrc │ └── chrome-extension │ ├── .npmignore │ ├── assets │ │ └── img │ │ ├── loading.svg │ │ └── logo │ │ ├── 128x128.png │ │ ├── 16x16.png │ │ ├── 38x38.png │ │ ├── 48x48.png │ │ ├── error.png │ │ ├── gray.png │ │ └── scalable.png │ ├── config │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── LICENSE.md │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── components │ │ │ │ ├── FieldTree.tsx │ │ │ │ ├── filter.ts │ │ │ │ ├── LeftPanel.tsx │ │ │ │ ├── RightPanel.tsx │ │ │ │ ├── SearchBox.tsx │ │ │ │ └── Tabs.tsx │ │ │ ├── demo.tsx │ │ │ └── index.tsx │ │ └── extension │ │ ├── backend.ts │ │ ├── background.ts │ │ ├── content.ts │ │ ├── devpanel.tsx │ │ ├── devtools.tsx │ │ ├── inject.ts │ │ ├── manifest.json │ │ ├── popup.tsx │ │ └── views │ │ ├── devpanel.ejs │ │ ├── devtools.ejs │ │ └── popup.ejs │ ├── tsconfig.build.json │ └── tsconfig.json ├── docs │ ├── functions │ │ ├── contributors.ts │ │ └── npm-search.ts │ ├── guide │ │ ├── advanced │ │ │ ├── async.md │ │ │ ├── async.zh-CN.md │ │ │ ├── build.md │ │ │ ├── build.zh-CN.md │ │ │ ├── business-logic.md │ │ │ ├── business-logic.zh-CN.md │ │ │ ├── calculator.md │ │ │ ├── calculator.zh-CN.md │ │ │ ├── controlled.md │ │ │ ├── controlled.zh-CN.md │ │ │ ├── custom.md │ │ │ ├── custom.zh-CN.md │ │ │ ├── destructor.md │ │ │ ├── destructor.zh-CN.md │ │ │ ├── input.less │ │ │ ├── layout.md │ │ │ ├── layout.zh-CN.md │ │ │ ├── linkages.md │ │ │ ├── linkages.zh-CN.md │ │ │ ├── validate.md │ │ │ └── validate.zh-CN.md │ │ ├── contribution.md │ │ ├── contribution.zh-CN.md │ │ ├── form-builder.md │ │ ├── form-builder.zh-CN.md │ │ ├── index.md │ │ ├── index.zh-CN.md │ │ ├── issue-helper.md │ │ ├── issue-helper.zh-CN.md │ │ ├── learn-formily.md │ │ ├── learn-formily.zh-CN.md │ │ ├── quick-start.md │ │ ├── quick-start.zh-CN.md │ │ ├── scenes │ │ │ ├── dialog-drawer.md │ │ │ ├── dialog-drawer.zh-CN.md │ │ │ ├── edit-detail.md │ │ │ ├── edit-detail.zh-CN.md │ │ │ ├── index.less │ │ │ ├── login-register.md │ │ │ ├── login-register.zh-CN.md │ │ │ ├── more.md │ │ │ ├── more.zh-CN.md │ │ │ ├── query-list.md │ │ │ ├── query-list.zh-CN.md │ │ │ ├── step-form.md │ │ │ ├── step-form.zh-CN.md │ │ │ ├── tab-form.md │ │ │ ├── tab-form.zh-CN.md │ │ │ └── VerifyCode.tsx │ │ ├── upgrade.md │ │ └── upgrade.zh-CN.md │ ├── index.md │ ├── index.zh-CN.md │ └── site │ ├── Contributors.less │ ├── Contributors.tsx │ ├── QrCode.less │ ├── QrCode.tsx │ ├── Section.less │ ├── Section.tsx │ └── styles.less ├── global.config.ts ├── jest.config.js ├── lerna.json ├── LICENSE.md ├── package.json ├── packages │ ├── .eslintrc │ ├── antd │ │ ├── __tests__ │ │ │ ├── moment.spec.ts │ │ │ └── sideEffects.spec.ts │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── build-style.ts │ │ ├── create-style.ts │ │ ├── docs │ │ │ ├── components │ │ │ │ ├── ArrayCards.md │ │ │ │ ├── ArrayCards.zh-CN.md │ │ │ │ ├── ArrayCollapse.md │ │ │ │ ├── ArrayCollapse.zh-CN.md │ │ │ │ ├── ArrayItems.md │ │ │ │ ├── ArrayItems.zh-CN.md │ │ │ │ ├── ArrayTable.md │ │ │ │ ├── ArrayTable.zh-CN.md │ │ │ │ ├── ArrayTabs.md │ │ │ │ ├── ArrayTabs.zh-CN.md │ │ │ │ ├── Cascader.md │ │ │ │ ├── Cascader.zh-CN.md │ │ │ │ ├── Checkbox.md │ │ │ │ ├── Checkbox.zh-CN.md │ │ │ │ ├── DatePicker.md │ │ │ │ ├── DatePicker.zh-CN.md │ │ │ │ ├── Editable.md │ │ │ │ ├── Editable.zh-CN.md │ │ │ │ ├── Form.md │ │ │ │ ├── Form.zh-CN.md │ │ │ │ ├── FormButtonGroup.md │ │ │ │ ├── FormButtonGroup.zh-CN.md │ │ │ │ ├── FormCollapse.md │ │ │ │ ├── FormCollapse.zh-CN.md │ │ │ │ ├── FormDialog.md │ │ │ │ ├── FormDialog.zh-CN.md │ │ │ │ ├── FormDrawer.md │ │ │ │ ├── FormDrawer.zh-CN.md │ │ │ │ ├── FormGrid.md │ │ │ │ ├── FormGrid.zh-CN.md │ │ │ │ ├── FormItem.md │ │ │ │ ├── FormItem.zh-CN.md │ │ │ │ ├── FormLayout.md │ │ │ │ ├── FormLayout.zh-CN.md │ │ │ │ ├── FormStep.md │ │ │ │ ├── FormStep.zh-CN.md │ │ │ │ ├── FormTab.md │ │ │ │ ├── FormTab.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ ├── index.zh-CN.md │ │ │ │ ├── Input.md │ │ │ │ ├── Input.zh-CN.md │ │ │ │ ├── NumberPicker.md │ │ │ │ ├── NumberPicker.zh-CN.md │ │ │ │ ├── Password.md │ │ │ │ ├── Password.zh-CN.md │ │ │ │ ├── PreviewText.md │ │ │ │ ├── PreviewText.zh-CN.md │ │ │ │ ├── Radio.md │ │ │ │ ├── Radio.zh-CN.md │ │ │ │ ├── Reset.md │ │ │ │ ├── Reset.zh-CN.md │ │ │ │ ├── Select.md │ │ │ │ ├── Select.zh-CN.md │ │ │ │ ├── SelectTable.md │ │ │ │ ├── SelectTable.zh-CN.md │ │ │ │ ├── Space.md │ │ │ │ ├── Space.zh-CN.md │ │ │ │ ├── Submit.md │ │ │ │ ├── Submit.zh-CN.md │ │ │ │ ├── Switch.md │ │ │ │ ├── Switch.zh-CN.md │ │ │ │ ├── TimePicker.md │ │ │ │ ├── TimePicker.zh-CN.md │ │ │ │ ├── Transfer.md │ │ │ │ ├── Transfer.zh-CN.md │ │ │ │ ├── TreeSelect.md │ │ │ │ ├── TreeSelect.zh-CN.md │ │ │ │ ├── Upload.md │ │ │ │ └── Upload.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __builtins__ │ │ │ │ ├── hooks │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useClickAway.ts │ │ │ │ │ └── usePrefixCls.ts │ │ │ │ ├── index.ts │ │ │ │ ├── loading.ts │ │ │ │ ├── moment.ts │ │ │ │ ├── pickDataProps.ts │ │ │ │ ├── portal.tsx │ │ │ │ ├── render.ts │ │ │ │ └── sort.tsx │ │ │ ├── array-base │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-cards │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-collapse │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-items │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-table │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-tabs │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── cascader │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── checkbox │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── date-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── editable │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-button-group │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-collapse │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-dialog │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-drawer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-grid │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-item │ │ │ │ ├── animation.less │ │ │ │ ├── grid.less │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-layout │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ ├── style.ts │ │ │ │ └── useResponsiveFormLayout.ts │ │ │ ├── form-step │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-tab │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── index.ts │ │ │ ├── input │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── number-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── password │ │ │ │ ├── index.tsx │ │ │ │ ├── PasswordStrength.tsx │ │ │ │ └── style.ts │ │ │ ├── preview-text │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── radio │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── reset │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select-table │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ ├── style.ts │ │ │ │ ├── useCheckSlackly.tsx │ │ │ │ ├── useFilterOptions.tsx │ │ │ │ ├── useFlatOptions.tsx │ │ │ │ ├── useSize.tsx │ │ │ │ ├── useTitleAddon.tsx │ │ │ │ └── utils.ts │ │ │ ├── space │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── style.less │ │ │ ├── style.ts │ │ │ ├── submit │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── switch │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── time-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── transfer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── tree-select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ └── upload │ │ │ ├── index.tsx │ │ │ ├── placeholder.ts │ │ │ └── style.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── benchmark │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ └── index.tsx │ │ ├── template.ejs │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── core │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── docs │ │ │ ├── api │ │ │ │ ├── entry │ │ │ │ │ ├── ActionResponse.less │ │ │ │ │ ├── ActionResponse.tsx │ │ │ │ │ ├── createForm.md │ │ │ │ │ ├── createForm.zh-CN.md │ │ │ │ │ ├── FieldEffectHooks.md │ │ │ │ │ ├── FieldEffectHooks.zh-CN.md │ │ │ │ │ ├── FormChecker.md │ │ │ │ │ ├── FormChecker.zh-CN.md │ │ │ │ │ ├── FormEffectHooks.md │ │ │ │ │ ├── FormEffectHooks.zh-CN.md │ │ │ │ │ ├── FormHooksAPI.md │ │ │ │ │ ├── FormHooksAPI.zh-CN.md │ │ │ │ │ ├── FormPath.md │ │ │ │ │ ├── FormPath.zh-CN.md │ │ │ │ │ ├── FormValidatorRegistry.md │ │ │ │ │ └── FormValidatorRegistry.zh-CN.md │ │ │ │ └── models │ │ │ │ ├── ArrayField.md │ │ │ │ ├── ArrayField.zh-CN.md │ │ │ │ ├── Field.md │ │ │ │ ├── Field.zh-CN.md │ │ │ │ ├── Form.md │ │ │ │ ├── Form.zh-CN.md │ │ │ │ ├── ObjectField.md │ │ │ │ ├── ObjectField.zh-CN.md │ │ │ │ ├── Query.md │ │ │ │ ├── Query.zh-CN.md │ │ │ │ ├── VoidField.md │ │ │ │ └── VoidField.zh-CN.md │ │ │ ├── guide │ │ │ │ ├── architecture.md │ │ │ │ ├── architecture.zh-CN.md │ │ │ │ ├── field.md │ │ │ │ ├── field.zh-CN.md │ │ │ │ ├── form.md │ │ │ │ ├── form.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ ├── index.zh-CN.md │ │ │ │ ├── mvvm.md │ │ │ │ ├── mvvm.zh-CN.md │ │ │ │ ├── values.md │ │ │ │ └── values.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── array.spec.ts │ │ │ │ ├── effects.spec.ts │ │ │ │ ├── externals.spec.ts │ │ │ │ ├── field.spec.ts │ │ │ │ ├── form.spec.ts │ │ │ │ ├── graph.spec.ts │ │ │ │ ├── heart.spec.ts │ │ │ │ ├── internals.spec.ts │ │ │ │ ├── lifecycle.spec.ts │ │ │ │ ├── object.spec.ts │ │ │ │ ├── shared.ts │ │ │ │ └── void.spec.ts │ │ │ ├── effects │ │ │ │ ├── index.ts │ │ │ │ ├── onFieldEffects.ts │ │ │ │ └── onFormEffects.ts │ │ │ ├── global.d.ts │ │ │ ├── index.ts │ │ │ ├── models │ │ │ │ ├── ArrayField.ts │ │ │ │ ├── BaseField.ts │ │ │ │ ├── Field.ts │ │ │ │ ├── Form.ts │ │ │ │ ├── Graph.ts │ │ │ │ ├── Heart.ts │ │ │ │ ├── index.ts │ │ │ │ ├── LifeCycle.ts │ │ │ │ ├── ObjectField.ts │ │ │ │ ├── Query.ts │ │ │ │ ├── types.ts │ │ │ │ └── VoidField.ts │ │ │ ├── shared │ │ │ │ ├── checkers.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── effective.ts │ │ │ │ ├── externals.ts │ │ │ │ └── internals.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── element │ │ ├── .npmignore │ │ ├── build-style.ts │ │ ├── create-style.ts │ │ ├── docs │ │ │ ├── .vuepress │ │ │ │ ├── components │ │ │ │ │ ├── createCodeSandBox.js │ │ │ │ │ ├── dumi-previewer.vue │ │ │ │ │ └── highlight.js │ │ │ │ ├── config.js │ │ │ │ ├── enhanceApp.js │ │ │ │ ├── styles │ │ │ │ │ └── index.styl │ │ │ │ └── util.js │ │ │ ├── demos │ │ │ │ ├── guide │ │ │ │ │ ├── array-cards │ │ │ │ │ │ ├── effects-json-schema.vue │ │ │ │ │ │ ├── effects-markup-schema.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-collapse │ │ │ │ │ │ ├── effects-json-schema.vue │ │ │ │ │ │ ├── effects-markup-schema.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-items │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-table │ │ │ │ │ │ ├── effects-json-schema.vue │ │ │ │ │ │ ├── effects-markup-schema.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-tabs │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── cascader │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── checkbox │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── date-picker │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── editable │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-button-group.vue │ │ │ │ │ ├── form-collapse │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── form-dialog │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-drawer │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-grid │ │ │ │ │ │ ├── form.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── native.vue │ │ │ │ │ ├── form-item │ │ │ │ │ │ ├── bordered-none.vue │ │ │ │ │ │ ├── common.vue │ │ │ │ │ │ ├── feedback.vue │ │ │ │ │ │ ├── inset.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ ├── size.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-layout │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-step │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── form-tab │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── form.vue │ │ │ │ │ ├── input │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── input-number │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── password │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── preview-text │ │ │ │ │ │ ├── base.vue │ │ │ │ │ │ └── extend.vue │ │ │ │ │ ├── radio │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── reset │ │ │ │ │ │ ├── base.vue │ │ │ │ │ │ ├── force.vue │ │ │ │ │ │ └── validate.vue │ │ │ │ │ ├── select │ │ │ │ │ │ ├── json-schema-async.vue │ │ │ │ │ │ ├── json-schema-sync.vue │ │ │ │ │ │ ├── markup-schema-async-search.vue │ │ │ │ │ │ ├── markup-schema-async.vue │ │ │ │ │ │ ├── markup-schema-sync.vue │ │ │ │ │ │ ├── template-async.vue │ │ │ │ │ │ └── template-sync.vue │ │ │ │ │ ├── space │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── submit │ │ │ │ │ │ ├── base.vue │ │ │ │ │ │ └── loading.vue │ │ │ │ │ ├── switch │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── time-picker │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── transfer │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ └── upload │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ └── template.vue │ │ │ │ └── index.vue │ │ │ ├── guide │ │ │ │ ├── array-cards.md │ │ │ │ ├── array-collapse.md │ │ │ │ ├── array-items.md │ │ │ │ ├── array-table.md │ │ │ │ ├── array-tabs.md │ │ │ │ ├── cascader.md │ │ │ │ ├── checkbox.md │ │ │ │ ├── date-picker.md │ │ │ │ ├── editable.md │ │ │ │ ├── form-button-group.md │ │ │ │ ├── form-collapse.md │ │ │ │ ├── form-dialog.md │ │ │ │ ├── form-drawer.md │ │ │ │ ├── form-grid.md │ │ │ │ ├── form-item.md │ │ │ │ ├── form-layout.md │ │ │ │ ├── form-step.md │ │ │ │ ├── form-tab.md │ │ │ │ ├── form.md │ │ │ │ ├── index.md │ │ │ │ ├── input-number.md │ │ │ │ ├── input.md │ │ │ │ ├── password.md │ │ │ │ ├── preview-text.md │ │ │ │ ├── radio.md │ │ │ │ ├── reset.md │ │ │ │ ├── select.md │ │ │ │ ├── space.md │ │ │ │ ├── submit.md │ │ │ │ ├── switch.md │ │ │ │ ├── time-picker.md │ │ │ │ ├── transfer.md │ │ │ │ └── upload.md │ │ │ └── README.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __builtins__ │ │ │ │ ├── configs │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── shared │ │ │ │ │ ├── create-context.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── loading.ts │ │ │ │ │ ├── portal.ts │ │ │ │ │ ├── resolve-component.ts │ │ │ │ │ ├── transform-component.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ └── styles │ │ │ │ └── common.scss │ │ │ ├── array-base │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-cards │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-collapse │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-items │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-table │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-tabs │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── cascader │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── checkbox │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── date-picker │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── editable │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── el-form │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── el-form-item │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── form │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-button-group │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-collapse │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-dialog │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── form-drawer │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-grid │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-item │ │ │ │ ├── animation.scss │ │ │ │ ├── grid.scss │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ ├── style.ts │ │ │ │ └── var.scss │ │ │ ├── form-layout │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ ├── style.ts │ │ │ │ └── useResponsiveFormLayout.ts │ │ │ ├── form-step │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── form-tab │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── index.ts │ │ │ ├── input │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── input-number │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── password │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── preview-text │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── radio │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── reset │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── select │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── space │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── style.ts │ │ │ ├── submit │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── switch │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── time-picker │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── transfer │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ └── upload │ │ │ ├── index.ts │ │ │ └── style.ts │ │ ├── transformer.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── grid │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── index.ts │ │ │ └── observer.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── json-schema │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── schema.spec.ts.snap │ │ │ │ ├── compiler.spec.ts │ │ │ │ ├── patches.spec.ts │ │ │ │ ├── schema.spec.ts │ │ │ │ ├── server-validate.spec.ts │ │ │ │ ├── shared.spec.ts │ │ │ │ ├── transformer.spec.ts │ │ │ │ └── traverse.spec.ts │ │ │ ├── compiler.ts │ │ │ ├── global.d.ts │ │ │ ├── index.ts │ │ │ ├── patches.ts │ │ │ ├── polyfills │ │ │ │ ├── index.ts │ │ │ │ └── SPECIFICATION_1_0.ts │ │ │ ├── schema.ts │ │ │ ├── shared.ts │ │ │ ├── transformer.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── next │ │ ├── __tests__ │ │ │ ├── moment.spec.ts │ │ │ └── sideEffects.spec.ts │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── build-style.ts │ │ ├── create-style.ts │ │ ├── docs │ │ │ ├── components │ │ │ │ ├── ArrayCards.md │ │ │ │ ├── ArrayCards.zh-CN.md │ │ │ │ ├── ArrayCollapse.md │ │ │ │ ├── ArrayCollapse.zh-CN.md │ │ │ │ ├── ArrayItems.md │ │ │ │ ├── ArrayItems.zh-CN.md │ │ │ │ ├── ArrayTable.md │ │ │ │ ├── ArrayTable.zh-CN.md │ │ │ │ ├── Cascader.md │ │ │ │ ├── Cascader.zh-CN.md │ │ │ │ ├── Checkbox.md │ │ │ │ ├── Checkbox.zh-CN.md │ │ │ │ ├── DatePicker.md │ │ │ │ ├── DatePicker.zh-CN.md │ │ │ │ ├── DatePicker2.md │ │ │ │ ├── DatePicker2.zh-CN.md │ │ │ │ ├── Editable.md │ │ │ │ ├── Editable.zh-CN.md │ │ │ │ ├── Form.md │ │ │ │ ├── Form.zh-CN.md │ │ │ │ ├── FormButtonGroup.md │ │ │ │ ├── FormButtonGroup.zh-CN.md │ │ │ │ ├── FormCollapse.md │ │ │ │ ├── FormCollapse.zh-CN.md │ │ │ │ ├── FormDialog.md │ │ │ │ ├── FormDialog.zh-CN.md │ │ │ │ ├── FormDrawer.md │ │ │ │ ├── FormDrawer.zh-CN.md │ │ │ │ ├── FormGrid.md │ │ │ │ ├── FormGrid.zh-CN.md │ │ │ │ ├── FormItem.md │ │ │ │ ├── FormItem.zh-CN.md │ │ │ │ ├── FormLayout.md │ │ │ │ ├── FormLayout.zh-CN.md │ │ │ │ ├── FormStep.md │ │ │ │ ├── FormStep.zh-CN.md │ │ │ │ ├── FormTab.md │ │ │ │ ├── FormTab.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ ├── index.zh-CN.md │ │ │ │ ├── Input.md │ │ │ │ ├── Input.zh-CN.md │ │ │ │ ├── NumberPicker.md │ │ │ │ ├── NumberPicker.zh-CN.md │ │ │ │ ├── Password.md │ │ │ │ ├── Password.zh-CN.md │ │ │ │ ├── PreviewText.md │ │ │ │ ├── PreviewText.zh-CN.md │ │ │ │ ├── Radio.md │ │ │ │ ├── Radio.zh-CN.md │ │ │ │ ├── Reset.md │ │ │ │ ├── Reset.zh-CN.md │ │ │ │ ├── Select.md │ │ │ │ ├── Select.zh-CN.md │ │ │ │ ├── SelectTable.md │ │ │ │ ├── SelectTable.zh-CN.md │ │ │ │ ├── Space.md │ │ │ │ ├── Space.zh-CN.md │ │ │ │ ├── Submit.md │ │ │ │ ├── Submit.zh-CN.md │ │ │ │ ├── Switch.md │ │ │ │ ├── Switch.zh-CN.md │ │ │ │ ├── TimePicker.md │ │ │ │ ├── TimePicker.zh-CN.md │ │ │ │ ├── TimePicker2.md │ │ │ │ ├── TimePicker2.zh-CN.md │ │ │ │ ├── Transfer.md │ │ │ │ ├── Transfer.zh-CN.md │ │ │ │ ├── TreeSelect.md │ │ │ │ ├── TreeSelect.zh-CN.md │ │ │ │ ├── Upload.md │ │ │ │ └── Upload.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LESENCE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __builtins__ │ │ │ │ ├── empty.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useClickAway.ts │ │ │ │ │ └── usePrefixCls.ts │ │ │ │ ├── icons.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── loading.ts │ │ │ │ ├── mapSize.ts │ │ │ │ ├── mapStatus.ts │ │ │ │ ├── moment.ts │ │ │ │ ├── pickDataProps.ts │ │ │ │ ├── portal.tsx │ │ │ │ ├── render.ts │ │ │ │ └── toArray.ts │ │ │ ├── array-base │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-cards │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-collapse │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-items │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-table │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── cascader │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── checkbox │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── date-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── date-picker2 │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── editable │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form-button-group │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form-collapse │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-dialog │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-drawer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-grid │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form-item │ │ │ │ ├── animation.scss │ │ │ │ ├── grid.scss │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ ├── scss │ │ │ │ │ └── variable.scss │ │ │ │ └── style.ts │ │ │ ├── form-layout │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ ├── style.ts │ │ │ │ └── useResponsiveFormLayout.ts │ │ │ ├── form-step │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-tab │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── index.ts │ │ │ ├── input │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── main.scss │ │ │ ├── number-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── password │ │ │ │ ├── index.tsx │ │ │ │ ├── PasswordStrength.tsx │ │ │ │ └── style.ts │ │ │ ├── preview-text │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── radio │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── reset │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select-table │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ ├── style.ts │ │ │ │ ├── useCheckSlackly.tsx │ │ │ │ ├── useFilterOptions.tsx │ │ │ │ ├── useFlatOptions.tsx │ │ │ │ ├── useSize.tsx │ │ │ │ ├── useTitleAddon.tsx │ │ │ │ └── utils.ts │ │ │ ├── space │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── style.ts │ │ │ ├── submit │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── switch │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── time-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── time-picker2 │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── transfer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── tree-select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ └── upload │ │ │ ├── index.tsx │ │ │ ├── main.scss │ │ │ ├── placeholder.ts │ │ │ └── style.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── path │ │ ├── .npmignore │ │ ├── benchmark.ts │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── accessor.spec.ts │ │ │ │ ├── basic.spec.ts │ │ │ │ ├── match.spec.ts │ │ │ │ ├── parser.spec.ts │ │ │ │ └── share.spec.ts │ │ │ ├── contexts.ts │ │ │ ├── destructor.ts │ │ │ ├── index.ts │ │ │ ├── matcher.ts │ │ │ ├── parser.ts │ │ │ ├── shared.ts │ │ │ ├── tokenizer.ts │ │ │ ├── tokens.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── react │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── docs │ │ │ ├── api │ │ │ │ ├── components │ │ │ │ │ ├── ArrayField.md │ │ │ │ │ ├── ArrayField.zh-CN.md │ │ │ │ │ ├── ExpressionScope.md │ │ │ │ │ ├── ExpressionScope.zh-CN.md │ │ │ │ │ ├── Field.md │ │ │ │ │ ├── Field.zh-CN.md │ │ │ │ │ ├── FormConsumer.md │ │ │ │ │ ├── FormConsumer.zh-CN.md │ │ │ │ │ ├── FormProvider.md │ │ │ │ │ ├── FormProvider.zh-CN.md │ │ │ │ │ ├── ObjectField.md │ │ │ │ │ ├── ObjectField.zh-CN.md │ │ │ │ │ ├── RecordScope.md │ │ │ │ │ ├── RecordScope.zh-CN.md │ │ │ │ │ ├── RecordsScope.md │ │ │ │ │ ├── RecordsScope.zh-CN.md │ │ │ │ │ ├── RecursionField.md │ │ │ │ │ ├── RecursionField.zh-CN.md │ │ │ │ │ ├── SchemaField.md │ │ │ │ │ ├── SchemaField.zh-CN.md │ │ │ │ │ ├── VoidField.md │ │ │ │ │ └── VoidField.zh-CN.md │ │ │ │ ├── hooks │ │ │ │ │ ├── useExpressionScope.md │ │ │ │ │ ├── useExpressionScope.zh-CN.md │ │ │ │ │ ├── useField.md │ │ │ │ │ ├── useField.zh-CN.md │ │ │ │ │ ├── useFieldSchema.md │ │ │ │ │ ├── useFieldSchema.zh-CN.md │ │ │ │ │ ├── useForm.md │ │ │ │ │ ├── useForm.zh-CN.md │ │ │ │ │ ├── useFormEffects.md │ │ │ │ │ ├── useFormEffects.zh-CN.md │ │ │ │ │ ├── useParentForm.md │ │ │ │ │ └── useParentForm.zh-CN.md │ │ │ │ └── shared │ │ │ │ ├── connect.md │ │ │ │ ├── connect.zh-CN.md │ │ │ │ ├── context.md │ │ │ │ ├── context.zh-CN.md │ │ │ │ ├── mapProps.md │ │ │ │ ├── mapProps.zh-CN.md │ │ │ │ ├── mapReadPretty.md │ │ │ │ ├── mapReadPretty.zh-CN.md │ │ │ │ ├── observer.md │ │ │ │ ├── observer.zh-CN.md │ │ │ │ ├── Schema.md │ │ │ │ └── Schema.zh-CN.md │ │ │ ├── guide │ │ │ │ ├── architecture.md │ │ │ │ ├── architecture.zh-CN.md │ │ │ │ ├── concept.md │ │ │ │ ├── concept.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ └── index.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── expression.spec.tsx │ │ │ │ ├── field.spec.tsx │ │ │ │ ├── form.spec.tsx │ │ │ │ ├── schema.json.spec.tsx │ │ │ │ ├── schema.markup.spec.tsx │ │ │ │ └── shared.tsx │ │ │ ├── components │ │ │ │ ├── ArrayField.tsx │ │ │ │ ├── ExpressionScope.tsx │ │ │ │ ├── Field.tsx │ │ │ │ ├── FormConsumer.tsx │ │ │ │ ├── FormProvider.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── ObjectField.tsx │ │ │ │ ├── ReactiveField.tsx │ │ │ │ ├── RecordScope.tsx │ │ │ │ ├── RecordsScope.tsx │ │ │ │ ├── RecursionField.tsx │ │ │ │ ├── SchemaField.tsx │ │ │ │ └── VoidField.tsx │ │ │ ├── global.d.ts │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ ├── useAttach.ts │ │ │ │ ├── useExpressionScope.ts │ │ │ │ ├── useField.ts │ │ │ │ ├── useFieldSchema.ts │ │ │ │ ├── useForm.ts │ │ │ │ ├── useFormEffects.ts │ │ │ │ └── useParentForm.ts │ │ │ ├── index.ts │ │ │ ├── shared │ │ │ │ ├── connect.ts │ │ │ │ ├── context.ts │ │ │ │ ├── index.ts │ │ │ │ └── render.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── reactive │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── benchmark.ts │ │ ├── docs │ │ │ ├── api │ │ │ │ ├── action.md │ │ │ │ ├── action.zh-CN.md │ │ │ │ ├── autorun.md │ │ │ │ ├── autorun.zh-CN.md │ │ │ │ ├── batch.md │ │ │ │ ├── batch.zh-CN.md │ │ │ │ ├── define.md │ │ │ │ ├── define.zh-CN.md │ │ │ │ ├── hasCollected.md │ │ │ │ ├── hasCollected.zh-CN.md │ │ │ │ ├── markObservable.md │ │ │ │ ├── markObservable.zh-CN.md │ │ │ │ ├── markRaw.md │ │ │ │ ├── markRaw.zh-CN.md │ │ │ │ ├── model.md │ │ │ │ ├── model.zh-CN.md │ │ │ │ ├── observable.md │ │ │ │ ├── observable.zh-CN.md │ │ │ │ ├── observe.md │ │ │ │ ├── observe.zh-CN.md │ │ │ │ ├── raw.md │ │ │ │ ├── raw.zh-CN.md │ │ │ │ ├── react │ │ │ │ │ ├── observer.md │ │ │ │ │ └── observer.zh-CN.md │ │ │ │ ├── reaction.md │ │ │ │ ├── reaction.zh-CN.md │ │ │ │ ├── toJS.md │ │ │ │ ├── toJS.zh-CN.md │ │ │ │ ├── tracker.md │ │ │ │ ├── tracker.zh-CN.md │ │ │ │ ├── typeChecker.md │ │ │ │ ├── typeChecker.zh-CN.md │ │ │ │ ├── untracked.md │ │ │ │ ├── untracked.zh-CN.md │ │ │ │ └── vue │ │ │ │ ├── observer.md │ │ │ │ └── observer.zh-CN.md │ │ │ ├── guide │ │ │ │ ├── best-practice.md │ │ │ │ ├── best-practice.zh-CN.md │ │ │ │ ├── concept.md │ │ │ │ ├── concept.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ └── index.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── action.spec.ts │ │ │ │ ├── annotations.spec.ts │ │ │ │ ├── array.spec.ts │ │ │ │ ├── autorun.spec.ts │ │ │ │ ├── batch.spec.ts │ │ │ │ ├── collections-map.spec.ts │ │ │ │ ├── collections-set.spec.ts │ │ │ │ ├── collections-weakmap.spec.ts │ │ │ │ ├── collections-weakset.spec.ts │ │ │ │ ├── define.spec.ts │ │ │ │ ├── externals.spec.ts │ │ │ │ ├── hasCollected.spec.ts │ │ │ │ ├── observable.spec.ts │ │ │ │ ├── observe.spec.ts │ │ │ │ ├── tracker.spec.ts │ │ │ │ └── untracked.spec.ts │ │ │ ├── action.ts │ │ │ ├── annotations │ │ │ │ ├── box.ts │ │ │ │ ├── computed.ts │ │ │ │ ├── index.ts │ │ │ │ ├── observable.ts │ │ │ │ ├── ref.ts │ │ │ │ └── shallow.ts │ │ │ ├── array.ts │ │ │ ├── autorun.ts │ │ │ ├── batch.ts │ │ │ ├── checkers.ts │ │ │ ├── environment.ts │ │ │ ├── externals.ts │ │ │ ├── global.d.ts │ │ │ ├── handlers.ts │ │ │ ├── index.ts │ │ │ ├── internals.ts │ │ │ ├── model.ts │ │ │ ├── observable.ts │ │ │ ├── observe.ts │ │ │ ├── reaction.ts │ │ │ ├── tracker.ts │ │ │ ├── tree.ts │ │ │ ├── types.ts │ │ │ └── untracked.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── reactive-react │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ ├── useCompatEffect.ts │ │ │ │ ├── useCompatFactory.ts │ │ │ │ ├── useDidUpdate.ts │ │ │ │ ├── useForceUpdate.ts │ │ │ │ ├── useLayoutEffect.ts │ │ │ │ └── useObserver.ts │ │ │ ├── index.ts │ │ │ ├── observer.ts │ │ │ ├── shared │ │ │ │ ├── gc.ts │ │ │ │ ├── global.ts │ │ │ │ ├── immediate.ts │ │ │ │ └── index.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── reactive-test-cases-for-react18 │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.js │ │ │ └── MySlowList.js │ │ ├── template.ejs │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── reactive-vue │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ └── observer.spec.ts │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ └── useObserver.ts │ │ │ ├── index.ts │ │ │ ├── observer │ │ │ │ ├── collectData.ts │ │ │ │ ├── index.ts │ │ │ │ ├── observerInVue2.ts │ │ │ │ └── observerInVue3.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── shared │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ └── index.spec.ts │ │ │ ├── array.ts │ │ │ ├── case.ts │ │ │ ├── checkers.ts │ │ │ ├── clone.ts │ │ │ ├── compare.ts │ │ │ ├── defaults.ts │ │ │ ├── deprecate.ts │ │ │ ├── global.ts │ │ │ ├── index.ts │ │ │ ├── instanceof.ts │ │ │ ├── isEmpty.ts │ │ │ ├── merge.ts │ │ │ ├── middleware.ts │ │ │ ├── path.ts │ │ │ ├── string.ts │ │ │ ├── subscribable.ts │ │ │ └── uid.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── validator │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── parser.spec.ts │ │ │ │ ├── registry.spec.ts │ │ │ │ └── validator.spec.ts │ │ │ ├── formats.ts │ │ │ ├── index.ts │ │ │ ├── locale.ts │ │ │ ├── parser.ts │ │ │ ├── registry.ts │ │ │ ├── rules.ts │ │ │ ├── template.ts │ │ │ ├── types.ts │ │ │ └── validator.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ └── vue │ ├── .npmignore │ ├── bin │ │ ├── formily-vue-fix.js │ │ └── formily-vue-switch.js │ ├── docs │ │ ├── .vuepress │ │ │ ├── components │ │ │ │ ├── createCodeSandBox.js │ │ │ │ ├── dumi-previewer.vue │ │ │ │ └── highlight.js │ │ │ ├── config.js │ │ │ ├── enhanceApp.js │ │ │ └── styles │ │ │ └── index.styl │ │ ├── api │ │ │ ├── components │ │ │ │ ├── array-field.md │ │ │ │ ├── expression-scope.md │ │ │ │ ├── field.md │ │ │ │ ├── form-consumer.md │ │ │ │ ├── form-provider.md │ │ │ │ ├── object-field.md │ │ │ │ ├── recursion-field-with-component.md │ │ │ │ ├── recursion-field.md │ │ │ │ ├── schema-field-with-schema.md │ │ │ │ ├── schema-field.md │ │ │ │ └── void-field.md │ │ │ ├── hooks │ │ │ │ ├── use-field-schema.md │ │ │ │ ├── use-field.md │ │ │ │ ├── use-form-effects.md │ │ │ │ ├── use-form.md │ │ │ │ └── use-parent-form.md │ │ │ └── shared │ │ │ ├── connect.md │ │ │ ├── injections.md │ │ │ ├── map-props.md │ │ │ ├── map-read-pretty.md │ │ │ ├── observer.md │ │ │ └── schema.md │ │ ├── demos │ │ │ ├── api │ │ │ │ ├── components │ │ │ │ │ ├── array-field.vue │ │ │ │ │ ├── expression-scope.vue │ │ │ │ │ ├── field.vue │ │ │ │ │ ├── form-consumer.vue │ │ │ │ │ ├── form-provider.vue │ │ │ │ │ ├── object-field.vue │ │ │ │ │ ├── recursion-field-with-component.vue │ │ │ │ │ ├── recursion-field.vue │ │ │ │ │ ├── schema-field-with-schema.vue │ │ │ │ │ ├── schema-field.vue │ │ │ │ │ └── void-field.vue │ │ │ │ ├── hooks │ │ │ │ │ ├── use-field-schema.vue │ │ │ │ │ ├── use-field.vue │ │ │ │ │ ├── use-form-effects.vue │ │ │ │ │ ├── use-form.vue │ │ │ │ │ └── use-parent-form.vue │ │ │ │ └── shared │ │ │ │ ├── connect.vue │ │ │ │ ├── map-props.vue │ │ │ │ ├── map-read-pretty.vue │ │ │ │ └── observer.vue │ │ │ ├── index.vue │ │ │ └── questions │ │ │ ├── default-slot.vue │ │ │ ├── events.vue │ │ │ ├── named-slot.vue │ │ │ └── scoped-slot.vue │ │ ├── guide │ │ │ ├── architecture.md │ │ │ ├── concept.md │ │ │ └── README.md │ │ ├── questions │ │ │ └── README.md │ │ └── README.md │ ├── package.json │ ├── README.md │ ├── rollup.config.js │ ├── scripts │ │ ├── postinstall.js │ │ ├── switch-cli.js │ │ └── utils.js │ ├── src │ │ ├── __tests__ │ │ │ ├── expression.scope.spec.ts │ │ │ ├── field.spec.ts │ │ │ ├── form.spec.ts │ │ │ ├── schema.json.spec.ts │ │ │ ├── schema.markup.spec.ts │ │ │ ├── shared.spec.ts │ │ │ └── utils.spec.ts │ │ ├── components │ │ │ ├── ArrayField.ts │ │ │ ├── ExpressionScope.ts │ │ │ ├── Field.ts │ │ │ ├── FormConsumer.ts │ │ │ ├── FormProvider.ts │ │ │ ├── index.ts │ │ │ ├── ObjectField.ts │ │ │ ├── ReactiveField.ts │ │ │ ├── RecursionField.ts │ │ │ ├── SchemaField.ts │ │ │ └── VoidField.ts │ │ ├── global.d.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useAttach.ts │ │ │ ├── useField.ts │ │ │ ├── useFieldSchema.ts │ │ │ ├── useForm.ts │ │ │ ├── useFormEffects.ts │ │ │ ├── useInjectionCleaner.ts │ │ │ └── useParentForm.ts │ │ ├── index.ts │ │ ├── shared │ │ │ ├── connect.ts │ │ │ ├── context.ts │ │ │ ├── createForm.ts │ │ │ ├── fragment.ts │ │ │ ├── h.ts │ │ │ └── index.ts │ │ ├── types │ │ │ └── index.ts │ │ ├── utils │ │ │ ├── formatVNodeData.ts │ │ │ ├── getFieldProps.ts │ │ │ ├── getRawComponent.ts │ │ │ └── resolveSchemaProps.ts │ │ └── vue2-components.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── tsconfig.types.json ├── README.md ├── README.zh-cn.md ├── scripts │ ├── build-style │ │ ├── buildAllStyles.ts │ │ ├── copy.ts │ │ ├── helper.ts │ │ └── index.ts │ └── rollup.base.js ├── tsconfig.build.json ├── tsconfig.jest.json ├── tsconfig.json └── yarn.lock ``` # Files -------------------------------------------------------------------------------- /packages/next/docs/components/FormDrawer.zh-CN.md: -------------------------------------------------------------------------------- ```markdown # 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 ( <Button onClick={() => { FormDrawer('抽屉表单', () => { return ( <FormLayout labelCol={6} wrapperCol={10}> <SchemaField> <SchemaField.String name="aaa" required title="输入框1" x-decorator="FormItem" x-component="Input" /> <SchemaField.String name="bbb" required title="输入框2" x-decorator="FormItem" x-component="Input" /> <SchemaField.String name="ccc" required title="输入框3" x-decorator="FormItem" x-component="Input" /> <SchemaField.String name="ddd" required title="输入框4" x-decorator="FormItem" x-component="Input" /> </SchemaField> <FormDrawer.Footer> <FormButtonGroup align="right"> <Submit onSubmit={() => { return new Promise((resolve) => { setTimeout(resolve, 1000) }) }} > 提交 </Submit> <Reset>重置</Reset> </FormButtonGroup> </FormDrawer.Footer> </FormLayout> ) }) .forOpen((props, next) => { setTimeout(() => { next() }, 1000) }) .open({ initialValues: { aaa: '123', }, }) .then(console.log) }} > 点我打开表单 </Button> ) } ``` ## 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 ( <Button onClick={() => { FormDrawer('弹窗表单', () => { return ( <FormLayout labelCol={6} wrapperCol={14}> <SchemaField schema={schema} /> <FormDrawer.Footer> <FormButtonGroup align="right"> <Submit onSubmit={() => { return new Promise((resolve) => { setTimeout(resolve, 1000) }) }} > 提交 </Submit> <Reset>重置</Reset> </FormButtonGroup> </FormDrawer.Footer> </FormLayout> ) }) .open({ initialValues: { aaa: '123', }, }) .then(console.log) }} > 点我打开表单 </Button> ) } ``` ## 纯 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 ( <Button onClick={() => { FormDrawer('弹窗表单', () => { return ( <FormLayout labelCol={6} wrapperCol={14}> <Field name="aaa" required title="输入框1" decorator={[FormItem]} component={[Input]} /> <Field name="bbb" required title="输入框2" decorator={[FormItem]} component={[Input]} /> <Field name="ccc" required title="输入框3" decorator={[FormItem]} component={[Input]} /> <Field name="ddd" required title="输入框4" decorator={[FormItem]} component={[Input]} /> <FormDrawer.Footer> <FormButtonGroup align="right"> <Submit onSubmit={() => { return new Promise((resolve) => { setTimeout(resolve, 1000) }) }} > 提交 </Submit> <Reset>重置</Reset> </FormButtonGroup> </FormDrawer.Footer> </FormLayout> ) }) .open({ initialValues: { aaa: '123', }, }) .then(console.log) }} > 点我打开表单 </Button> ) } ``` ## 使用 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 ( <ConfigProvider defaultPropsConfig={{ Drawer: {}, }} > <Button onClick={() => { FormDrawer('弹窗表单', () => { return ( <FormLayout labelCol={6} wrapperCol={14}> <Field name="aaa" required title="输入框1" decorator={[FormItem]} component={[Input]} /> <Field name="bbb" required title="输入框2" decorator={[FormItem]} component={[Input]} /> <Field name="ccc" required title="输入框3" decorator={[FormItem]} component={[Input]} /> <Field name="ddd" required title="输入框4" decorator={[FormItem]} component={[Input]} /> <FormDrawer.Footer> <FormButtonGroup align="right"> <Submit onSubmit={() => { return new Promise((resolve) => { setTimeout(resolve, 1000) }) }} > 提交 </Submit> <Reset>重置</Reset> </FormButtonGroup> </FormDrawer.Footer> </FormLayout> ) }) .open({ initialValues: { aaa: '123', }, }) .then(console.log) }} > 点我打开表单 </Button> </ConfigProvider> ) } ``` ## 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 ): any //中间件拦截器,可以拦截Drawer打开 //打开弹窗,接收表单属性,可以传入initialValues/values/effects etc. open(props: IFormProps): Promise<any> //返回表单数据 //关闭弹窗 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 ``` -------------------------------------------------------------------------------- /packages/next/docs/components/Select.md: -------------------------------------------------------------------------------- ```markdown # 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 () => ( <FormProvider form={form}> <SchemaField> <SchemaField.Number name="select" 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, }, }} /> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) ``` ## 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 () => ( <FormProvider form={form}> <SchemaField> <SchemaField.Number name="linkage" title="Linkage selection box" x-decorator="FormItem" x-component="Select" enum={[ { label: 'Request 1', value: 1 }, { label: 'Request 2', value: 2 }, ]} x-component-props={{ style: { width: 120, }, }} /> <SchemaField.String name="select" title="Asynchronous select box" x-decorator="FormItem" x-component="Select" x-component-props={{ style: { width: 120, }, }} /> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) ``` ## 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 () => ( <FormProvider form={form}> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) ``` ## 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 () => ( <FormProvider form={form}> <SchemaField schema={schema} scope={{ useAsyncDataSource, loadData }} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) ``` ## 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 () => ( <FormProvider form={form}> <Field name="select" title="select box" dataSource={[ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 }, ]} decorator={[FormItem]} component={[ Select, { style: { width: 120, }, }, ]} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) ``` ## 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 () => ( <FormProvider form={form}> <Field name="linkage" title="Linkage selection box" dataSource={[ { label: 'Request 1', value: 1 }, { label: 'Request 2', value: 2 }, ]} decorator={[FormItem]} component={[ Select, { style: { width: 120, }, }, ]} /> <Field name="select" title="Asynchronous select box" decorator={[FormItem]} component={[ Select, { style: { width: 120, }, }, ]} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) ``` ## API Reference https://fusion.design/pc/component/basic/select ``` -------------------------------------------------------------------------------- /packages/antd/src/preview-text/index.tsx: -------------------------------------------------------------------------------- ```typescript import React, { createContext, useContext } from 'react' import { isArr, toArr, isValid } from '@formily/shared' import { Field } from '@formily/core' import { observer, useField } from '@formily/react' import { InputProps } from 'antd/lib/input' import { InputNumberProps } from 'antd/lib/input-number' import { SelectProps } from 'antd/lib/select' import { TreeSelectProps } from 'antd/lib/tree-select' import { CascaderProps, DefaultOptionType } from 'antd/lib/cascader' import { DatePickerProps, RangePickerProps as DateRangePickerProps, } from 'antd/lib/date-picker' import { TimePickerProps, TimeRangePickerProps } from 'antd/lib/time-picker' import { Tag, Space } from 'antd' import cls from 'classnames' import { formatMomentValue, usePrefixCls } from '../__builtins__' const PlaceholderContext = createContext<React.ReactNode>('N/A') const Placeholder = PlaceholderContext.Provider const usePlaceholder = (value?: any) => { const placeholder = useContext(PlaceholderContext) || 'N/A' return isValid(value) && value !== '' ? value : placeholder } const Input: React.FC<React.PropsWithChildren<InputProps>> = (props) => { const prefixCls = usePrefixCls('form-text', props) return ( <Space className={cls(prefixCls, props.className)} style={props.style}> {props.addonBefore} {props.prefix} {usePlaceholder(props.value)} {props.suffix} {props.addonAfter} </Space> ) } const NumberPicker: React.FC<React.PropsWithChildren<InputNumberProps>> = ( props ) => { const prefixCls = usePrefixCls('form-text', props) return ( <Space className={cls(prefixCls, props.className)} style={props.style}> {props.addonBefore} {props.prefix} {usePlaceholder( props.formatter ? props.formatter(String(props.value), { userTyping: false, input: '', }) : props.value )} {props['suffix']} {props.addonAfter} </Space> ) } const Select: React.FC<React.PropsWithChildren<SelectProps<any>>> = observer( (props) => { const field = useField<Field>() const prefixCls = usePrefixCls('form-text', props) const dataSource: any[] = field?.dataSource?.length ? field.dataSource : props?.options?.length ? props.options : [] const placeholder = usePlaceholder() const getSelected = () => { const value = props.value if (props.mode === 'multiple' || props.mode === 'tags') { if (props.labelInValue) { return isArr(value) ? value : [] } else { return isArr(value) ? value.map((val) => ({ label: val, value: val })) : [] } } else { if (props.labelInValue) { return isValid(value) ? [value] : [] } else { return isValid(value) ? [{ label: value, value }] : [] } } } const getLabel = (target: any) => { const labelKey = props.fieldNames?.label || 'label' return ( dataSource?.find((item) => { const valueKey = props.fieldNames?.value || 'value' return item[valueKey] == target?.value })?.[labelKey] || target.label || placeholder ) } const getLabels = () => { const selected = getSelected() if (!selected.length) return placeholder if (selected.length === 1) return getLabel(selected[0]) return selected.map((item, key) => { return <Tag key={key}>{getLabel(item)}</Tag> }) } return ( <div className={cls(prefixCls, props.className)} style={props.style}> {getLabels()} </div> ) } ) const TreeSelect: React.FC<React.PropsWithChildren<TreeSelectProps<any>>> = observer((props) => { const field = useField<Field>() const placeholder = usePlaceholder() const prefixCls = usePrefixCls('form-text', props) const dataSource = field?.dataSource?.length ? field.dataSource : props?.treeData?.length ? props.treeData : [] const getSelected = () => { const value = props.value if (props.multiple) { if (props.labelInValue) { return isArr(value) ? value : [] } else { return isArr(value) ? value.map((val) => ({ label: val, value: val })) : [] } } else { if (props.labelInValue) { return value ? [value] : [] } else { return value ? [{ label: value, value }] : [] } } } const findLabel = ( value: any, dataSource: any[], treeNodeLabelProp?: string ) => { for (let i = 0; i < dataSource?.length; i++) { const item = dataSource[i] if (item?.value === value) { return item?.label ?? item[treeNodeLabelProp] } else { const childLabel = findLabel(value, item?.children, treeNodeLabelProp) if (childLabel) return childLabel } } } const getLabels = () => { const selected = getSelected() if (!selected?.length) return <Tag>{placeholder}</Tag> return selected.map(({ value, label }, key) => { return ( <Tag key={key}> {findLabel(value, dataSource, props.treeNodeLabelProp) || label || placeholder} </Tag> ) }) } return ( <div className={cls(prefixCls, props.className)} style={props.style}> {getLabels()} </div> ) }) const Cascader: React.FC<React.PropsWithChildren<CascaderProps<any>>> = observer((props) => { const field = useField<Field>() const placeholder = usePlaceholder() const prefixCls = usePrefixCls('form-text', props) const dataSource: any[] = field?.dataSource?.length ? field.dataSource : props?.options?.length ? props.options : [] const findSelectedItem = ( items: DefaultOptionType[], val: string | number ) => { return items.find((item) => item.value == val) } const findSelectedItems = ( sources: DefaultOptionType[], selectedValues: Array<string[] | number[]> ): Array<any[]> => { return selectedValues.map((value) => { const result: Array<DefaultOptionType> = [] let items = sources value.forEach((val) => { const selectedItem = findSelectedItem(items, val) result.push({ label: selectedItem?.label ?? '', value: selectedItem?.value, }) items = selectedItem?.children ?? [] }) return result }) } const getSelected = () => { const val = toArr(props.value) // unified conversion to multi selection mode return props.multiple ? val : [val] } const getLabels = () => { const selected = getSelected() const values = findSelectedItems(dataSource, selected) const labels = values .map((val: Array<DefaultOptionType>) => { return val.map((item) => item.label).join('/') }) .join(' ') return labels || placeholder } return ( <div className={cls(prefixCls, props.className)} style={props.style}> {getLabels()} </div> ) }) const DatePicker: React.FC<React.PropsWithChildren<DatePickerProps>> = ( props ) => { const placeholder = usePlaceholder() const prefixCls = usePrefixCls('form-text', props) const getLabels = () => { const labels = formatMomentValue(props.value, props.format, placeholder) return isArr(labels) ? labels.join('~') : labels } return <div className={cls(prefixCls, props.className)}>{getLabels()}</div> } const DateRangePicker: React.FC< React.PropsWithChildren<DateRangePickerProps> > = (props) => { const placeholder = usePlaceholder() const prefixCls = usePrefixCls('form-text', props) const getLabels = () => { const labels = formatMomentValue(props.value, props.format, placeholder) return isArr(labels) ? labels.join('~') : labels } return ( <div className={cls(prefixCls, props.className)} style={props.style}> {getLabels()} </div> ) } const TimePicker: React.FC<React.PropsWithChildren<TimePickerProps>> = ( props ) => { const placeholder = usePlaceholder() const prefixCls = usePrefixCls('form-text', props) const getLabels = () => { const labels = formatMomentValue(props.value, props.format, placeholder) return isArr(labels) ? labels.join('~') : labels } return ( <div className={cls(prefixCls, props.className)} style={props.style}> {getLabels()} </div> ) } const TimeRangePicker: React.FC< React.PropsWithChildren<TimeRangePickerProps> > = (props) => { const placeholder = usePlaceholder() const prefixCls = usePrefixCls('form-text', props) const getLabels = () => { const labels = formatMomentValue(props.value, props.format, placeholder) return isArr(labels) ? labels.join('~') : labels } return ( <div className={cls(prefixCls, props.className)} style={props.style}> {getLabels()} </div> ) } const Text = (props: React.PropsWithChildren<any>) => { const prefixCls = usePrefixCls('form-text', props) return ( <div className={cls(prefixCls, props.className)} style={props.style}> {usePlaceholder(props.value)} </div> ) } Text.Input = Input Text.Select = Select Text.TreeSelect = TreeSelect Text.Cascader = Cascader Text.DatePicker = DatePicker Text.DateRangePicker = DateRangePicker Text.TimePicker = TimePicker Text.TimeRangePicker = TimeRangePicker Text.Placeholder = Placeholder Text.usePlaceholder = usePlaceholder Text.NumberPicker = NumberPicker export const PreviewText = Text export default PreviewText ``` -------------------------------------------------------------------------------- /docs/guide/advanced/calculator.zh-CN.md: -------------------------------------------------------------------------------- ```markdown # 实现联动计算器 联动计算器,主要用于在填写表单的过程中做求值汇总,在 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 ( <Form form={form} layout="vertical"> <SchemaField> <SchemaField.Array name="projects" title="Projects" x-decorator="FormItem" x-component="ArrayTable" > <SchemaField.Object> <SchemaField.Void x-component="ArrayTable.Column" x-component-props={{ width: 50, title: 'Sort', align: 'center' }} > <SchemaField.Void x-decorator="FormItem" x-component="ArrayTable.SortHandle" /> </SchemaField.Void> <SchemaField.Void x-component="ArrayTable.Column" x-component-props={{ width: 80, title: 'Index', align: 'center' }} > <SchemaField.String x-decorator="FormItem" x-component="ArrayTable.Index" /> </SchemaField.Void> <SchemaField.Void x-component="ArrayTable.Column" x-component-props={{ title: 'Price' }} > <SchemaField.Number name="price" x-decorator="Editable" required x-component="NumberPicker" x-component-props={{ addonAfter: '$', }} default={0} /> </SchemaField.Void> <SchemaField.Void x-component="ArrayTable.Column" x-component-props={{ title: 'Count' }} > <SchemaField.Number name="count" x-decorator="Editable" required x-component="NumberPicker" default={0} /> </SchemaField.Void> <SchemaField.Void x-component="ArrayTable.Column" x-component-props={{ title: 'Total' }} > <SchemaField.Number x-decorator="FormItem" name="total" x-component="NumberPicker" x-pattern="readPretty" x-component-props={{ addonAfter: '$', }} x-reactions={{ dependencies: ['.price', '.count'], when: '{{$deps[0] && $deps[1]}}', fulfill: { state: { value: '{{$deps[0] * $deps[1]}}', }, }, }} /> </SchemaField.Void> <SchemaField.Void x-component="ArrayTable.Column" x-component-props={{ title: 'Operations', dataIndex: 'operations', width: 200, fixed: 'right', }} > <SchemaField.Void x-component="FormItem"> <SchemaField.Void x-component="ArrayTable.Remove" /> <SchemaField.Void x-component="ArrayTable.MoveDown" /> <SchemaField.Void x-component="ArrayTable.MoveUp" /> </SchemaField.Void> </SchemaField.Void> </SchemaField.Object> <SchemaField.Void x-component="ArrayTable.Addition" title="Add" /> </SchemaField.Array> <SchemaField.Number name="total" 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)}}', }, }, }} /> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>提交</Submit> </FormButtonGroup> </Form> ) } ``` ## 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 ( <Form form={form} layout="vertical"> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>提交</Submit> </FormButtonGroup> </Form> ) } ``` ``` -------------------------------------------------------------------------------- /docs/guide/advanced/calculator.md: -------------------------------------------------------------------------------- ```markdown # 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 ( <Form form={form} layout="vertical"> <SchemaField> <SchemaField.Array name="projects" title="Projects" x-decorator="FormItem" x-component="ArrayTable" > <SchemaField.Object> <SchemaField.Void x-component="ArrayTable.Column" x-component-props={{ width: 50, title: 'Sort', align: 'center' }} > <SchemaField.Void x-decorator="FormItem" x-component="ArrayTable.SortHandle" /> </SchemaField.Void> <SchemaField.Void x-component="ArrayTable.Column" x-component-props={{ width: 80, title: 'Index', align: 'center' }} > <SchemaField.String x-decorator="FormItem" x-component="ArrayTable.Index" /> </SchemaField.Void> <SchemaField.Void x-component="ArrayTable.Column" x-component-props={{ title: 'Price' }} > <SchemaField.Number name="price" x-decorator="Editable" required x-component="NumberPicker" x-component-props={{ addonAfter: '$', }} default={0} /> </SchemaField.Void> <SchemaField.Void x-component="ArrayTable.Column" x-component-props={{ title: 'Count' }} > <SchemaField.Number name="count" x-decorator="Editable" required x-component="NumberPicker" default={0} /> </SchemaField.Void> <SchemaField.Void x-component="ArrayTable.Column" x-component-props={{ title: 'Total' }} > <SchemaField.Number x-decorator="FormItem" name="total" x-component="NumberPicker" x-pattern="readPretty" x-component-props={{ addonAfter: '$', }} x-reactions={{ dependencies: ['.price', '.count'], when: '{{$deps[0] && $deps[1]}}', fulfill: { state: { value: '{{$deps[0] * $deps[1]}}', }, }, }} /> </SchemaField.Void> <SchemaField.Void x-component="ArrayTable.Column" x-component-props={{ title: 'Operations', dataIndex: 'operations', width: 200, fixed: 'right', }} > <SchemaField.Void x-component="FormItem"> <SchemaField.Void x-component="ArrayTable.Remove" /> <SchemaField.Void x-component="ArrayTable.MoveDown" /> <SchemaField.Void x-component="ArrayTable.MoveUp" /> </SchemaField.Void> </SchemaField.Void> </SchemaField.Object> <SchemaField.Void x-component="ArrayTable.Addition" title="Add" /> </SchemaField.Array> <SchemaField.Number name="total" 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)}}', }, }, }} /> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>提交</Submit> </FormButtonGroup> </Form> ) } ``` ## 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 ( <Form form={form} layout="vertical"> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>submit</Submit> </FormButtonGroup> </Form> ) } ``` ``` -------------------------------------------------------------------------------- /packages/next/src/array-base/index.tsx: -------------------------------------------------------------------------------- ```typescript 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<Button> icon?: React.ReactNode } export interface IArrayBaseContext { props: IArrayBaseProps field: ArrayField schema: Schema } export interface IArrayBaseItemProps { index: number record: ((index: number) => Record<string, any>) | Record<string, any> } export type ArrayBaseMixins = { Addition?: React.FC<React.PropsWithChildren<IArrayBaseAdditionProps>> Remove?: React.FC< React.PropsWithChildren<IArrayBaseOperationProps & { index?: number }> > Copy?: React.FC< React.PropsWithChildren< IArrayBaseAdditionProps & IArrayBaseOperationProps & { index?: number } > > MoveUp?: React.FC< React.PropsWithChildren<IArrayBaseOperationProps & { index?: number }> > MoveDown?: React.FC< React.PropsWithChildren<IArrayBaseOperationProps & { index?: number }> > SortHandle?: React.FC< React.PropsWithChildren<IArrayBaseOperationProps & { index?: number }> > Index?: React.FC useArray?: () => IArrayBaseContext useIndex?: (index?: number) => number useRecord?: (record?: number) => any } export interface IArrayBaseProps { disabled?: boolean onCopy?: (index: number) => void onAdd?: (index: number) => void onRemove?: (index: number) => void onMoveDown?: (index: number) => void onMoveUp?: (index: number) => void } type ComposedArrayBase = React.FC<React.PropsWithChildren<IArrayBaseProps>> & ArrayBaseMixins & { Item?: React.FC<React.PropsWithChildren<IArrayBaseItemProps>> mixin?: <T extends JSXComponent>(target: T) => T & ArrayBaseMixins } const ArrayBaseContext = createContext<IArrayBaseContext>(null) const ItemContext = createContext<IArrayBaseItemProps>(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<ArrayField>() const schema = useFieldSchema() return ( <ArrayBaseContext.Provider value={{ field, schema, props }}> {props.children} </ArrayBaseContext.Provider> ) } ArrayBase.Item = ({ children, ...props }) => { return <ItemContext.Provider value={props}>{children}</ItemContext.Provider> } const SortHandle = SortableHandle((props: any) => { const prefixCls = usePrefixCls('formily-array-base') return ( <MenuOutlinedIcon {...props} className={cls(`${prefixCls}-sort-handle`, props.className)} style={{ ...props.style }} /> ) }) as any ArrayBase.SortHandle = () => { const array = useArray() if (!array) return null if (array.field?.pattern !== 'editable') return null return <SortHandle /> } ArrayBase.Index = (props) => { const index = useIndex() const prefixCls = usePrefixCls('formily-array-base') return ( <span {...props} className={`${prefixCls}-index`}> #{index + 1}. </span> ) } 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 ( <Button {...props} disabled={self?.disabled} className={cls(`${prefixCls}-addition`, props.className)} style={{ display: 'block', width: '100%', ...props.style }} onClick={(e) => { if (array.props?.disabled) return e.stopPropagation() const defaultValue = getDefaultValue(props.defaultValue, array.schema) if (props.method === 'unshift') { array.field?.unshift?.(defaultValue) array.props?.onAdd?.(0) } else { array.field?.push?.(defaultValue) array.props?.onAdd?.(array?.field?.value?.length - 1) } if (props.onClick) { props.onClick(e) } }} > {isUndef(props.icon) ? <PlusOutlinedIcon /> : props.icon} {props.title || self.title} </Button> ) } 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 ( <Button text {...props} disabled={self?.disabled} className={cls( `${prefixCls}-remove`, self?.disabled ? `${prefixCls}-remove-disabled` : '', props.className )} ref={ref} onClick={(e) => { if (self?.disabled) return e.stopPropagation() array.field?.remove?.(index) array.props?.onRemove?.(index) if (props.onClick) { props.onClick(e) } }} > {isUndef(props.icon) ? <DeleteOutlinedIcon /> : props.icon} {props.title || self.title} </Button> ) }) 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 ( <Button text {...props} disabled={self?.disabled} className={cls( `${prefixCls}-copy`, self?.disabled ? `${prefixCls}-copy-disabled` : '', props.className )} ref={ref} onClick={(e) => { if (self?.disabled) return e.stopPropagation() if (array.props?.disabled) return const value = clone(array?.field?.value[index]) const distIndex = index + 1 array.field?.insert?.(distIndex, value) array.props?.onCopy?.(distIndex) if (props.onClick) { props.onClick(e) } }} > {isUndef(props.icon) ? <CopyOutlinedIcon /> : props.icon} {props.title || self.title} </Button> ) }) 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 ( <Button text {...props} disabled={self?.disabled} className={cls( `${prefixCls}-move-down`, self?.disabled ? `${prefixCls}-move-down-disabled` : '', props.className )} ref={ref} onClick={(e) => { if (self?.disabled) return e.stopPropagation() array.field?.moveDown?.(index) array.props?.onMoveDown?.(index) if (props.onClick) { props.onClick(e) } }} > {isUndef(props.icon) ? <DownOutlinedIcon /> : props.icon} {props.title || self.title} </Button> ) }) 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 ( <Button text {...props} disabled={self?.disabled} className={cls( `${prefixCls}-move-up`, self?.disabled ? `${prefixCls}-move-up-disabled` : '', props.className )} ref={ref} onClick={(e) => { if (self?.disabled) return e.stopPropagation() array?.field?.moveUp(index) array?.props?.onMoveUp?.(index) if (props.onClick) { props.onClick(e) } }} > {isUndef(props.icon) ? <UpOutlinedIcon /> : props.icon} {props.title || self.title} </Button> ) }) 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 ``` -------------------------------------------------------------------------------- /packages/antd/src/array-base/index.tsx: -------------------------------------------------------------------------------- ```typescript 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<HTMLElement> } export interface IArrayBaseContext { props: IArrayBaseProps field: ArrayField schema: Schema } export interface IArrayBaseItemProps { index: number record: ((index: number) => Record<string, any>) | Record<string, any> } export type ArrayBaseMixins = { Addition?: React.FC<React.PropsWithChildren<IArrayBaseAdditionProps>> Copy?: React.FC< React.PropsWithChildren<IArrayBaseOperationProps & { index?: number }> > Remove?: React.FC< React.PropsWithChildren<IArrayBaseOperationProps & { index?: number }> > MoveUp?: React.FC< React.PropsWithChildren<IArrayBaseOperationProps & { index?: number }> > MoveDown?: React.FC< React.PropsWithChildren<IArrayBaseOperationProps & { index?: number }> > SortHandle?: React.FC< React.PropsWithChildren<IArrayBaseOperationProps & { index?: number }> > 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<React.PropsWithChildren<IArrayBaseProps>> & ArrayBaseMixins & { Item?: React.FC<React.PropsWithChildren<IArrayBaseItemProps>> mixin?: <T extends JSXComponent>(target: T) => T & ArrayBaseMixins } const ArrayBaseContext = createContext<IArrayBaseContext>(null) const ItemContext = createContext<IArrayBaseItemProps>(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<ArrayField>() const schema = useFieldSchema() return ( <ArrayBaseContext.Provider value={{ field, schema, props }}> {props.children} </ArrayBaseContext.Provider> ) } ArrayBase.Item = ({ children, ...props }) => { return <ItemContext.Provider value={props}>{children}</ItemContext.Provider> } const SortHandle = SortableHandle((props: any) => { const prefixCls = usePrefixCls('formily-array-base') return ( <MenuOutlined {...props} className={cls(`${prefixCls}-sort-handle`, props.className)} style={{ ...props.style }} /> ) }) as any ArrayBase.SortHandle = (props) => { const array = useArray() if (!array) return null if (array.field?.pattern !== 'editable') return null return <SortHandle {...props} /> } ArrayBase.Index = (props) => { const index = useIndex() const prefixCls = usePrefixCls('formily-array-base') return ( <span {...props} className={`${prefixCls}-index`}> #{index + 1}. </span> ) } 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 ( <Button type="dashed" block {...props} disabled={self?.disabled} className={cls(`${prefixCls}-addition`, props.className)} onClick={(e) => { if (array.props?.disabled) return if (props.onClick) { props.onClick(e) if (e.defaultPrevented) return } const defaultValue = getDefaultValue(props.defaultValue, array.schema) if (props.method === 'unshift') { array.field?.unshift?.(defaultValue) array.props?.onAdd?.(0) } else { array.field?.push?.(defaultValue) array.props?.onAdd?.(array?.field?.value?.length - 1) } }} icon={isUndef(props.icon) ? <PlusOutlined /> : props.icon} > {props.title || self.title} </Button> ) } 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 ( <Button type="text" {...props} disabled={self?.disabled} className={cls( `${prefixCls}-copy`, self?.disabled ? `${prefixCls}-copy-disabled` : '', props.className )} ref={ref} onClick={(e) => { if (self?.disabled) return e.stopPropagation() if (array.props?.disabled) return if (props.onClick) { props.onClick(e) if (e.defaultPrevented) return } const value = clone(array?.field?.value[index]) const distIndex = index + 1 array.field?.insert?.(distIndex, value) array.props?.onCopy?.(distIndex) }} icon={isUndef(props.icon) ? <CopyOutlined /> : props.icon} > {props.title || self.title} </Button> ) }) 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 ( <Button type="text" {...props} disabled={self?.disabled} className={cls( `${prefixCls}-remove`, self?.disabled ? `${prefixCls}-remove-disabled` : '', props.className )} ref={ref} onClick={(e) => { if (self?.disabled) return e.stopPropagation() if (props.onClick) { props.onClick(e) if (e.defaultPrevented) return } array.field?.remove?.(index) array.props?.onRemove?.(index) }} icon={isUndef(props.icon) ? <DeleteOutlined /> : props.icon} > {props.title || self.title} </Button> ) }) 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 ( <Button type="text" {...props} disabled={self?.disabled} className={cls( `${prefixCls}-move-down`, self?.disabled ? `${prefixCls}-move-down-disabled` : '', props.className )} ref={ref} onClick={(e) => { if (self?.disabled) return e.stopPropagation() if (props.onClick) { props.onClick(e) if (e.defaultPrevented) return } array.field?.moveDown?.(index) array.props?.onMoveDown?.(index) }} icon={isUndef(props.icon) ? <DownOutlined /> : props.icon} > {props.title || self.title} </Button> ) }) 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 ( <Button type="text" {...props} disabled={self?.disabled} className={cls( `${prefixCls}-move-up`, self?.disabled ? `${prefixCls}-move-up-disabled` : '', props.className )} ref={ref} onClick={(e) => { if (self?.disabled) return e.stopPropagation() if (props.onClick) { props.onClick(e) if (e.defaultPrevented) return } array?.field?.moveUp(index) array?.props?.onMoveUp?.(index) }} icon={isUndef(props.icon) ? <UpOutlined /> : props.icon} > {props.title || self.title} </Button> ) }) 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 ``` -------------------------------------------------------------------------------- /packages/next/docs/components/FormDrawer.md: -------------------------------------------------------------------------------- ```markdown # 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 ( <Button onClick={() => { FormDrawer('Drawer Form', () => { return ( <FormLayout labelCol={6} wrapperCol={14}> <SchemaField> <SchemaField.String name="aaa" required title="input box 1" x-decorator="FormItem" x-component="Input" /> <SchemaField.String name="bbb" required title="input box 2" x-decorator="FormItem" x-component="Input" /> <SchemaField.String name="ccc" required title="input box 3" x-decorator="FormItem" x-component="Input" /> <SchemaField.String name="ddd" required title="input box 4" x-decorator="FormItem" x-component="Input" /> </SchemaField> <FormDrawer.Footer> <FormButtonGroup align="right"> <Submit onSubmit={() => { return new Promise((resolve) => { setTimeout(resolve, 1000) }) }} > Submit </Submit> <Reset>Reset</Reset> </FormButtonGroup> </FormDrawer.Footer> </FormLayout> ) }) .open({ initialValues: { aaa: '123', }, }) .then(console.log) }} > Click me to open the form </Button> ) } ``` ## 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 ( <Button onClick={() => { FormDrawer('Pop-up form', () => { return ( <FormLayout labelCol={6} wrapperCol={14}> <SchemaField schema={schema} /> <FormDrawer.Footer> <FormButtonGroup align="right"> <Submit onSubmit={() => { return new Promise((resolve) => { setTimeout(resolve, 1000) }) }} > Submit </Submit> <Reset>Reset</Reset> </FormButtonGroup> </FormDrawer.Footer> </FormLayout> ) }) .open({ initialValues: { aaa: '123', }, }) .then(console.log) }} > Click me to open the form </Button> ) } ``` ## 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 ( <Button onClick={() => { FormDrawer('Pop-up form', () => { return ( <FormLayout labelCol={6} wrapperCol={14}> <Field name="aaa" required title="input box 1" decorator={[FormItem]} component={[Input]} /> <Field name="bbb" required title="input box 2" decorator={[FormItem]} component={[Input]} /> <Field name="ccc" required title="input box 3" decorator={[FormItem]} component={[Input]} /> <Field name="ddd" required title="input box 4" decorator={[FormItem]} component={[Input]} /> <FormDrawer.Footer> <FormButtonGroup align="right"> <Submit onSubmit={() => { return new Promise((resolve) => { setTimeout(resolve, 1000) }) }} > Submit </Submit> <Reset>Reset</Reset> </FormButtonGroup> </FormDrawer.Footer> </FormLayout> ) }) .open({ initialValues: { aaa: '123', }, }) .then(console.log) }} > Click me to open the form </Button> ) } ``` ## 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 ( <ConfigProvider defaultPropsConfig={{ Drawer: {}, }} > <Button onClick={() => { FormDrawer('Pop-up form', () => { return ( <FormLayout labelCol={6} wrapperCol={14}> <Field name="aaa" required title="input box 1" decorator={[FormItem]} component={[Input]} /> <Field name="bbb" required title="input box 2" decorator={[FormItem]} component={[Input]} /> <Field name="ccc" required title="input box 3" decorator={[FormItem]} component={[Input]} /> <Field name="ddd" required title="input box 4" decorator={[FormItem]} component={[Input]} /> <FormDrawer.Footer> <FormButtonGroup align="right"> <Submit onSubmit={() => { return new Promise((resolve) => { setTimeout(resolve, 1000) }) }} > Submit </Submit> <Reset>Reset</Reset> </FormButtonGroup> </FormDrawer.Footer> </FormLayout> ) }) .open({ initialValues: { aaa: '123', }, }) .then(console.log) }} > Click me to open the form </Button> </ConfigProvider> ) } ``` ## 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 ): 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<any> //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 ``` -------------------------------------------------------------------------------- /packages/reactive/src/__tests__/action.spec.ts: -------------------------------------------------------------------------------- ```typescript 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<any>({}) 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<any>({}) 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) }) ``` -------------------------------------------------------------------------------- /packages/reactive/src/__tests__/collections-map.spec.ts: -------------------------------------------------------------------------------- ```typescript 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<string, Record<string, any>>([])) 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<string, Record<string, any>>([])) 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) }) }) ``` -------------------------------------------------------------------------------- /packages/path/src/__tests__/match.spec.ts: -------------------------------------------------------------------------------- ```typescript import expect from 'expect' import { Path } from '../' import { Matcher } from '../matcher' const match = (obj) => { for (let name in obj) { test('test match ' + name, () => { const path = new Path(name) if (Array.isArray(obj[name]) && Array.isArray(obj[name][0])) { obj[name].forEach((_path) => { expect(path.match(_path)).toBeTruthy() }) } else { expect(path.match(obj[name])).toBeTruthy() } }) } } const unmatch = (obj) => { for (let name in obj) { test('test unmatch ' + name, () => { const path = new Path(name) if (Array.isArray(obj[name]) && Array.isArray(obj[name][0])) { obj[name].forEach((_path) => { expect(path.match(_path)).toBeFalsy() }) } else { expect(path.match(obj[name])).toBeFalsy() } }) } } test('basic match', () => { expect(Path.parse('xxx').match('')).toBeFalsy() expect(Path.parse('xxx').match('aaa')).toBeFalsy() expect(Path.parse('xxx.eee').match('xxx')).toBeFalsy() expect(Path.parse('*(xxx.eee~)').match('xxx')).toBeFalsy() expect(Path.parse('xxx.eee~').match('xxx.eee')).toBeTruthy() expect(Path.parse('*(!xxx.eee,yyy)').match('xxx')).toBeFalsy() expect(Path.parse('*(!xxx.eee,yyy)').match('xxx.ooo.ppp')).toBeTruthy() expect(Path.parse('*(!xxx.eee,yyy)').match('xxx.eee')).toBeFalsy() expect(Path.parse('*(!xxx.eee~,yyy)').match('xxx.eee')).toBeFalsy() expect(Path.parse('~.aa').match('xxx.aa')).toBeTruthy() }) test('not expect match not', () => { expect(new Matcher({}).match(['']).matched).toBeFalsy() }) test('test matchGroup', () => { const pattern = new Path('*(aa,bb,cc)') expect(pattern.matchAliasGroup('aa', 'bb')).toEqual(true) const excludePattern = new Path('aa.bb.*(11,22,33).*(!aa,bb,cc)') expect( excludePattern.matchAliasGroup('aa.bb.11.mm', 'aa.cc.dd.bb.11.mm') ).toEqual(true) expect(excludePattern.matchAliasGroup('aa.cc', 'aa.kk.cc')).toEqual(false) expect(new Path('aa.*(!bb)').matchAliasGroup('kk.mm.aa.bb', 'aa.bb')).toEqual( false ) expect( new Path('aa.*(!bb)').matchAliasGroup('kk.mm.aa.bb.cc', 'kk.mm.aa') ).toEqual(false) expect(new Path('aa.*(!bb,oo)').matchAliasGroup('kk.mm', 'aa')).toEqual(false) expect(new Path('aa.*(!bb.*)').matchAliasGroup('kk.mm', 'aa')).toEqual(false) expect(new Path('aa.*(!bb)').matchAliasGroup('kk.mm.aa.cc', 'aa.cc')).toEqual( true ) const patttern2 = Path.parse('*(array)') expect(patttern2.matchAliasGroup(['array', 0], ['array', 0])).toEqual(false) }) test('exclude match', () => { //路径长度相等 expect(Path.parse('*(!aaa)').match('ggg')).toBeTruthy() expect(Path.parse('*(!aaa)').match('aaa')).toBeFalsy() expect(Path.parse('*(!aaa.bbb)').match('ggg.ddd')).toBeTruthy() expect(Path.parse('*(!aaa.ccc)').match('aaa.ccc')).toBeFalsy() //长路径匹配短路径 expect(Path.parse('*(!aaa.bbb)').match('ggg')).toBeTruthy() expect(Path.parse('*(!aaa.bbb)').match('aaa')).toBeFalsy() //短路径匹配长路径 expect(Path.parse('*(!aaa)').match('aaa.bbb')).toBeTruthy() expect(Path.parse('*(!aaa)').match('aaa.ccc')).toBeTruthy() expect(Path.parse('*(!aaa)').match('bbb.ccc')).toBeTruthy() expect(Path.parse('*(!aaa,bbb)').match('bbb')).toBeFalsy() expect(Path.parse('*(!aaa.bbb)').match('aaa.ccc')).toBeTruthy() expect(Path.parse('*(!basic.name,versionTag)').match('basic.id')).toBeTruthy() expect(Path.parse('*(!basic.name,versionTag)').match('basic')).toBeFalsy() expect( Path.parse('*(!basic.name,versionTag)').match('isExecutable') ).toBeTruthy() expect( Path.parse('*(!basic.name,versionTag)').match('versionTag') ).toBeFalsy() expect( Path.parse('*(!basic.name,basic.name.*,versionTag)').match('basic.name') ).toBeFalsy() expect( Path.parse('*(!basic.name,basic.name.*,versionTag)').match('basic.name.kkk') ).toBeFalsy() expect(Path.parse('aa.*(!bb)').match('kk.mm.aa.bb.cc')).toBeFalsy() expect(Path.parse('aa.*(!bb)').match('aa')).toBeFalsy() expect(Path.parse('aa.*(!bb.*)').match('aa')).toBeFalsy() expect(Path.parse('aa.*(!bb,cc)').match('aa')).toBeFalsy() expect(Path.parse('aa.*(!bb,cc)').match('aa.dd')).toBeTruthy() expect(Path.parse('aa.*(!bb,cc)').match('aa.kk')).toBeTruthy() }) test('match regexp', () => { expect(Path.parse(/^\d+$/).match('212')).toBeTruthy() expect(Path.parse(/^\d+$/).match('212dd')).toBeFalsy() }) test('test zero', () => { expect(Path.parse('t.0.value~').match(['t', 0, 'value_list'])).toEqual(true) }) test('test optional wild match', () => { expect(Path.parse('aa.**').match(['aa'])).toEqual(true) expect(Path.parse('aa.**').match(['aa', 'bb', 'cc'])).toEqual(true) expect(Path.parse('aa.*').match(['aa'])).toEqual(false) expect(Path.parse('aa.\\*\\*\\.aa').match(['aa', '**.aa'])).toEqual(true) expect(Path.parse('aa.[[**.aa]]').match(['aa', '**.aa'])).toEqual(true) expect(() => Path.parse('aa.**.aa').match(['aa'])).toThrowError() expect(() => Path.parse('aa.**(bb)').match(['aa'])).toThrowError() expect(Path.parse('*(aa.**)').match(['aa'])).toEqual(true) expect(Path.parse('*(aa.**,bb.**)').match(['aa'])).toEqual(true) expect(Path.parse('*(aa.**,bb.**)').match(['aa', 'bb', 'cc'])).toEqual(true) expect(Path.parse('*(aa.**,bb.**)').match(['bb'])).toEqual(true) expect(Path.parse('*(aa.**,bb.**)').match(['bb', 'cc', 'dd'])).toEqual(true) expect(Path.parse('*(aa.**,bb.**)').match(['cc'])).toEqual(false) expect(Path.parse('*(aa.**,bb.**).bb').match(['aa', 'oo'])).toEqual(true) expect(Path.parse('*(aa.**,bb.**).bb').match(['bb', 'oo'])).toEqual(true) expect(Path.parse('*(aa.**,bb.**).bb').match(['aa', 'oo', 'bb'])).toEqual( true ) expect(Path.parse('*(aa.**,bb.**).bb').match(['bb', 'oo', 'bb'])).toEqual( true ) expect( Path.parse('*(aa.**,bb.**).bb').match(['aa', 'oo', 'kk', 'dd', 'bb']) ).toEqual(true) expect( Path.parse('*(aa.**,bb.**).bb').match(['cc', 'oo', 'kk', 'dd', 'bb']) ).toEqual(false) expect( Path.parse('*(aa.**,bb.**).bb').match(['bb', 'oo', 'kk', 'dd', 'bb']) ).toEqual(true) expect( Path.parse('*(aa.**,bb.**).bb').match(['kk', 'oo', 'kk', 'dd', 'bb']) ).toEqual(false) }) test('test expand', () => { expect( Path.parse('t.0.value~').match(['t', 0, 'value_list', 'hello']) ).toEqual(false) }) test('test multi expand', () => { expect(Path.parse('*(aa~,bb~).*').match(['aa12323', 'asdasd'])).toEqual(true) }) test('test group', () => { const node = Path.parse('*(phases.*.type,phases.*.steps.*.type)') expect(node.match('phases.0.steps.1.type')).toBeTruthy() }) test('test segments', () => { const node = Path.parse('a.0.b') expect(node.match(['a', 0, 'b'])).toEqual(true) }) test('nested group match', () => { expect( Path.parse('aa.*.*(bb,cc).dd.*(kk,oo).ee').match('aa.0.cc.dd.kk.ee') ).toEqual(true) }) test('group match with destructor', () => { expect(Path.parse('*([startDate,endDate],date,weak)').match('date')).toEqual( true ) expect(Path.parse('*({startDate,endDate},date,weak)').match('date')).toEqual( true ) expect(Path.parse('*([startDate,endDate],date,weak)').match('xxx')).toEqual( false ) expect(Path.parse('*({startDate,endDate},date,weak)').match('xxx')).toEqual( false ) expect( Path.parse('*([startDate,endDate],date,weak)').match('[startDate,endDate]') ).toEqual(true) expect( Path.parse('*({startDate,endDate},date,weak)').match('{startDate,endDate}') ).toEqual(true) }) test('all range match', () => { expect( Path.parse('array.*[:].*[:].*[:].bb').match('array.0.0.0.aa') ).toBeFalsy() }) match({ '*': [[], ['aa'], ['aa', 'bb', 'cc'], ['aa', 'dd', 'gg']], '*.a.b': [ ['c', 'a', 'b'], ['k', 'a', 'b'], ['m', 'a', 'b'], ], 'a.*.k': [ ['a', 'b', 'k'], ['a', 'd', 'k'], ['a', 'c', 'k'], ], 'a.*(b,d,m).k': [ ['a', 'b', 'k'], ['a', 'd', 'k'], ['a', 'm', 'k'], ], 'a.*(!b,d,m).*(!a,b)': [ ['a', 'o', 'k'], ['a', 'q', 'k'], ['a', 'c', 'k'], ], 'a.*(b.c.d,d,m).k': [ ['a', 'b', 'c', 'd', 'k'], ['a', 'd', 'k'], ['a', 'm', 'k'], ], 'a.*(b.*(c,k).d,d,m).k': [ ['a', 'b', 'c', 'd', 'k'], ['a', 'b', 'k', 'd', 'k'], ['a', 'd', 'k'], ['a', 'm', 'k'], ], 'a.b.*': [ ['a', 'b', 'c', 'd'], ['a', 'b', 'c'], ['a', 'b', 2, 'aaa', 3, 'bbb'], ], '*(step1,step2).*': [ ['step1', 'aa', 'bb'], ['step1', 'aa', 'bb', 'ccc', 'ddd'], ], 'dyanmic.*(!dynamic-1)': [ ['dyanmic', 'dynamic-2'], ['dyanmic', 'dynamic-3'], ], 't.0.value~': [['t', '0', 'value']], 'a.*[10:50].*(!a,b)': [ ['a', 49, 's'], ['a', 10, 's'], ['a', 50, 's'], ], 'a.*[10:].*(!a,b)': [ ['a', 49, 's'], ['a', 10, 's'], ['a', 50, 's'], ], 'a.*[].*(!a,b)': [ ['a', 49, 's'], ['a', 10, 's'], ['a', 50, 's'], ], 'a.*[:50].*(!a,b)': [ ['a', 49, 's'], ['a', 10, 's'], ['a', 50, 's'], ], 'a.*([[a.b.c]],[[c.b.d~]])': [ ['a', '[[a.b.c]]'], ['a', 'c.b.d~'], ], 'a.*(!k,d,m).k': [ ['a', 'u', 'k'], ['a', 'o', 'k'], ['a', 'p', 'k'], ], 'a\\.\\*\\[1\\]': [['a.*[1]']], '[[\\[aa,bb\\]]]': [['[aa,bb]']], '[[\\[aa,bb\\] ]]': [['[aa,bb] ']], '[[ \\[aa,bb~\\] ]]': [[' [aa,bb~] ']], 'aa.bb.*': [['aa', 'bb', 'ccc']], 'a.*': [ ['a', 'b'], ['a', 'b', 'c'], ], 'aa.*.*(bb,cc).dd': [['aa', '0', 'cc', 'dd']], 'aaa.products.0.*': [['aaa', 'products', '0', 'aaa']], 'aa~.ccc': [ ['aa', 'ccc'], ['aa12', 'ccc'], ], '*(aa~,bb~).*': [ ['aa12323', 'asdasd'], ['bb12222', 'asd'], ], '*(aa,bb,bb.aa)': [['bb', 'aa']], '*(!aa,bb,bb.aa)': [['xx'], ['yyy']], '*(!aaa)': [['bbb']], '*(!aaa,bbb)': [['ccc'], ['ggg']], '*([startDate,endDate],date,weak)': [['date']], }) unmatch({ 'a.*': [['a'], ['b']], '*(array)': [['array', '0']], 'aa.bb.*': [['aa', 'bb']], 'a.*.b': [['a', 'k', 'b', 'd']], '*(!aaa)': [['aaa']], 'dyanmic.*(!dynamic-1)': [['dyanmic', 'dynamic-1']], 'dyanmic.*(!dynamic-1.*)': [['dyanmic', 'dynamic-1', 'ccc']], a: [['c', 'b']], 'aa~.ccc': [['a', 'ccc'], ['aa'], ['aaasdd']], bb: [['bb', 'cc']], 'aa.*(cc,bb).*.aa': [['aa', 'cc', '0', 'bb']], }) ``` -------------------------------------------------------------------------------- /packages/next/src/array-table/index.tsx: -------------------------------------------------------------------------------- ```typescript 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<PaginationProps, 'children'> { 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<ExtendTableProps> & ArrayBaseMixins & { Column?: ReactFC<ColumnProps> } 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 = ( <ArrayBase.Item key={index} index={index} record={() => field.value?.[index]} > <RecursionField schema={schema} name={index} onlyRenderProperties /> </ArrayBase.Item> ) return children }, }) }, []) } const useAddition = () => { const schema = useFieldSchema() return schema.reduceProperties((addition, schema, key) => { if (isAdditionComponent(schema)) { return <RecursionField schema={schema} name={key} /> } return addition }, null) } const schedulerRequest = { request: null, } const StatusSelect: ReactFC<IStatusSelectProps> = observer( ({ pageSize, ...props }) => { const field = useField<ArrayField>() 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 ? <Badge dot>{label}</Badge> : label, value, } }) return ( <Select {...props} value={props.value} onChange={props.onChange} dataSource={options} useVirtual className={cls(`${prefixCls}-status-select`, { 'has-error': errors?.length, })} /> ) }, { scheduler: (update) => { clearTimeout(schedulerRequest.request) schedulerRequest.request = setTimeout(() => { update() }, 100) }, } ) const PaginationContext = createContext<PaginationAction>({}) const usePagination = () => { return useContext(PaginationContext) } const ArrayTablePagination: ReactFC<IArrayTablePaginationProps> = ({ children, dataSource, ...props }) => { const [current, setCurrent] = useState(1) const prefixCls = usePrefixCls('formily-array-table') const pageSize = props.pageSize || 10 const size = props.size || 'medium' const sources = dataSource || [] const startIndex = (current - 1) * pageSize const endIndex = startIndex + pageSize - 1 const total = sources?.length || 0 const totalPage = Math.ceil(total / pageSize) const pages = Array.from(new Array(totalPage)).map((_, index) => { const page = index + 1 return { label: page, value: page, } }) const handleChange = (current: number) => { setCurrent(current) } useEffect(() => { if (totalPage > 0 && totalPage < current) { handleChange(totalPage) } }, [totalPage, current]) const renderPagination = () => { if (totalPage <= 1) return return ( <div className={`${prefixCls}-pagination`}> <StatusSelect value={current} pageSize={pageSize} onChange={handleChange} dataSource={pages} notFoundContent={false} /> <Pagination {...props} pageSize={pageSize} current={current} total={dataSource.length} size={size} pageSizeSelector={false} onChange={handleChange} /> </div> ) } return ( <Fragment> <PaginationContext.Provider value={{ totalPage, pageSize, changePage: handleChange }} > {children?.( dataSource?.slice(startIndex, endIndex + 1), renderPagination() )} </PaginationContext.Provider> </Fragment> ) } const omit = (props: any, keys?: string[]) => { return Object.keys(props) .filter((key) => !keys?.includes(key)) .reduce((buf, key) => { buf[key] = props[key] return buf }, {}) } export const ArrayTable: ComposedArrayTable = observer( (props: ExtendTableProps) => { const ref = useRef<HTMLDivElement>() const field = useField<ArrayField>() const prefixCls = usePrefixCls('formily-array-table') const dataSource = Array.isArray(field.value) ? field.value.slice() : [] const sources = useArrayTableSources() const columns = useArrayTableColumns(dataSource, field, sources) const pagination = isBool(props.pagination) ? {} : props.pagination const { onAdd, onCopy, onRemove, onMoveDown, onMoveUp } = props const addition = useAddition() return ( <ArrayTablePagination {...pagination} dataSource={dataSource}> {(dataSource, pager) => ( <div ref={ref} className={prefixCls}> <ArrayBase onAdd={onAdd} onCopy={onCopy} onRemove={onRemove} onMoveUp={onMoveUp} onMoveDown={onMoveDown} > <Table size="small" {...omit(props, ['value', 'onChange', 'pagination'])} columns={columns} dataSource={dataSource} /> <div style={{ marginTop: 5, marginBottom: 5 }}>{pager}</div> {sources.map((column, key) => { //专门用来承接对Column的状态管理 if (!isColumnComponent(column.schema)) return return React.createElement(RecursionField, { name: column.name, schema: column.schema, onlyRenderSelf: true, key, }) })} {addition} </ArrayBase> </div> )} </ArrayTablePagination> ) } ) ArrayTable.displayName = 'ArrayTable' ArrayTable.Column = () => { return <Fragment /> } ArrayBase.mixin(ArrayTable) const Addition: ArrayBaseMixins['Addition'] = (props) => { const array = ArrayBase.useArray() const { totalPage = 0, pageSize = 10, changePage } = usePagination() return ( <ArrayBase.Addition {...props} onClick={(e) => { // 如果添加数据后将超过当前页,则自动切换到下一页 const total = array?.field?.value.length || 0 if (total === totalPage * pageSize + 1 && isFn(changePage)) { changePage(totalPage + 1) } props.onClick?.(e) }} /> ) } ArrayTable.Addition = Addition export default ArrayTable ```