This is page 26 of 52. Use http://codebase.md/alibaba/formily?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .all-contributorsrc ├── .codecov.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ └── config.yml │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows │ ├── check-pr-title.yml │ ├── ci.yml │ ├── commitlint.yml │ ├── issue-open-check.yml │ ├── package-size.yml │ └── pr-welcome.yml ├── .gitignore ├── .prettierrc.js ├── .umirc.js ├── .vscode │ └── cspell.json ├── .yarnrc ├── CHANGELOG.md ├── commitlint.config.js ├── devtools │ ├── .eslintrc │ └── chrome-extension │ ├── .npmignore │ ├── assets │ │ └── img │ │ ├── loading.svg │ │ └── logo │ │ ├── 128x128.png │ │ ├── 16x16.png │ │ ├── 38x38.png │ │ ├── 48x48.png │ │ ├── error.png │ │ ├── gray.png │ │ └── scalable.png │ ├── config │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── LICENSE.md │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── components │ │ │ │ ├── FieldTree.tsx │ │ │ │ ├── filter.ts │ │ │ │ ├── LeftPanel.tsx │ │ │ │ ├── RightPanel.tsx │ │ │ │ ├── SearchBox.tsx │ │ │ │ └── Tabs.tsx │ │ │ ├── demo.tsx │ │ │ └── index.tsx │ │ └── extension │ │ ├── backend.ts │ │ ├── background.ts │ │ ├── content.ts │ │ ├── devpanel.tsx │ │ ├── devtools.tsx │ │ ├── inject.ts │ │ ├── manifest.json │ │ ├── popup.tsx │ │ └── views │ │ ├── devpanel.ejs │ │ ├── devtools.ejs │ │ └── popup.ejs │ ├── tsconfig.build.json │ └── tsconfig.json ├── docs │ ├── functions │ │ ├── contributors.ts │ │ └── npm-search.ts │ ├── guide │ │ ├── advanced │ │ │ ├── async.md │ │ │ ├── async.zh-CN.md │ │ │ ├── build.md │ │ │ ├── build.zh-CN.md │ │ │ ├── business-logic.md │ │ │ ├── business-logic.zh-CN.md │ │ │ ├── calculator.md │ │ │ ├── calculator.zh-CN.md │ │ │ ├── controlled.md │ │ │ ├── controlled.zh-CN.md │ │ │ ├── custom.md │ │ │ ├── custom.zh-CN.md │ │ │ ├── destructor.md │ │ │ ├── destructor.zh-CN.md │ │ │ ├── input.less │ │ │ ├── layout.md │ │ │ ├── layout.zh-CN.md │ │ │ ├── linkages.md │ │ │ ├── linkages.zh-CN.md │ │ │ ├── validate.md │ │ │ └── validate.zh-CN.md │ │ ├── contribution.md │ │ ├── contribution.zh-CN.md │ │ ├── form-builder.md │ │ ├── form-builder.zh-CN.md │ │ ├── index.md │ │ ├── index.zh-CN.md │ │ ├── issue-helper.md │ │ ├── issue-helper.zh-CN.md │ │ ├── learn-formily.md │ │ ├── learn-formily.zh-CN.md │ │ ├── quick-start.md │ │ ├── quick-start.zh-CN.md │ │ ├── scenes │ │ │ ├── dialog-drawer.md │ │ │ ├── dialog-drawer.zh-CN.md │ │ │ ├── edit-detail.md │ │ │ ├── edit-detail.zh-CN.md │ │ │ ├── index.less │ │ │ ├── login-register.md │ │ │ ├── login-register.zh-CN.md │ │ │ ├── more.md │ │ │ ├── more.zh-CN.md │ │ │ ├── query-list.md │ │ │ ├── query-list.zh-CN.md │ │ │ ├── step-form.md │ │ │ ├── step-form.zh-CN.md │ │ │ ├── tab-form.md │ │ │ ├── tab-form.zh-CN.md │ │ │ └── VerifyCode.tsx │ │ ├── upgrade.md │ │ └── upgrade.zh-CN.md │ ├── index.md │ ├── index.zh-CN.md │ └── site │ ├── Contributors.less │ ├── Contributors.tsx │ ├── QrCode.less │ ├── QrCode.tsx │ ├── Section.less │ ├── Section.tsx │ └── styles.less ├── global.config.ts ├── jest.config.js ├── lerna.json ├── LICENSE.md ├── package.json ├── packages │ ├── .eslintrc │ ├── antd │ │ ├── __tests__ │ │ │ ├── moment.spec.ts │ │ │ └── sideEffects.spec.ts │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── build-style.ts │ │ ├── create-style.ts │ │ ├── docs │ │ │ ├── components │ │ │ │ ├── ArrayCards.md │ │ │ │ ├── ArrayCards.zh-CN.md │ │ │ │ ├── ArrayCollapse.md │ │ │ │ ├── ArrayCollapse.zh-CN.md │ │ │ │ ├── ArrayItems.md │ │ │ │ ├── ArrayItems.zh-CN.md │ │ │ │ ├── ArrayTable.md │ │ │ │ ├── ArrayTable.zh-CN.md │ │ │ │ ├── ArrayTabs.md │ │ │ │ ├── ArrayTabs.zh-CN.md │ │ │ │ ├── Cascader.md │ │ │ │ ├── Cascader.zh-CN.md │ │ │ │ ├── Checkbox.md │ │ │ │ ├── Checkbox.zh-CN.md │ │ │ │ ├── DatePicker.md │ │ │ │ ├── DatePicker.zh-CN.md │ │ │ │ ├── Editable.md │ │ │ │ ├── Editable.zh-CN.md │ │ │ │ ├── Form.md │ │ │ │ ├── Form.zh-CN.md │ │ │ │ ├── FormButtonGroup.md │ │ │ │ ├── FormButtonGroup.zh-CN.md │ │ │ │ ├── FormCollapse.md │ │ │ │ ├── FormCollapse.zh-CN.md │ │ │ │ ├── FormDialog.md │ │ │ │ ├── FormDialog.zh-CN.md │ │ │ │ ├── FormDrawer.md │ │ │ │ ├── FormDrawer.zh-CN.md │ │ │ │ ├── FormGrid.md │ │ │ │ ├── FormGrid.zh-CN.md │ │ │ │ ├── FormItem.md │ │ │ │ ├── FormItem.zh-CN.md │ │ │ │ ├── FormLayout.md │ │ │ │ ├── FormLayout.zh-CN.md │ │ │ │ ├── FormStep.md │ │ │ │ ├── FormStep.zh-CN.md │ │ │ │ ├── FormTab.md │ │ │ │ ├── FormTab.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ ├── index.zh-CN.md │ │ │ │ ├── Input.md │ │ │ │ ├── Input.zh-CN.md │ │ │ │ ├── NumberPicker.md │ │ │ │ ├── NumberPicker.zh-CN.md │ │ │ │ ├── Password.md │ │ │ │ ├── Password.zh-CN.md │ │ │ │ ├── PreviewText.md │ │ │ │ ├── PreviewText.zh-CN.md │ │ │ │ ├── Radio.md │ │ │ │ ├── Radio.zh-CN.md │ │ │ │ ├── Reset.md │ │ │ │ ├── Reset.zh-CN.md │ │ │ │ ├── Select.md │ │ │ │ ├── Select.zh-CN.md │ │ │ │ ├── SelectTable.md │ │ │ │ ├── SelectTable.zh-CN.md │ │ │ │ ├── Space.md │ │ │ │ ├── Space.zh-CN.md │ │ │ │ ├── Submit.md │ │ │ │ ├── Submit.zh-CN.md │ │ │ │ ├── Switch.md │ │ │ │ ├── Switch.zh-CN.md │ │ │ │ ├── TimePicker.md │ │ │ │ ├── TimePicker.zh-CN.md │ │ │ │ ├── Transfer.md │ │ │ │ ├── Transfer.zh-CN.md │ │ │ │ ├── TreeSelect.md │ │ │ │ ├── TreeSelect.zh-CN.md │ │ │ │ ├── Upload.md │ │ │ │ └── Upload.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __builtins__ │ │ │ │ ├── hooks │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useClickAway.ts │ │ │ │ │ └── usePrefixCls.ts │ │ │ │ ├── index.ts │ │ │ │ ├── loading.ts │ │ │ │ ├── moment.ts │ │ │ │ ├── pickDataProps.ts │ │ │ │ ├── portal.tsx │ │ │ │ ├── render.ts │ │ │ │ └── sort.tsx │ │ │ ├── array-base │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-cards │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-collapse │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-items │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-table │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-tabs │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── cascader │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── checkbox │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── date-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── editable │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-button-group │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-collapse │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-dialog │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-drawer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-grid │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-item │ │ │ │ ├── animation.less │ │ │ │ ├── grid.less │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-layout │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ ├── style.ts │ │ │ │ └── useResponsiveFormLayout.ts │ │ │ ├── form-step │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-tab │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── index.ts │ │ │ ├── input │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── number-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── password │ │ │ │ ├── index.tsx │ │ │ │ ├── PasswordStrength.tsx │ │ │ │ └── style.ts │ │ │ ├── preview-text │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── radio │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── reset │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select-table │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ ├── style.ts │ │ │ │ ├── useCheckSlackly.tsx │ │ │ │ ├── useFilterOptions.tsx │ │ │ │ ├── useFlatOptions.tsx │ │ │ │ ├── useSize.tsx │ │ │ │ ├── useTitleAddon.tsx │ │ │ │ └── utils.ts │ │ │ ├── space │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── style.less │ │ │ ├── style.ts │ │ │ ├── submit │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── switch │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── time-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── transfer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── tree-select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ └── upload │ │ │ ├── index.tsx │ │ │ ├── placeholder.ts │ │ │ └── style.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── benchmark │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ └── index.tsx │ │ ├── template.ejs │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── core │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── docs │ │ │ ├── api │ │ │ │ ├── entry │ │ │ │ │ ├── ActionResponse.less │ │ │ │ │ ├── ActionResponse.tsx │ │ │ │ │ ├── createForm.md │ │ │ │ │ ├── createForm.zh-CN.md │ │ │ │ │ ├── FieldEffectHooks.md │ │ │ │ │ ├── FieldEffectHooks.zh-CN.md │ │ │ │ │ ├── FormChecker.md │ │ │ │ │ ├── FormChecker.zh-CN.md │ │ │ │ │ ├── FormEffectHooks.md │ │ │ │ │ ├── FormEffectHooks.zh-CN.md │ │ │ │ │ ├── FormHooksAPI.md │ │ │ │ │ ├── FormHooksAPI.zh-CN.md │ │ │ │ │ ├── FormPath.md │ │ │ │ │ ├── FormPath.zh-CN.md │ │ │ │ │ ├── FormValidatorRegistry.md │ │ │ │ │ └── FormValidatorRegistry.zh-CN.md │ │ │ │ └── models │ │ │ │ ├── ArrayField.md │ │ │ │ ├── ArrayField.zh-CN.md │ │ │ │ ├── Field.md │ │ │ │ ├── Field.zh-CN.md │ │ │ │ ├── Form.md │ │ │ │ ├── Form.zh-CN.md │ │ │ │ ├── ObjectField.md │ │ │ │ ├── ObjectField.zh-CN.md │ │ │ │ ├── Query.md │ │ │ │ ├── Query.zh-CN.md │ │ │ │ ├── VoidField.md │ │ │ │ └── VoidField.zh-CN.md │ │ │ ├── guide │ │ │ │ ├── architecture.md │ │ │ │ ├── architecture.zh-CN.md │ │ │ │ ├── field.md │ │ │ │ ├── field.zh-CN.md │ │ │ │ ├── form.md │ │ │ │ ├── form.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ ├── index.zh-CN.md │ │ │ │ ├── mvvm.md │ │ │ │ ├── mvvm.zh-CN.md │ │ │ │ ├── values.md │ │ │ │ └── values.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── array.spec.ts │ │ │ │ ├── effects.spec.ts │ │ │ │ ├── externals.spec.ts │ │ │ │ ├── field.spec.ts │ │ │ │ ├── form.spec.ts │ │ │ │ ├── graph.spec.ts │ │ │ │ ├── heart.spec.ts │ │ │ │ ├── internals.spec.ts │ │ │ │ ├── lifecycle.spec.ts │ │ │ │ ├── object.spec.ts │ │ │ │ ├── shared.ts │ │ │ │ └── void.spec.ts │ │ │ ├── effects │ │ │ │ ├── index.ts │ │ │ │ ├── onFieldEffects.ts │ │ │ │ └── onFormEffects.ts │ │ │ ├── global.d.ts │ │ │ ├── index.ts │ │ │ ├── models │ │ │ │ ├── ArrayField.ts │ │ │ │ ├── BaseField.ts │ │ │ │ ├── Field.ts │ │ │ │ ├── Form.ts │ │ │ │ ├── Graph.ts │ │ │ │ ├── Heart.ts │ │ │ │ ├── index.ts │ │ │ │ ├── LifeCycle.ts │ │ │ │ ├── ObjectField.ts │ │ │ │ ├── Query.ts │ │ │ │ ├── types.ts │ │ │ │ └── VoidField.ts │ │ │ ├── shared │ │ │ │ ├── checkers.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── effective.ts │ │ │ │ ├── externals.ts │ │ │ │ └── internals.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── element │ │ ├── .npmignore │ │ ├── build-style.ts │ │ ├── create-style.ts │ │ ├── docs │ │ │ ├── .vuepress │ │ │ │ ├── components │ │ │ │ │ ├── createCodeSandBox.js │ │ │ │ │ ├── dumi-previewer.vue │ │ │ │ │ └── highlight.js │ │ │ │ ├── config.js │ │ │ │ ├── enhanceApp.js │ │ │ │ ├── styles │ │ │ │ │ └── index.styl │ │ │ │ └── util.js │ │ │ ├── demos │ │ │ │ ├── guide │ │ │ │ │ ├── array-cards │ │ │ │ │ │ ├── effects-json-schema.vue │ │ │ │ │ │ ├── effects-markup-schema.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-collapse │ │ │ │ │ │ ├── effects-json-schema.vue │ │ │ │ │ │ ├── effects-markup-schema.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-items │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-table │ │ │ │ │ │ ├── effects-json-schema.vue │ │ │ │ │ │ ├── effects-markup-schema.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-tabs │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── cascader │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── checkbox │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── date-picker │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── editable │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-button-group.vue │ │ │ │ │ ├── form-collapse │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── form-dialog │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-drawer │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-grid │ │ │ │ │ │ ├── form.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── native.vue │ │ │ │ │ ├── form-item │ │ │ │ │ │ ├── bordered-none.vue │ │ │ │ │ │ ├── common.vue │ │ │ │ │ │ ├── feedback.vue │ │ │ │ │ │ ├── inset.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ ├── size.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-layout │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-step │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── form-tab │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── form.vue │ │ │ │ │ ├── input │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── input-number │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── password │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── preview-text │ │ │ │ │ │ ├── base.vue │ │ │ │ │ │ └── extend.vue │ │ │ │ │ ├── radio │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── reset │ │ │ │ │ │ ├── base.vue │ │ │ │ │ │ ├── force.vue │ │ │ │ │ │ └── validate.vue │ │ │ │ │ ├── select │ │ │ │ │ │ ├── json-schema-async.vue │ │ │ │ │ │ ├── json-schema-sync.vue │ │ │ │ │ │ ├── markup-schema-async-search.vue │ │ │ │ │ │ ├── markup-schema-async.vue │ │ │ │ │ │ ├── markup-schema-sync.vue │ │ │ │ │ │ ├── template-async.vue │ │ │ │ │ │ └── template-sync.vue │ │ │ │ │ ├── space │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── submit │ │ │ │ │ │ ├── base.vue │ │ │ │ │ │ └── loading.vue │ │ │ │ │ ├── switch │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── time-picker │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── transfer │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ └── upload │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ └── template.vue │ │ │ │ └── index.vue │ │ │ ├── guide │ │ │ │ ├── array-cards.md │ │ │ │ ├── array-collapse.md │ │ │ │ ├── array-items.md │ │ │ │ ├── array-table.md │ │ │ │ ├── array-tabs.md │ │ │ │ ├── cascader.md │ │ │ │ ├── checkbox.md │ │ │ │ ├── date-picker.md │ │ │ │ ├── editable.md │ │ │ │ ├── form-button-group.md │ │ │ │ ├── form-collapse.md │ │ │ │ ├── form-dialog.md │ │ │ │ ├── form-drawer.md │ │ │ │ ├── form-grid.md │ │ │ │ ├── form-item.md │ │ │ │ ├── form-layout.md │ │ │ │ ├── form-step.md │ │ │ │ ├── form-tab.md │ │ │ │ ├── form.md │ │ │ │ ├── index.md │ │ │ │ ├── input-number.md │ │ │ │ ├── input.md │ │ │ │ ├── password.md │ │ │ │ ├── preview-text.md │ │ │ │ ├── radio.md │ │ │ │ ├── reset.md │ │ │ │ ├── select.md │ │ │ │ ├── space.md │ │ │ │ ├── submit.md │ │ │ │ ├── switch.md │ │ │ │ ├── time-picker.md │ │ │ │ ├── transfer.md │ │ │ │ └── upload.md │ │ │ └── README.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __builtins__ │ │ │ │ ├── configs │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── shared │ │ │ │ │ ├── create-context.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── loading.ts │ │ │ │ │ ├── portal.ts │ │ │ │ │ ├── resolve-component.ts │ │ │ │ │ ├── transform-component.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ └── styles │ │ │ │ └── common.scss │ │ │ ├── array-base │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-cards │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-collapse │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-items │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-table │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-tabs │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── cascader │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── checkbox │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── date-picker │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── editable │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── el-form │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── el-form-item │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── form │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-button-group │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-collapse │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-dialog │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── form-drawer │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-grid │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-item │ │ │ │ ├── animation.scss │ │ │ │ ├── grid.scss │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ ├── style.ts │ │ │ │ └── var.scss │ │ │ ├── form-layout │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ ├── style.ts │ │ │ │ └── useResponsiveFormLayout.ts │ │ │ ├── form-step │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── form-tab │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── index.ts │ │ │ ├── input │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── input-number │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── password │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── preview-text │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── radio │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── reset │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── select │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── space │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── style.ts │ │ │ ├── submit │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── switch │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── time-picker │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── transfer │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ └── upload │ │ │ ├── index.ts │ │ │ └── style.ts │ │ ├── transformer.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── grid │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── index.ts │ │ │ └── observer.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── json-schema │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── schema.spec.ts.snap │ │ │ │ ├── compiler.spec.ts │ │ │ │ ├── patches.spec.ts │ │ │ │ ├── schema.spec.ts │ │ │ │ ├── server-validate.spec.ts │ │ │ │ ├── shared.spec.ts │ │ │ │ ├── transformer.spec.ts │ │ │ │ └── traverse.spec.ts │ │ │ ├── compiler.ts │ │ │ ├── global.d.ts │ │ │ ├── index.ts │ │ │ ├── patches.ts │ │ │ ├── polyfills │ │ │ │ ├── index.ts │ │ │ │ └── SPECIFICATION_1_0.ts │ │ │ ├── schema.ts │ │ │ ├── shared.ts │ │ │ ├── transformer.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── next │ │ ├── __tests__ │ │ │ ├── moment.spec.ts │ │ │ └── sideEffects.spec.ts │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── build-style.ts │ │ ├── create-style.ts │ │ ├── docs │ │ │ ├── components │ │ │ │ ├── ArrayCards.md │ │ │ │ ├── ArrayCards.zh-CN.md │ │ │ │ ├── ArrayCollapse.md │ │ │ │ ├── ArrayCollapse.zh-CN.md │ │ │ │ ├── ArrayItems.md │ │ │ │ ├── ArrayItems.zh-CN.md │ │ │ │ ├── ArrayTable.md │ │ │ │ ├── ArrayTable.zh-CN.md │ │ │ │ ├── Cascader.md │ │ │ │ ├── Cascader.zh-CN.md │ │ │ │ ├── Checkbox.md │ │ │ │ ├── Checkbox.zh-CN.md │ │ │ │ ├── DatePicker.md │ │ │ │ ├── DatePicker.zh-CN.md │ │ │ │ ├── DatePicker2.md │ │ │ │ ├── DatePicker2.zh-CN.md │ │ │ │ ├── Editable.md │ │ │ │ ├── Editable.zh-CN.md │ │ │ │ ├── Form.md │ │ │ │ ├── Form.zh-CN.md │ │ │ │ ├── FormButtonGroup.md │ │ │ │ ├── FormButtonGroup.zh-CN.md │ │ │ │ ├── FormCollapse.md │ │ │ │ ├── FormCollapse.zh-CN.md │ │ │ │ ├── FormDialog.md │ │ │ │ ├── FormDialog.zh-CN.md │ │ │ │ ├── FormDrawer.md │ │ │ │ ├── FormDrawer.zh-CN.md │ │ │ │ ├── FormGrid.md │ │ │ │ ├── FormGrid.zh-CN.md │ │ │ │ ├── FormItem.md │ │ │ │ ├── FormItem.zh-CN.md │ │ │ │ ├── FormLayout.md │ │ │ │ ├── FormLayout.zh-CN.md │ │ │ │ ├── FormStep.md │ │ │ │ ├── FormStep.zh-CN.md │ │ │ │ ├── FormTab.md │ │ │ │ ├── FormTab.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ ├── index.zh-CN.md │ │ │ │ ├── Input.md │ │ │ │ ├── Input.zh-CN.md │ │ │ │ ├── NumberPicker.md │ │ │ │ ├── NumberPicker.zh-CN.md │ │ │ │ ├── Password.md │ │ │ │ ├── Password.zh-CN.md │ │ │ │ ├── PreviewText.md │ │ │ │ ├── PreviewText.zh-CN.md │ │ │ │ ├── Radio.md │ │ │ │ ├── Radio.zh-CN.md │ │ │ │ ├── Reset.md │ │ │ │ ├── Reset.zh-CN.md │ │ │ │ ├── Select.md │ │ │ │ ├── Select.zh-CN.md │ │ │ │ ├── SelectTable.md │ │ │ │ ├── SelectTable.zh-CN.md │ │ │ │ ├── Space.md │ │ │ │ ├── Space.zh-CN.md │ │ │ │ ├── Submit.md │ │ │ │ ├── Submit.zh-CN.md │ │ │ │ ├── Switch.md │ │ │ │ ├── Switch.zh-CN.md │ │ │ │ ├── TimePicker.md │ │ │ │ ├── TimePicker.zh-CN.md │ │ │ │ ├── TimePicker2.md │ │ │ │ ├── TimePicker2.zh-CN.md │ │ │ │ ├── Transfer.md │ │ │ │ ├── Transfer.zh-CN.md │ │ │ │ ├── TreeSelect.md │ │ │ │ ├── TreeSelect.zh-CN.md │ │ │ │ ├── Upload.md │ │ │ │ └── Upload.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LESENCE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __builtins__ │ │ │ │ ├── empty.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useClickAway.ts │ │ │ │ │ └── usePrefixCls.ts │ │ │ │ ├── icons.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── loading.ts │ │ │ │ ├── mapSize.ts │ │ │ │ ├── mapStatus.ts │ │ │ │ ├── moment.ts │ │ │ │ ├── pickDataProps.ts │ │ │ │ ├── portal.tsx │ │ │ │ ├── render.ts │ │ │ │ └── toArray.ts │ │ │ ├── array-base │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-cards │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-collapse │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-items │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-table │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── cascader │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── checkbox │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── date-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── date-picker2 │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── editable │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form-button-group │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form-collapse │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-dialog │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-drawer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-grid │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form-item │ │ │ │ ├── animation.scss │ │ │ │ ├── grid.scss │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ ├── scss │ │ │ │ │ └── variable.scss │ │ │ │ └── style.ts │ │ │ ├── form-layout │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ ├── style.ts │ │ │ │ └── useResponsiveFormLayout.ts │ │ │ ├── form-step │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-tab │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── index.ts │ │ │ ├── input │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── main.scss │ │ │ ├── number-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── password │ │ │ │ ├── index.tsx │ │ │ │ ├── PasswordStrength.tsx │ │ │ │ └── style.ts │ │ │ ├── preview-text │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── radio │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── reset │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select-table │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ ├── style.ts │ │ │ │ ├── useCheckSlackly.tsx │ │ │ │ ├── useFilterOptions.tsx │ │ │ │ ├── useFlatOptions.tsx │ │ │ │ ├── useSize.tsx │ │ │ │ ├── useTitleAddon.tsx │ │ │ │ └── utils.ts │ │ │ ├── space │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── style.ts │ │ │ ├── submit │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── switch │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── time-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── time-picker2 │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── transfer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── tree-select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ └── upload │ │ │ ├── index.tsx │ │ │ ├── main.scss │ │ │ ├── placeholder.ts │ │ │ └── style.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── path │ │ ├── .npmignore │ │ ├── benchmark.ts │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── accessor.spec.ts │ │ │ │ ├── basic.spec.ts │ │ │ │ ├── match.spec.ts │ │ │ │ ├── parser.spec.ts │ │ │ │ └── share.spec.ts │ │ │ ├── contexts.ts │ │ │ ├── destructor.ts │ │ │ ├── index.ts │ │ │ ├── matcher.ts │ │ │ ├── parser.ts │ │ │ ├── shared.ts │ │ │ ├── tokenizer.ts │ │ │ ├── tokens.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── react │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── docs │ │ │ ├── api │ │ │ │ ├── components │ │ │ │ │ ├── ArrayField.md │ │ │ │ │ ├── ArrayField.zh-CN.md │ │ │ │ │ ├── ExpressionScope.md │ │ │ │ │ ├── ExpressionScope.zh-CN.md │ │ │ │ │ ├── Field.md │ │ │ │ │ ├── Field.zh-CN.md │ │ │ │ │ ├── FormConsumer.md │ │ │ │ │ ├── FormConsumer.zh-CN.md │ │ │ │ │ ├── FormProvider.md │ │ │ │ │ ├── FormProvider.zh-CN.md │ │ │ │ │ ├── ObjectField.md │ │ │ │ │ ├── ObjectField.zh-CN.md │ │ │ │ │ ├── RecordScope.md │ │ │ │ │ ├── RecordScope.zh-CN.md │ │ │ │ │ ├── RecordsScope.md │ │ │ │ │ ├── RecordsScope.zh-CN.md │ │ │ │ │ ├── RecursionField.md │ │ │ │ │ ├── RecursionField.zh-CN.md │ │ │ │ │ ├── SchemaField.md │ │ │ │ │ ├── SchemaField.zh-CN.md │ │ │ │ │ ├── VoidField.md │ │ │ │ │ └── VoidField.zh-CN.md │ │ │ │ ├── hooks │ │ │ │ │ ├── useExpressionScope.md │ │ │ │ │ ├── useExpressionScope.zh-CN.md │ │ │ │ │ ├── useField.md │ │ │ │ │ ├── useField.zh-CN.md │ │ │ │ │ ├── useFieldSchema.md │ │ │ │ │ ├── useFieldSchema.zh-CN.md │ │ │ │ │ ├── useForm.md │ │ │ │ │ ├── useForm.zh-CN.md │ │ │ │ │ ├── useFormEffects.md │ │ │ │ │ ├── useFormEffects.zh-CN.md │ │ │ │ │ ├── useParentForm.md │ │ │ │ │ └── useParentForm.zh-CN.md │ │ │ │ └── shared │ │ │ │ ├── connect.md │ │ │ │ ├── connect.zh-CN.md │ │ │ │ ├── context.md │ │ │ │ ├── context.zh-CN.md │ │ │ │ ├── mapProps.md │ │ │ │ ├── mapProps.zh-CN.md │ │ │ │ ├── mapReadPretty.md │ │ │ │ ├── mapReadPretty.zh-CN.md │ │ │ │ ├── observer.md │ │ │ │ ├── observer.zh-CN.md │ │ │ │ ├── Schema.md │ │ │ │ └── Schema.zh-CN.md │ │ │ ├── guide │ │ │ │ ├── architecture.md │ │ │ │ ├── architecture.zh-CN.md │ │ │ │ ├── concept.md │ │ │ │ ├── concept.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ └── index.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── expression.spec.tsx │ │ │ │ ├── field.spec.tsx │ │ │ │ ├── form.spec.tsx │ │ │ │ ├── schema.json.spec.tsx │ │ │ │ ├── schema.markup.spec.tsx │ │ │ │ └── shared.tsx │ │ │ ├── components │ │ │ │ ├── ArrayField.tsx │ │ │ │ ├── ExpressionScope.tsx │ │ │ │ ├── Field.tsx │ │ │ │ ├── FormConsumer.tsx │ │ │ │ ├── FormProvider.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── ObjectField.tsx │ │ │ │ ├── ReactiveField.tsx │ │ │ │ ├── RecordScope.tsx │ │ │ │ ├── RecordsScope.tsx │ │ │ │ ├── RecursionField.tsx │ │ │ │ ├── SchemaField.tsx │ │ │ │ └── VoidField.tsx │ │ │ ├── global.d.ts │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ ├── useAttach.ts │ │ │ │ ├── useExpressionScope.ts │ │ │ │ ├── useField.ts │ │ │ │ ├── useFieldSchema.ts │ │ │ │ ├── useForm.ts │ │ │ │ ├── useFormEffects.ts │ │ │ │ └── useParentForm.ts │ │ │ ├── index.ts │ │ │ ├── shared │ │ │ │ ├── connect.ts │ │ │ │ ├── context.ts │ │ │ │ ├── index.ts │ │ │ │ └── render.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── reactive │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── benchmark.ts │ │ ├── docs │ │ │ ├── api │ │ │ │ ├── action.md │ │ │ │ ├── action.zh-CN.md │ │ │ │ ├── autorun.md │ │ │ │ ├── autorun.zh-CN.md │ │ │ │ ├── batch.md │ │ │ │ ├── batch.zh-CN.md │ │ │ │ ├── define.md │ │ │ │ ├── define.zh-CN.md │ │ │ │ ├── hasCollected.md │ │ │ │ ├── hasCollected.zh-CN.md │ │ │ │ ├── markObservable.md │ │ │ │ ├── markObservable.zh-CN.md │ │ │ │ ├── markRaw.md │ │ │ │ ├── markRaw.zh-CN.md │ │ │ │ ├── model.md │ │ │ │ ├── model.zh-CN.md │ │ │ │ ├── observable.md │ │ │ │ ├── observable.zh-CN.md │ │ │ │ ├── observe.md │ │ │ │ ├── observe.zh-CN.md │ │ │ │ ├── raw.md │ │ │ │ ├── raw.zh-CN.md │ │ │ │ ├── react │ │ │ │ │ ├── observer.md │ │ │ │ │ └── observer.zh-CN.md │ │ │ │ ├── reaction.md │ │ │ │ ├── reaction.zh-CN.md │ │ │ │ ├── toJS.md │ │ │ │ ├── toJS.zh-CN.md │ │ │ │ ├── tracker.md │ │ │ │ ├── tracker.zh-CN.md │ │ │ │ ├── typeChecker.md │ │ │ │ ├── typeChecker.zh-CN.md │ │ │ │ ├── untracked.md │ │ │ │ ├── untracked.zh-CN.md │ │ │ │ └── vue │ │ │ │ ├── observer.md │ │ │ │ └── observer.zh-CN.md │ │ │ ├── guide │ │ │ │ ├── best-practice.md │ │ │ │ ├── best-practice.zh-CN.md │ │ │ │ ├── concept.md │ │ │ │ ├── concept.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ └── index.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── action.spec.ts │ │ │ │ ├── annotations.spec.ts │ │ │ │ ├── array.spec.ts │ │ │ │ ├── autorun.spec.ts │ │ │ │ ├── batch.spec.ts │ │ │ │ ├── collections-map.spec.ts │ │ │ │ ├── collections-set.spec.ts │ │ │ │ ├── collections-weakmap.spec.ts │ │ │ │ ├── collections-weakset.spec.ts │ │ │ │ ├── define.spec.ts │ │ │ │ ├── externals.spec.ts │ │ │ │ ├── hasCollected.spec.ts │ │ │ │ ├── observable.spec.ts │ │ │ │ ├── observe.spec.ts │ │ │ │ ├── tracker.spec.ts │ │ │ │ └── untracked.spec.ts │ │ │ ├── action.ts │ │ │ ├── annotations │ │ │ │ ├── box.ts │ │ │ │ ├── computed.ts │ │ │ │ ├── index.ts │ │ │ │ ├── observable.ts │ │ │ │ ├── ref.ts │ │ │ │ └── shallow.ts │ │ │ ├── array.ts │ │ │ ├── autorun.ts │ │ │ ├── batch.ts │ │ │ ├── checkers.ts │ │ │ ├── environment.ts │ │ │ ├── externals.ts │ │ │ ├── global.d.ts │ │ │ ├── handlers.ts │ │ │ ├── index.ts │ │ │ ├── internals.ts │ │ │ ├── model.ts │ │ │ ├── observable.ts │ │ │ ├── observe.ts │ │ │ ├── reaction.ts │ │ │ ├── tracker.ts │ │ │ ├── tree.ts │ │ │ ├── types.ts │ │ │ └── untracked.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── reactive-react │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ ├── useCompatEffect.ts │ │ │ │ ├── useCompatFactory.ts │ │ │ │ ├── useDidUpdate.ts │ │ │ │ ├── useForceUpdate.ts │ │ │ │ ├── useLayoutEffect.ts │ │ │ │ └── useObserver.ts │ │ │ ├── index.ts │ │ │ ├── observer.ts │ │ │ ├── shared │ │ │ │ ├── gc.ts │ │ │ │ ├── global.ts │ │ │ │ ├── immediate.ts │ │ │ │ └── index.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── reactive-test-cases-for-react18 │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.js │ │ │ └── MySlowList.js │ │ ├── template.ejs │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── reactive-vue │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ └── observer.spec.ts │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ └── useObserver.ts │ │ │ ├── index.ts │ │ │ ├── observer │ │ │ │ ├── collectData.ts │ │ │ │ ├── index.ts │ │ │ │ ├── observerInVue2.ts │ │ │ │ └── observerInVue3.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── shared │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ └── index.spec.ts │ │ │ ├── array.ts │ │ │ ├── case.ts │ │ │ ├── checkers.ts │ │ │ ├── clone.ts │ │ │ ├── compare.ts │ │ │ ├── defaults.ts │ │ │ ├── deprecate.ts │ │ │ ├── global.ts │ │ │ ├── index.ts │ │ │ ├── instanceof.ts │ │ │ ├── isEmpty.ts │ │ │ ├── merge.ts │ │ │ ├── middleware.ts │ │ │ ├── path.ts │ │ │ ├── string.ts │ │ │ ├── subscribable.ts │ │ │ └── uid.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── validator │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── parser.spec.ts │ │ │ │ ├── registry.spec.ts │ │ │ │ └── validator.spec.ts │ │ │ ├── formats.ts │ │ │ ├── index.ts │ │ │ ├── locale.ts │ │ │ ├── parser.ts │ │ │ ├── registry.ts │ │ │ ├── rules.ts │ │ │ ├── template.ts │ │ │ ├── types.ts │ │ │ └── validator.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ └── vue │ ├── .npmignore │ ├── bin │ │ ├── formily-vue-fix.js │ │ └── formily-vue-switch.js │ ├── docs │ │ ├── .vuepress │ │ │ ├── components │ │ │ │ ├── createCodeSandBox.js │ │ │ │ ├── dumi-previewer.vue │ │ │ │ └── highlight.js │ │ │ ├── config.js │ │ │ ├── enhanceApp.js │ │ │ └── styles │ │ │ └── index.styl │ │ ├── api │ │ │ ├── components │ │ │ │ ├── array-field.md │ │ │ │ ├── expression-scope.md │ │ │ │ ├── field.md │ │ │ │ ├── form-consumer.md │ │ │ │ ├── form-provider.md │ │ │ │ ├── object-field.md │ │ │ │ ├── recursion-field-with-component.md │ │ │ │ ├── recursion-field.md │ │ │ │ ├── schema-field-with-schema.md │ │ │ │ ├── schema-field.md │ │ │ │ └── void-field.md │ │ │ ├── hooks │ │ │ │ ├── use-field-schema.md │ │ │ │ ├── use-field.md │ │ │ │ ├── use-form-effects.md │ │ │ │ ├── use-form.md │ │ │ │ └── use-parent-form.md │ │ │ └── shared │ │ │ ├── connect.md │ │ │ ├── injections.md │ │ │ ├── map-props.md │ │ │ ├── map-read-pretty.md │ │ │ ├── observer.md │ │ │ └── schema.md │ │ ├── demos │ │ │ ├── api │ │ │ │ ├── components │ │ │ │ │ ├── array-field.vue │ │ │ │ │ ├── expression-scope.vue │ │ │ │ │ ├── field.vue │ │ │ │ │ ├── form-consumer.vue │ │ │ │ │ ├── form-provider.vue │ │ │ │ │ ├── object-field.vue │ │ │ │ │ ├── recursion-field-with-component.vue │ │ │ │ │ ├── recursion-field.vue │ │ │ │ │ ├── schema-field-with-schema.vue │ │ │ │ │ ├── schema-field.vue │ │ │ │ │ └── void-field.vue │ │ │ │ ├── hooks │ │ │ │ │ ├── use-field-schema.vue │ │ │ │ │ ├── use-field.vue │ │ │ │ │ ├── use-form-effects.vue │ │ │ │ │ ├── use-form.vue │ │ │ │ │ └── use-parent-form.vue │ │ │ │ └── shared │ │ │ │ ├── connect.vue │ │ │ │ ├── map-props.vue │ │ │ │ ├── map-read-pretty.vue │ │ │ │ └── observer.vue │ │ │ ├── index.vue │ │ │ └── questions │ │ │ ├── default-slot.vue │ │ │ ├── events.vue │ │ │ ├── named-slot.vue │ │ │ └── scoped-slot.vue │ │ ├── guide │ │ │ ├── architecture.md │ │ │ ├── concept.md │ │ │ └── README.md │ │ ├── questions │ │ │ └── README.md │ │ └── README.md │ ├── package.json │ ├── README.md │ ├── rollup.config.js │ ├── scripts │ │ ├── postinstall.js │ │ ├── switch-cli.js │ │ └── utils.js │ ├── src │ │ ├── __tests__ │ │ │ ├── expression.scope.spec.ts │ │ │ ├── field.spec.ts │ │ │ ├── form.spec.ts │ │ │ ├── schema.json.spec.ts │ │ │ ├── schema.markup.spec.ts │ │ │ ├── shared.spec.ts │ │ │ └── utils.spec.ts │ │ ├── components │ │ │ ├── ArrayField.ts │ │ │ ├── ExpressionScope.ts │ │ │ ├── Field.ts │ │ │ ├── FormConsumer.ts │ │ │ ├── FormProvider.ts │ │ │ ├── index.ts │ │ │ ├── ObjectField.ts │ │ │ ├── ReactiveField.ts │ │ │ ├── RecursionField.ts │ │ │ ├── SchemaField.ts │ │ │ └── VoidField.ts │ │ ├── global.d.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useAttach.ts │ │ │ ├── useField.ts │ │ │ ├── useFieldSchema.ts │ │ │ ├── useForm.ts │ │ │ ├── useFormEffects.ts │ │ │ ├── useInjectionCleaner.ts │ │ │ └── useParentForm.ts │ │ ├── index.ts │ │ ├── shared │ │ │ ├── connect.ts │ │ │ ├── context.ts │ │ │ ├── createForm.ts │ │ │ ├── fragment.ts │ │ │ ├── h.ts │ │ │ └── index.ts │ │ ├── types │ │ │ └── index.ts │ │ ├── utils │ │ │ ├── formatVNodeData.ts │ │ │ ├── getFieldProps.ts │ │ │ ├── getRawComponent.ts │ │ │ └── resolveSchemaProps.ts │ │ └── vue2-components.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── tsconfig.types.json ├── README.md ├── README.zh-cn.md ├── scripts │ ├── build-style │ │ ├── buildAllStyles.ts │ │ ├── copy.ts │ │ ├── helper.ts │ │ └── index.ts │ └── rollup.base.js ├── tsconfig.build.json ├── tsconfig.jest.json ├── tsconfig.json └── yarn.lock ``` # Files -------------------------------------------------------------------------------- /packages/next/docs/components/FormDrawer.md: -------------------------------------------------------------------------------- ```markdown 1 | # FormDrawer 2 | 3 | > Drawer form, mainly used in simple event to open form scene 4 | 5 | ## Markup Schema example 6 | 7 | ```tsx 8 | import React from 'react' 9 | import { 10 | FormDrawer, 11 | FormItem, 12 | Input, 13 | Submit, 14 | Reset, 15 | FormButtonGroup, 16 | FormLayout, 17 | } from '@formily/next' 18 | import { createSchemaField } from '@formily/react' 19 | import { Button } from '@alifd/next' 20 | 21 | const SchemaField = createSchemaField({ 22 | components: { 23 | FormItem, 24 | Input, 25 | }, 26 | }) 27 | 28 | export default () => { 29 | return ( 30 | <Button 31 | onClick={() => { 32 | FormDrawer('Drawer Form', () => { 33 | return ( 34 | <FormLayout labelCol={6} wrapperCol={14}> 35 | <SchemaField> 36 | <SchemaField.String 37 | name="aaa" 38 | required 39 | title="input box 1" 40 | x-decorator="FormItem" 41 | x-component="Input" 42 | /> 43 | <SchemaField.String 44 | name="bbb" 45 | required 46 | title="input box 2" 47 | x-decorator="FormItem" 48 | x-component="Input" 49 | /> 50 | <SchemaField.String 51 | name="ccc" 52 | required 53 | title="input box 3" 54 | x-decorator="FormItem" 55 | x-component="Input" 56 | /> 57 | <SchemaField.String 58 | name="ddd" 59 | required 60 | title="input box 4" 61 | x-decorator="FormItem" 62 | x-component="Input" 63 | /> 64 | </SchemaField> 65 | <FormDrawer.Footer> 66 | <FormButtonGroup align="right"> 67 | <Submit 68 | onSubmit={() => { 69 | return new Promise((resolve) => { 70 | setTimeout(resolve, 1000) 71 | }) 72 | }} 73 | > 74 | Submit 75 | </Submit> 76 | <Reset>Reset</Reset> 77 | </FormButtonGroup> 78 | </FormDrawer.Footer> 79 | </FormLayout> 80 | ) 81 | }) 82 | .open({ 83 | initialValues: { 84 | aaa: '123', 85 | }, 86 | }) 87 | .then(console.log) 88 | }} 89 | > 90 | Click me to open the form 91 | </Button> 92 | ) 93 | } 94 | ``` 95 | 96 | ## JSON Schema case 97 | 98 | ```tsx 99 | import React from 'react' 100 | import { 101 | FormDrawer, 102 | FormItem, 103 | Input, 104 | Submit, 105 | Reset, 106 | FormButtonGroup, 107 | FormLayout, 108 | } from '@formily/next' 109 | import { createSchemaField } from '@formily/react' 110 | import { Button } from '@alifd/next' 111 | 112 | const SchemaField = createSchemaField({ 113 | components: { 114 | FormItem, 115 | Input, 116 | }, 117 | }) 118 | 119 | const schema = { 120 | type: 'object', 121 | properties: { 122 | aaa: { 123 | type: 'string', 124 | title: 'input box 1', 125 | required: true, 126 | 'x-decorator': 'FormItem', 127 | 'x-component': 'Input', 128 | }, 129 | bbb: { 130 | type: 'string', 131 | title: 'input box 2', 132 | required: true, 133 | 'x-decorator': 'FormItem', 134 | 'x-component': 'Input', 135 | }, 136 | ccc: { 137 | type: 'string', 138 | title: 'input box 3', 139 | required: true, 140 | 'x-decorator': 'FormItem', 141 | 'x-component': 'Input', 142 | }, 143 | ddd: { 144 | type: 'string', 145 | title: 'input box 4', 146 | required: true, 147 | 'x-decorator': 'FormItem', 148 | 'x-component': 'Input', 149 | }, 150 | }, 151 | } 152 | 153 | export default () => { 154 | return ( 155 | <Button 156 | onClick={() => { 157 | FormDrawer('Pop-up form', () => { 158 | return ( 159 | <FormLayout labelCol={6} wrapperCol={14}> 160 | <SchemaField schema={schema} /> 161 | <FormDrawer.Footer> 162 | <FormButtonGroup align="right"> 163 | <Submit 164 | onSubmit={() => { 165 | return new Promise((resolve) => { 166 | setTimeout(resolve, 1000) 167 | }) 168 | }} 169 | > 170 | Submit 171 | </Submit> 172 | <Reset>Reset</Reset> 173 | </FormButtonGroup> 174 | </FormDrawer.Footer> 175 | </FormLayout> 176 | ) 177 | }) 178 | .open({ 179 | initialValues: { 180 | aaa: '123', 181 | }, 182 | }) 183 | .then(console.log) 184 | }} 185 | > 186 | Click me to open the form 187 | </Button> 188 | ) 189 | } 190 | ``` 191 | 192 | ## Pure JSX case 193 | 194 | ```tsx 195 | import React from 'react' 196 | import { 197 | FormDrawer, 198 | FormItem, 199 | Input, 200 | Submit, 201 | Reset, 202 | FormButtonGroup, 203 | FormLayout, 204 | } from '@formily/next' 205 | import { Field } from '@formily/react' 206 | import { Button } from '@alifd/next' 207 | 208 | export default () => { 209 | return ( 210 | <Button 211 | onClick={() => { 212 | FormDrawer('Pop-up form', () => { 213 | return ( 214 | <FormLayout labelCol={6} wrapperCol={14}> 215 | <Field 216 | name="aaa" 217 | required 218 | title="input box 1" 219 | decorator={[FormItem]} 220 | component={[Input]} 221 | /> 222 | <Field 223 | name="bbb" 224 | required 225 | title="input box 2" 226 | decorator={[FormItem]} 227 | component={[Input]} 228 | /> 229 | <Field 230 | name="ccc" 231 | required 232 | title="input box 3" 233 | decorator={[FormItem]} 234 | component={[Input]} 235 | /> 236 | <Field 237 | name="ddd" 238 | required 239 | title="input box 4" 240 | decorator={[FormItem]} 241 | component={[Input]} 242 | /> 243 | <FormDrawer.Footer> 244 | <FormButtonGroup align="right"> 245 | <Submit 246 | onSubmit={() => { 247 | return new Promise((resolve) => { 248 | setTimeout(resolve, 1000) 249 | }) 250 | }} 251 | > 252 | Submit 253 | </Submit> 254 | <Reset>Reset</Reset> 255 | </FormButtonGroup> 256 | </FormDrawer.Footer> 257 | </FormLayout> 258 | ) 259 | }) 260 | .open({ 261 | initialValues: { 262 | aaa: '123', 263 | }, 264 | }) 265 | .then(console.log) 266 | }} 267 | > 268 | Click me to open the form 269 | </Button> 270 | ) 271 | } 272 | ``` 273 | 274 | ## Use Fusion Context 275 | 276 | ```tsx 277 | import React from 'react' 278 | import { 279 | FormDrawer, 280 | FormItem, 281 | Input, 282 | Submit, 283 | Reset, 284 | FormButtonGroup, 285 | FormLayout, 286 | } from '@formily/next' 287 | import { Field } from '@formily/react' 288 | import { Button, ConfigProvider } from '@alifd/next' 289 | 290 | export default () => { 291 | return ( 292 | <ConfigProvider 293 | defaultPropsConfig={{ 294 | Drawer: {}, 295 | }} 296 | > 297 | <Button 298 | onClick={() => { 299 | FormDrawer('Pop-up form', () => { 300 | return ( 301 | <FormLayout labelCol={6} wrapperCol={14}> 302 | <Field 303 | name="aaa" 304 | required 305 | title="input box 1" 306 | decorator={[FormItem]} 307 | component={[Input]} 308 | /> 309 | <Field 310 | name="bbb" 311 | required 312 | title="input box 2" 313 | decorator={[FormItem]} 314 | component={[Input]} 315 | /> 316 | <Field 317 | name="ccc" 318 | required 319 | title="input box 3" 320 | decorator={[FormItem]} 321 | component={[Input]} 322 | /> 323 | <Field 324 | name="ddd" 325 | required 326 | title="input box 4" 327 | decorator={[FormItem]} 328 | component={[Input]} 329 | /> 330 | <FormDrawer.Footer> 331 | <FormButtonGroup align="right"> 332 | <Submit 333 | onSubmit={() => { 334 | return new Promise((resolve) => { 335 | setTimeout(resolve, 1000) 336 | }) 337 | }} 338 | > 339 | Submit 340 | </Submit> 341 | <Reset>Reset</Reset> 342 | </FormButtonGroup> 343 | </FormDrawer.Footer> 344 | </FormLayout> 345 | ) 346 | }) 347 | .open({ 348 | initialValues: { 349 | aaa: '123', 350 | }, 351 | }) 352 | .then(console.log) 353 | }} 354 | > 355 | Click me to open the form 356 | </Button> 357 | </ConfigProvider> 358 | ) 359 | } 360 | ``` 361 | 362 | ## API 363 | 364 | ### FormDrawer 365 | 366 | ```ts pure 367 | import { IFormProps, Form } from '@formily/core' 368 | 369 | type FormDrawerRenderer = 370 | | React.ReactElement 371 | | ((form: Form) => React.ReactElement) 372 | 373 | interface IFormDrawer { 374 | forOpen( 375 | middleware: ( 376 | props: IFormProps, 377 | next: (props?: IFormProps) => Promise<any> 378 | ) => any 379 | ): any //Middleware interceptor, can intercept Drawer to open 380 | //Open the pop-up window to receive form attributes, you can pass in initialValues/values/effects etc. 381 | open(props: IFormProps): Promise<any> //return form data 382 | //Close the pop-up window 383 | close(): void 384 | } 385 | 386 | interface IDrawerProps extends DrawerProps { 387 | onClose?: (reason: string, e: React.MouseEvent) => void | boolean // return false can prevent onClose 388 | loadingText?: React.ReactNode 389 | } 390 | 391 | interface FormDrawer { 392 | (title: IDrawerProps, id: string, renderer: FormDrawerRenderer): IFormDrawer 393 | (title: IDrawerProps, renderer: FormDrawerRenderer): IFormDrawer 394 | (title: ModalTitle, id: string, renderer: FormDrawerRenderer): IFormDrawer 395 | (title: ModalTitle, renderer: FormDrawerRenderer): IFormDrawer 396 | } 397 | ``` 398 | 399 | `DrawerProps` type definition reference ant design [Drawer API](https://fusion.design/pc/component/drawer?themeid=2#API) 400 | 401 | ### FormDrawer.Footer 402 | 403 | No attributes, only child nodes are received 404 | 405 | ### FormDrawer.Portal 406 | 407 | 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 408 | ``` -------------------------------------------------------------------------------- /packages/reactive/src/__tests__/action.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { observable, action, autorun } from '..' 2 | import { reaction } from '../autorun' 3 | import { batch } from '../batch' 4 | import { define } from '../model' 5 | 6 | describe('normal action', () => { 7 | test('no action', () => { 8 | const obs = observable({ 9 | aa: { 10 | bb: 123, 11 | }, 12 | }) 13 | const handler = jest.fn() 14 | autorun(() => { 15 | handler(obs.aa.bb) 16 | }) 17 | obs.aa.bb = 111 18 | obs.aa.bb = 222 19 | expect(handler).toBeCalledTimes(3) 20 | 21 | obs.aa.bb = 333 22 | obs.aa.bb = 444 23 | 24 | expect(handler).toBeCalledTimes(5) 25 | }) 26 | 27 | test('action', () => { 28 | const obs = observable({ 29 | aa: { 30 | bb: 123, 31 | }, 32 | }) 33 | const handler = jest.fn() 34 | autorun(() => { 35 | handler(obs.aa.bb) 36 | }) 37 | obs.aa.bb = 111 38 | obs.aa.bb = 222 39 | expect(handler).toBeCalledTimes(3) 40 | action(() => { 41 | obs.aa.bb = 333 42 | obs.aa.bb = 444 43 | }) 44 | action(() => {}) 45 | action() 46 | expect(handler).toBeCalledTimes(4) 47 | }) 48 | 49 | test('action track', () => { 50 | const obs = observable({ 51 | aa: { 52 | bb: 123, 53 | }, 54 | cc: 1, 55 | }) 56 | const handler = jest.fn() 57 | autorun(() => { 58 | action(() => { 59 | if (obs.cc > 0) { 60 | handler(obs.aa.bb) 61 | obs.cc = obs.cc + 20 62 | } 63 | }) 64 | }) 65 | expect(handler).toBeCalledTimes(1) 66 | expect(obs.cc).toEqual(21) 67 | obs.aa.bb = 321 68 | expect(handler).toBeCalledTimes(1) 69 | expect(obs.cc).toEqual(21) 70 | }) 71 | 72 | test('action.bound', () => { 73 | const obs = observable({ 74 | aa: { 75 | bb: 123, 76 | }, 77 | }) 78 | const handler = jest.fn() 79 | const setData = action.bound(() => { 80 | obs.aa.bb = 333 81 | obs.aa.bb = 444 82 | }) 83 | autorun(() => { 84 | handler(obs.aa.bb) 85 | }) 86 | obs.aa.bb = 111 87 | obs.aa.bb = 222 88 | expect(handler).toBeCalledTimes(3) 89 | setData() 90 | action.bound(() => {}) 91 | expect(handler).toBeCalledTimes(4) 92 | }) 93 | 94 | test('action.bound track', () => { 95 | const obs = observable({ 96 | aa: { 97 | bb: 123, 98 | }, 99 | cc: 1, 100 | }) 101 | const handler = jest.fn() 102 | autorun(() => { 103 | action.bound(() => { 104 | if (obs.cc > 0) { 105 | handler(obs.aa.bb) 106 | obs.cc = obs.cc + 20 107 | } 108 | })() 109 | }) 110 | expect(handler).toBeCalledTimes(1) 111 | expect(obs.cc).toEqual(21) 112 | obs.aa.bb = 321 113 | expect(handler).toBeCalledTimes(1) 114 | expect(obs.cc).toEqual(21) 115 | }) 116 | 117 | test('action.scope xxx', () => { 118 | const obs = observable<any>({}) 119 | 120 | const handler = jest.fn() 121 | 122 | autorun(() => { 123 | handler(obs.aa, obs.bb, obs.cc, obs.dd) 124 | }) 125 | 126 | action(() => { 127 | action.scope(() => { 128 | obs.aa = 123 129 | }) 130 | action.scope(() => { 131 | obs.cc = 'ccccc' 132 | }) 133 | obs.bb = 321 134 | obs.dd = 'ddddd' 135 | }) 136 | 137 | expect(handler).toBeCalledTimes(4) 138 | }) 139 | 140 | test('action.scope bound', () => { 141 | const obs = observable<any>({}) 142 | 143 | const handler = jest.fn() 144 | 145 | autorun(() => { 146 | handler(obs.aa, obs.bb, obs.cc, obs.dd) 147 | }) 148 | 149 | const scope1 = action.scope.bound(() => { 150 | obs.aa = 123 151 | }) 152 | action(() => { 153 | scope1() 154 | action.scope.bound(() => { 155 | obs.cc = 'ccccc' 156 | })() 157 | obs.bb = 321 158 | obs.dd = 'ddddd' 159 | }) 160 | 161 | expect(handler).toBeCalledTimes(4) 162 | }) 163 | 164 | test('action.scope track', () => { 165 | const obs = observable({ 166 | aa: { 167 | bb: 123, 168 | }, 169 | cc: 1, 170 | }) 171 | const handler = jest.fn() 172 | autorun(() => { 173 | action.scope(() => { 174 | if (obs.cc > 0) { 175 | handler(obs.aa.bb) 176 | obs.cc = obs.cc + 20 177 | } 178 | }) 179 | }) 180 | expect(handler).toBeCalledTimes(1) 181 | expect(obs.cc).toEqual(21) 182 | obs.aa.bb = 321 183 | expect(handler).toBeCalledTimes(1) 184 | expect(obs.cc).toEqual(21) 185 | }) 186 | 187 | test('action.scope bound track', () => { 188 | const obs = observable({ 189 | aa: { 190 | bb: 123, 191 | }, 192 | cc: 1, 193 | }) 194 | const handler = jest.fn() 195 | autorun(() => { 196 | action.scope.bound(() => { 197 | if (obs.cc > 0) { 198 | handler(obs.aa.bb) 199 | obs.cc = obs.cc + 20 200 | } 201 | })() 202 | }) 203 | expect(handler).toBeCalledTimes(1) 204 | expect(obs.cc).toEqual(21) 205 | obs.aa.bb = 321 206 | expect(handler).toBeCalledTimes(1) 207 | expect(obs.cc).toEqual(21) 208 | }) 209 | }) 210 | 211 | describe('annotation action', () => { 212 | test('action', () => { 213 | const obs = define( 214 | { 215 | aa: { 216 | bb: 123, 217 | }, 218 | setData() { 219 | this.aa.bb = 333 220 | this.aa.bb = 444 221 | }, 222 | }, 223 | { 224 | aa: observable, 225 | setData: action, 226 | } 227 | ) 228 | const handler = jest.fn() 229 | autorun(() => { 230 | handler(obs.aa.bb) 231 | }) 232 | obs.aa.bb = 111 233 | obs.aa.bb = 222 234 | expect(handler).toBeCalledTimes(3) 235 | obs.setData() 236 | expect(handler).toBeCalledTimes(4) 237 | }) 238 | 239 | test('action track', () => { 240 | const obs = define( 241 | { 242 | aa: { 243 | bb: 123, 244 | }, 245 | cc: 1, 246 | setData() { 247 | if (obs.cc > 0) { 248 | handler(obs.aa.bb) 249 | obs.cc = obs.cc + 20 250 | } 251 | }, 252 | }, 253 | { 254 | aa: observable, 255 | setData: action, 256 | } 257 | ) 258 | const handler = jest.fn() 259 | autorun(() => { 260 | obs.setData() 261 | }) 262 | expect(handler).toBeCalledTimes(1) 263 | expect(obs.cc).toEqual(21) 264 | obs.aa.bb = 321 265 | expect(handler).toBeCalledTimes(1) 266 | expect(obs.cc).toEqual(21) 267 | }) 268 | 269 | test('action.bound', () => { 270 | const obs = define( 271 | { 272 | aa: { 273 | bb: 123, 274 | }, 275 | setData() { 276 | this.aa.bb = 333 277 | this.aa.bb = 444 278 | }, 279 | }, 280 | { 281 | aa: observable, 282 | setData: action.bound, 283 | } 284 | ) 285 | const handler = jest.fn() 286 | autorun(() => { 287 | handler(obs.aa.bb) 288 | }) 289 | obs.aa.bb = 111 290 | obs.aa.bb = 222 291 | expect(handler).toBeCalledTimes(3) 292 | obs.setData() 293 | expect(handler).toBeCalledTimes(4) 294 | }) 295 | 296 | test('action.bound track', () => { 297 | const obs = define( 298 | { 299 | aa: { 300 | bb: 123, 301 | }, 302 | cc: 1, 303 | setData() { 304 | if (obs.cc > 0) { 305 | handler(obs.aa.bb) 306 | obs.cc = obs.cc + 20 307 | } 308 | }, 309 | }, 310 | { 311 | aa: observable, 312 | setData: action.bound, 313 | } 314 | ) 315 | const handler = jest.fn() 316 | autorun(() => { 317 | obs.setData() 318 | }) 319 | expect(handler).toBeCalledTimes(1) 320 | expect(obs.cc).toEqual(21) 321 | obs.aa.bb = 321 322 | expect(handler).toBeCalledTimes(1) 323 | expect(obs.cc).toEqual(21) 324 | }) 325 | 326 | test('action.scope', () => { 327 | const obs = define( 328 | { 329 | aa: null, 330 | bb: null, 331 | cc: null, 332 | dd: null, 333 | scope1() { 334 | this.aa = 123 335 | }, 336 | scope2() { 337 | this.cc = 'ccccc' 338 | }, 339 | }, 340 | { 341 | aa: observable, 342 | bb: observable, 343 | cc: observable, 344 | dd: observable, 345 | scope1: action.scope, 346 | scope2: action.scope, 347 | } 348 | ) 349 | 350 | const handler = jest.fn() 351 | 352 | autorun(() => { 353 | handler(obs.aa, obs.bb, obs.cc, obs.dd) 354 | }) 355 | 356 | action(() => { 357 | obs.scope1() 358 | obs.scope2() 359 | obs.bb = 321 360 | obs.dd = 'ddddd' 361 | }) 362 | 363 | expect(handler).toBeCalledTimes(4) 364 | }) 365 | 366 | test('action.scope bound', () => { 367 | const obs = define( 368 | { 369 | aa: null, 370 | bb: null, 371 | cc: null, 372 | dd: null, 373 | scope1() { 374 | this.aa = 123 375 | }, 376 | scope2() { 377 | this.cc = 'ccccc' 378 | }, 379 | }, 380 | { 381 | aa: observable, 382 | bb: observable, 383 | cc: observable, 384 | dd: observable, 385 | scope1: action.scope.bound, 386 | scope2: action.scope.bound, 387 | } 388 | ) 389 | 390 | const handler = jest.fn() 391 | 392 | autorun(() => { 393 | handler(obs.aa, obs.bb, obs.cc, obs.dd) 394 | }) 395 | 396 | action(() => { 397 | obs.scope1() 398 | obs.scope2() 399 | obs.bb = 321 400 | obs.dd = 'ddddd' 401 | }) 402 | 403 | expect(handler).toBeCalledTimes(4) 404 | }) 405 | 406 | test('action.scope track', () => { 407 | const obs = define( 408 | { 409 | aa: { 410 | bb: 123, 411 | }, 412 | cc: 1, 413 | scope() { 414 | if (this.cc > 0) { 415 | handler(this.aa.bb) 416 | this.cc = this.cc + 20 417 | } 418 | }, 419 | }, 420 | { 421 | aa: observable, 422 | cc: observable, 423 | scope: action.scope, 424 | } 425 | ) 426 | const handler = jest.fn() 427 | autorun(() => { 428 | obs.scope() 429 | }) 430 | expect(handler).toBeCalledTimes(1) 431 | expect(obs.cc).toEqual(21) 432 | obs.aa.bb = 321 433 | expect(handler).toBeCalledTimes(1) 434 | expect(obs.cc).toEqual(21) 435 | }) 436 | 437 | test('action.scope bound track', () => { 438 | const obs = define( 439 | { 440 | aa: { 441 | bb: 123, 442 | }, 443 | cc: 1, 444 | scope() { 445 | if (this.cc > 0) { 446 | handler(this.aa.bb) 447 | this.cc = this.cc + 20 448 | } 449 | }, 450 | }, 451 | { 452 | aa: observable, 453 | cc: observable, 454 | scope: action.scope.bound, 455 | } 456 | ) 457 | const handler = jest.fn() 458 | autorun(() => { 459 | obs.scope() 460 | }) 461 | expect(handler).toBeCalledTimes(1) 462 | expect(obs.cc).toEqual(21) 463 | obs.aa.bb = 321 464 | expect(handler).toBeCalledTimes(1) 465 | expect(obs.cc).toEqual(21) 466 | }) 467 | }) 468 | 469 | test('nested action to reaction', () => { 470 | const obs = observable({ 471 | aa: 0, 472 | }) 473 | const handler = jest.fn() 474 | reaction( 475 | () => obs.aa, 476 | (v) => handler(v) 477 | ) 478 | action(() => { 479 | obs.aa = 1 480 | action(() => { 481 | obs.aa = 2 482 | }) 483 | }) 484 | action(() => { 485 | obs.aa = 3 486 | action(() => { 487 | obs.aa = 4 488 | }) 489 | }) 490 | expect(handler).nthCalledWith(1, 2) 491 | expect(handler).nthCalledWith(2, 4) 492 | expect(handler).toBeCalledTimes(2) 493 | }) 494 | 495 | test('nested action/batch to reaction', () => { 496 | const obs = define( 497 | { 498 | bb: 0, 499 | get aa() { 500 | return this.bb 501 | }, 502 | set aa(v) { 503 | this.bb = v 504 | }, 505 | }, 506 | { 507 | aa: observable.computed, 508 | bb: observable, 509 | } 510 | ) 511 | const handler = jest.fn() 512 | reaction( 513 | () => obs.aa, 514 | (v) => handler(v) 515 | ) 516 | action(() => { 517 | obs.aa = 1 518 | batch(() => { 519 | obs.aa = 2 520 | }) 521 | }) 522 | action(() => { 523 | obs.aa = 3 524 | batch(() => { 525 | obs.aa = 4 526 | }) 527 | }) 528 | expect(handler).nthCalledWith(1, 2) 529 | expect(handler).nthCalledWith(2, 4) 530 | expect(handler).toBeCalledTimes(2) 531 | }) 532 | ``` -------------------------------------------------------------------------------- /packages/reactive/src/__tests__/collections-map.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { observable, autorun, raw } from '..' 2 | 3 | describe('Map', () => { 4 | test('should be a proper JS Map', () => { 5 | const map = observable(new Map()) 6 | expect(map).toBeInstanceOf(Map) 7 | expect(raw(map)).toBeInstanceOf(Map) 8 | }) 9 | 10 | test('should autorun mutations', () => { 11 | const handler = jest.fn() 12 | const map = observable(new Map()) 13 | autorun(() => handler(map.get('key'))) 14 | 15 | expect(handler).toBeCalledTimes(1) 16 | expect(handler).lastCalledWith(undefined) 17 | map.set('key', 'value') 18 | expect(handler).toBeCalledTimes(2) 19 | expect(handler).lastCalledWith('value') 20 | map.set('key', 'value2') 21 | expect(handler).toBeCalledTimes(3) 22 | expect(handler).lastCalledWith('value2') 23 | map.delete('key') 24 | expect(handler).toBeCalledTimes(4) 25 | expect(handler).lastCalledWith(undefined) 26 | }) 27 | 28 | test('should autorun size mutations', () => { 29 | const handler = jest.fn() 30 | const map = observable(new Map()) 31 | autorun(() => handler(map.size)) 32 | 33 | expect(handler).toBeCalledTimes(1) 34 | expect(handler).lastCalledWith(0) 35 | map.set('key1', 'value') 36 | map.set('key2', 'value2') 37 | expect(handler).toBeCalledTimes(3) 38 | expect(handler).lastCalledWith(2) 39 | map.delete('key1') 40 | expect(handler).toBeCalledTimes(4) 41 | expect(handler).lastCalledWith(1) 42 | map.clear() 43 | expect(handler).toBeCalledTimes(5) 44 | expect(handler).lastCalledWith(0) 45 | }) 46 | 47 | test('should autorun for of iteration', () => { 48 | const handler = jest.fn() 49 | const map = observable(new Map()) 50 | autorun(() => { 51 | let sum = 0 52 | // eslint-disable-next-line no-unused-vars 53 | for (let [, num] of map) { 54 | sum += num 55 | } 56 | handler(sum) 57 | }) 58 | 59 | expect(handler).toBeCalledTimes(1) 60 | expect(handler).lastCalledWith(0) 61 | map.set('key0', 3) 62 | expect(handler).toBeCalledTimes(2) 63 | expect(handler).lastCalledWith(3) 64 | map.set('key1', 2) 65 | expect(handler).toBeCalledTimes(3) 66 | expect(handler).lastCalledWith(5) 67 | map.delete('key0') 68 | expect(handler).toBeCalledTimes(4) 69 | expect(handler).lastCalledWith(2) 70 | map.clear() 71 | expect(handler).toBeCalledTimes(5) 72 | expect(handler).lastCalledWith(0) 73 | }) 74 | 75 | test('should autorun forEach iteration', () => { 76 | const handler = jest.fn() 77 | const map = observable(new Map()) 78 | autorun(() => { 79 | let sum = 0 80 | map.forEach((num) => (sum += num)) 81 | handler(sum) 82 | }) 83 | 84 | expect(handler).toBeCalledTimes(1) 85 | expect(handler).lastCalledWith(0) 86 | map.set('key0', 3) 87 | expect(handler).toBeCalledTimes(2) 88 | expect(handler).lastCalledWith(3) 89 | map.set('key1', 2) 90 | expect(handler).toBeCalledTimes(3) 91 | expect(handler).lastCalledWith(5) 92 | map.delete('key0') 93 | expect(handler).toBeCalledTimes(4) 94 | expect(handler).lastCalledWith(2) 95 | map.clear() 96 | expect(handler).toBeCalledTimes(5) 97 | expect(handler).lastCalledWith(0) 98 | }) 99 | 100 | test('should autorun keys iteration', () => { 101 | const handler = jest.fn() 102 | const map = observable(new Map()) 103 | autorun(() => { 104 | let sum = 0 105 | for (let key of map.keys()) { 106 | sum += key 107 | } 108 | handler(sum) 109 | }) 110 | 111 | expect(handler).toBeCalledTimes(1) 112 | expect(handler).lastCalledWith(0) 113 | map.set(3, 3) 114 | expect(handler).toBeCalledTimes(2) 115 | expect(handler).lastCalledWith(3) 116 | map.set(2, 2) 117 | expect(handler).toBeCalledTimes(3) 118 | expect(handler).lastCalledWith(5) 119 | map.delete(3) 120 | expect(handler).toBeCalledTimes(4) 121 | expect(handler).lastCalledWith(2) 122 | map.clear() 123 | expect(handler).toBeCalledTimes(5) 124 | expect(handler).lastCalledWith(0) 125 | }) 126 | 127 | test('should autorun values iteration', () => { 128 | const handler = jest.fn() 129 | const map = observable(new Map()) 130 | autorun(() => { 131 | let sum = 0 132 | for (let num of map.values()) { 133 | sum += num 134 | } 135 | handler(sum) 136 | }) 137 | 138 | expect(handler).toBeCalledTimes(1) 139 | expect(handler).lastCalledWith(0) 140 | map.set('key0', 3) 141 | expect(handler).toBeCalledTimes(2) 142 | expect(handler).lastCalledWith(3) 143 | map.set('key1', 2) 144 | expect(handler).toBeCalledTimes(3) 145 | expect(handler).lastCalledWith(5) 146 | map.delete('key0') 147 | expect(handler).toBeCalledTimes(4) 148 | expect(handler).lastCalledWith(2) 149 | map.clear() 150 | expect(handler).toBeCalledTimes(5) 151 | expect(handler).lastCalledWith(0) 152 | }) 153 | 154 | test('should autorun entries iteration', () => { 155 | const handler = jest.fn() 156 | const map = observable(new Map()) 157 | autorun(() => { 158 | let sum = 0 159 | // eslint-disable-next-line no-unused-vars 160 | for (let [, num] of map.entries()) { 161 | sum += num 162 | } 163 | handler(sum) 164 | }) 165 | 166 | expect(handler).toBeCalledTimes(1) 167 | expect(handler).lastCalledWith(0) 168 | map.set('key0', 3) 169 | expect(handler).toBeCalledTimes(2) 170 | expect(handler).lastCalledWith(3) 171 | map.set('key1', 2) 172 | expect(handler).toBeCalledTimes(3) 173 | expect(handler).lastCalledWith(5) 174 | map.delete('key0') 175 | expect(handler).toBeCalledTimes(4) 176 | expect(handler).lastCalledWith(2) 177 | map.clear() 178 | expect(handler).toBeCalledTimes(5) 179 | expect(handler).lastCalledWith(0) 180 | }) 181 | 182 | test('should be triggered by clearing', () => { 183 | const handler = jest.fn() 184 | const map = observable(new Map()) 185 | autorun(() => handler(map.get('key'))) 186 | 187 | expect(handler).toBeCalledTimes(1) 188 | expect(handler).lastCalledWith(undefined) 189 | map.set('key', 3) 190 | expect(handler).toBeCalledTimes(2) 191 | expect(handler).lastCalledWith(3) 192 | map.clear() 193 | expect(handler).toBeCalledTimes(3) 194 | expect(handler).lastCalledWith(undefined) 195 | }) 196 | 197 | test('should not autorun custom property mutations', () => { 198 | const handler = jest.fn() 199 | const map = observable(new Map()) 200 | autorun(() => handler(map['customProp'])) 201 | 202 | expect(handler).toBeCalledTimes(1) 203 | expect(handler).lastCalledWith(undefined) 204 | map['customProp'] = 'Hello World' 205 | expect(handler).toBeCalledTimes(1) 206 | }) 207 | 208 | test('should not autorun non value changing mutations', () => { 209 | const handler = jest.fn() 210 | const map = observable(new Map()) 211 | autorun(() => handler(map.get('key'))) 212 | 213 | expect(handler).toBeCalledTimes(1) 214 | expect(handler).lastCalledWith(undefined) 215 | map.set('key', 'value') 216 | expect(handler).toBeCalledTimes(2) 217 | expect(handler).lastCalledWith('value') 218 | map.set('key', 'value') 219 | expect(handler).toBeCalledTimes(2) 220 | map.delete('key') 221 | expect(handler).toBeCalledTimes(3) 222 | expect(handler).lastCalledWith(undefined) 223 | map.delete('key') 224 | expect(handler).toBeCalledTimes(3) 225 | map.clear() 226 | expect(handler).toBeCalledTimes(3) 227 | }) 228 | 229 | test('should not autorun raw data', () => { 230 | const handler = jest.fn() 231 | const map = observable(new Map()) 232 | autorun(() => handler(raw(map).get('key'))) 233 | 234 | expect(handler).toBeCalledTimes(1) 235 | expect(handler).lastCalledWith(undefined) 236 | map.set('key', 'Hello') 237 | expect(handler).toBeCalledTimes(1) 238 | map.delete('key') 239 | expect(handler).toBeCalledTimes(1) 240 | }) 241 | 242 | test('should not autorun raw iterations', () => { 243 | const handler = jest.fn() 244 | const map = observable(new Map()) 245 | autorun(() => { 246 | let sum = 0 247 | // eslint-disable-next-line no-unused-vars 248 | for (let [, num] of raw(map).entries()) { 249 | sum += num 250 | } 251 | for (let key of raw(map).keys()) { 252 | sum += raw(map).get(key) 253 | } 254 | for (let num of raw(map).values()) { 255 | sum += num 256 | } 257 | raw(map).forEach((num) => { 258 | sum += num 259 | }) 260 | // eslint-disable-next-line no-unused-vars 261 | for (let [, num] of raw(map)) { 262 | sum += num 263 | } 264 | handler(sum) 265 | }) 266 | 267 | expect(handler).toBeCalledTimes(1) 268 | expect(handler).lastCalledWith(0) 269 | map.set('key1', 2) 270 | map.set('key2', 3) 271 | expect(handler).toBeCalledTimes(1) 272 | map.delete('key1') 273 | expect(handler).toBeCalledTimes(1) 274 | }) 275 | 276 | test('should not be triggered by raw mutations', () => { 277 | const handler = jest.fn() 278 | const map = observable(new Map()) 279 | autorun(() => handler(map.get('key'))) 280 | 281 | expect(handler).toBeCalledTimes(1) 282 | expect(handler).lastCalledWith(undefined) 283 | raw(map).set('key', 'Hello') 284 | expect(handler).toBeCalledTimes(1) 285 | raw(map).delete('key') 286 | expect(handler).toBeCalledTimes(1) 287 | raw(map).clear() 288 | expect(handler).toBeCalledTimes(1) 289 | }) 290 | 291 | test('should not autorun raw size mutations', () => { 292 | const handler = jest.fn() 293 | const map = observable(new Map()) 294 | autorun(() => handler(raw(map).size)) 295 | 296 | expect(handler).toBeCalledTimes(1) 297 | expect(handler).lastCalledWith(0) 298 | map.set('key', 'value') 299 | expect(handler).toBeCalledTimes(1) 300 | }) 301 | 302 | test('should not be triggered by raw size mutations', () => { 303 | const handler = jest.fn() 304 | const map = observable(new Map()) 305 | autorun(() => handler(map.size)) 306 | 307 | expect(handler).toBeCalledTimes(1) 308 | expect(handler).lastCalledWith(0) 309 | raw(map).set('key', 'value') 310 | expect(handler).toBeCalledTimes(1) 311 | }) 312 | 313 | test('should support objects as key', () => { 314 | const handler = jest.fn() 315 | const key = {} 316 | const map = observable(new Map()) 317 | autorun(() => handler(map.get(key))) 318 | 319 | expect(handler).toBeCalledTimes(1) 320 | expect(handler).lastCalledWith(undefined) 321 | 322 | map.set(key, 1) 323 | expect(handler).toBeCalledTimes(2) 324 | expect(handler).lastCalledWith(1) 325 | 326 | map.set({}, 2) 327 | expect(handler).toBeCalledTimes(2) 328 | expect(handler).lastCalledWith(1) 329 | }) 330 | 331 | test('observer object', () => { 332 | const handler = jest.fn() 333 | const map = observable(new Map<string, Record<string, any>>([])) 334 | map.set('key', {}) 335 | map.set('key2', observable({})) 336 | autorun(() => { 337 | const [obs1, obs2] = [...map.values()] 338 | 339 | handler(obs1.aa, obs2.aa) 340 | }) 341 | 342 | expect(handler).toBeCalledTimes(1) 343 | const obs1 = map.get('key') 344 | const obs2 = map.get('key2') 345 | obs1.aa = '123' 346 | obs2.aa = '234' 347 | expect(handler).toBeCalledTimes(3) 348 | }) 349 | 350 | test('shallow', () => { 351 | const handler = jest.fn() 352 | const map = observable.shallow(new Map<string, Record<string, any>>([])) 353 | map.set('key', {}) 354 | autorun(() => { 355 | const [obs] = [...map.values()] 356 | 357 | handler(obs.aa) 358 | }) 359 | 360 | expect(handler).toBeCalledTimes(1) 361 | const obs = map.get('key') 362 | obs.aa = '123' 363 | expect(handler).toBeCalledTimes(1) 364 | }) 365 | }) 366 | ``` -------------------------------------------------------------------------------- /packages/path/src/__tests__/match.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import expect from 'expect' 2 | import { Path } from '../' 3 | import { Matcher } from '../matcher' 4 | 5 | const match = (obj) => { 6 | for (let name in obj) { 7 | test('test match ' + name, () => { 8 | const path = new Path(name) 9 | if (Array.isArray(obj[name]) && Array.isArray(obj[name][0])) { 10 | obj[name].forEach((_path) => { 11 | expect(path.match(_path)).toBeTruthy() 12 | }) 13 | } else { 14 | expect(path.match(obj[name])).toBeTruthy() 15 | } 16 | }) 17 | } 18 | } 19 | 20 | const unmatch = (obj) => { 21 | for (let name in obj) { 22 | test('test unmatch ' + name, () => { 23 | const path = new Path(name) 24 | if (Array.isArray(obj[name]) && Array.isArray(obj[name][0])) { 25 | obj[name].forEach((_path) => { 26 | expect(path.match(_path)).toBeFalsy() 27 | }) 28 | } else { 29 | expect(path.match(obj[name])).toBeFalsy() 30 | } 31 | }) 32 | } 33 | } 34 | 35 | test('basic match', () => { 36 | expect(Path.parse('xxx').match('')).toBeFalsy() 37 | expect(Path.parse('xxx').match('aaa')).toBeFalsy() 38 | expect(Path.parse('xxx.eee').match('xxx')).toBeFalsy() 39 | expect(Path.parse('*(xxx.eee~)').match('xxx')).toBeFalsy() 40 | expect(Path.parse('xxx.eee~').match('xxx.eee')).toBeTruthy() 41 | expect(Path.parse('*(!xxx.eee,yyy)').match('xxx')).toBeFalsy() 42 | expect(Path.parse('*(!xxx.eee,yyy)').match('xxx.ooo.ppp')).toBeTruthy() 43 | expect(Path.parse('*(!xxx.eee,yyy)').match('xxx.eee')).toBeFalsy() 44 | expect(Path.parse('*(!xxx.eee~,yyy)').match('xxx.eee')).toBeFalsy() 45 | expect(Path.parse('~.aa').match('xxx.aa')).toBeTruthy() 46 | }) 47 | 48 | test('not expect match not', () => { 49 | expect(new Matcher({}).match(['']).matched).toBeFalsy() 50 | }) 51 | 52 | test('test matchGroup', () => { 53 | const pattern = new Path('*(aa,bb,cc)') 54 | expect(pattern.matchAliasGroup('aa', 'bb')).toEqual(true) 55 | const excludePattern = new Path('aa.bb.*(11,22,33).*(!aa,bb,cc)') 56 | expect( 57 | excludePattern.matchAliasGroup('aa.bb.11.mm', 'aa.cc.dd.bb.11.mm') 58 | ).toEqual(true) 59 | expect(excludePattern.matchAliasGroup('aa.cc', 'aa.kk.cc')).toEqual(false) 60 | expect(new Path('aa.*(!bb)').matchAliasGroup('kk.mm.aa.bb', 'aa.bb')).toEqual( 61 | false 62 | ) 63 | expect( 64 | new Path('aa.*(!bb)').matchAliasGroup('kk.mm.aa.bb.cc', 'kk.mm.aa') 65 | ).toEqual(false) 66 | expect(new Path('aa.*(!bb,oo)').matchAliasGroup('kk.mm', 'aa')).toEqual(false) 67 | expect(new Path('aa.*(!bb.*)').matchAliasGroup('kk.mm', 'aa')).toEqual(false) 68 | expect(new Path('aa.*(!bb)').matchAliasGroup('kk.mm.aa.cc', 'aa.cc')).toEqual( 69 | true 70 | ) 71 | const patttern2 = Path.parse('*(array)') 72 | expect(patttern2.matchAliasGroup(['array', 0], ['array', 0])).toEqual(false) 73 | }) 74 | 75 | test('exclude match', () => { 76 | //路径长度相等 77 | expect(Path.parse('*(!aaa)').match('ggg')).toBeTruthy() 78 | expect(Path.parse('*(!aaa)').match('aaa')).toBeFalsy() 79 | expect(Path.parse('*(!aaa.bbb)').match('ggg.ddd')).toBeTruthy() 80 | expect(Path.parse('*(!aaa.ccc)').match('aaa.ccc')).toBeFalsy() 81 | //长路径匹配短路径 82 | expect(Path.parse('*(!aaa.bbb)').match('ggg')).toBeTruthy() 83 | expect(Path.parse('*(!aaa.bbb)').match('aaa')).toBeFalsy() 84 | //短路径匹配长路径 85 | expect(Path.parse('*(!aaa)').match('aaa.bbb')).toBeTruthy() 86 | expect(Path.parse('*(!aaa)').match('aaa.ccc')).toBeTruthy() 87 | expect(Path.parse('*(!aaa)').match('bbb.ccc')).toBeTruthy() 88 | 89 | expect(Path.parse('*(!aaa,bbb)').match('bbb')).toBeFalsy() 90 | expect(Path.parse('*(!aaa.bbb)').match('aaa.ccc')).toBeTruthy() 91 | expect(Path.parse('*(!basic.name,versionTag)').match('basic.id')).toBeTruthy() 92 | expect(Path.parse('*(!basic.name,versionTag)').match('basic')).toBeFalsy() 93 | expect( 94 | Path.parse('*(!basic.name,versionTag)').match('isExecutable') 95 | ).toBeTruthy() 96 | expect( 97 | Path.parse('*(!basic.name,versionTag)').match('versionTag') 98 | ).toBeFalsy() 99 | expect( 100 | Path.parse('*(!basic.name,basic.name.*,versionTag)').match('basic.name') 101 | ).toBeFalsy() 102 | expect( 103 | Path.parse('*(!basic.name,basic.name.*,versionTag)').match('basic.name.kkk') 104 | ).toBeFalsy() 105 | expect(Path.parse('aa.*(!bb)').match('kk.mm.aa.bb.cc')).toBeFalsy() 106 | expect(Path.parse('aa.*(!bb)').match('aa')).toBeFalsy() 107 | expect(Path.parse('aa.*(!bb.*)').match('aa')).toBeFalsy() 108 | expect(Path.parse('aa.*(!bb,cc)').match('aa')).toBeFalsy() 109 | expect(Path.parse('aa.*(!bb,cc)').match('aa.dd')).toBeTruthy() 110 | expect(Path.parse('aa.*(!bb,cc)').match('aa.kk')).toBeTruthy() 111 | }) 112 | 113 | test('match regexp', () => { 114 | expect(Path.parse(/^\d+$/).match('212')).toBeTruthy() 115 | expect(Path.parse(/^\d+$/).match('212dd')).toBeFalsy() 116 | }) 117 | 118 | test('test zero', () => { 119 | expect(Path.parse('t.0.value~').match(['t', 0, 'value_list'])).toEqual(true) 120 | }) 121 | 122 | test('test optional wild match', () => { 123 | expect(Path.parse('aa.**').match(['aa'])).toEqual(true) 124 | expect(Path.parse('aa.**').match(['aa', 'bb', 'cc'])).toEqual(true) 125 | expect(Path.parse('aa.*').match(['aa'])).toEqual(false) 126 | expect(Path.parse('aa.\\*\\*\\.aa').match(['aa', '**.aa'])).toEqual(true) 127 | expect(Path.parse('aa.[[**.aa]]').match(['aa', '**.aa'])).toEqual(true) 128 | expect(() => Path.parse('aa.**.aa').match(['aa'])).toThrowError() 129 | expect(() => Path.parse('aa.**(bb)').match(['aa'])).toThrowError() 130 | expect(Path.parse('*(aa.**)').match(['aa'])).toEqual(true) 131 | expect(Path.parse('*(aa.**,bb.**)').match(['aa'])).toEqual(true) 132 | expect(Path.parse('*(aa.**,bb.**)').match(['aa', 'bb', 'cc'])).toEqual(true) 133 | expect(Path.parse('*(aa.**,bb.**)').match(['bb'])).toEqual(true) 134 | expect(Path.parse('*(aa.**,bb.**)').match(['bb', 'cc', 'dd'])).toEqual(true) 135 | expect(Path.parse('*(aa.**,bb.**)').match(['cc'])).toEqual(false) 136 | expect(Path.parse('*(aa.**,bb.**).bb').match(['aa', 'oo'])).toEqual(true) 137 | expect(Path.parse('*(aa.**,bb.**).bb').match(['bb', 'oo'])).toEqual(true) 138 | expect(Path.parse('*(aa.**,bb.**).bb').match(['aa', 'oo', 'bb'])).toEqual( 139 | true 140 | ) 141 | expect(Path.parse('*(aa.**,bb.**).bb').match(['bb', 'oo', 'bb'])).toEqual( 142 | true 143 | ) 144 | expect( 145 | Path.parse('*(aa.**,bb.**).bb').match(['aa', 'oo', 'kk', 'dd', 'bb']) 146 | ).toEqual(true) 147 | expect( 148 | Path.parse('*(aa.**,bb.**).bb').match(['cc', 'oo', 'kk', 'dd', 'bb']) 149 | ).toEqual(false) 150 | expect( 151 | Path.parse('*(aa.**,bb.**).bb').match(['bb', 'oo', 'kk', 'dd', 'bb']) 152 | ).toEqual(true) 153 | expect( 154 | Path.parse('*(aa.**,bb.**).bb').match(['kk', 'oo', 'kk', 'dd', 'bb']) 155 | ).toEqual(false) 156 | }) 157 | 158 | test('test expand', () => { 159 | expect( 160 | Path.parse('t.0.value~').match(['t', 0, 'value_list', 'hello']) 161 | ).toEqual(false) 162 | }) 163 | 164 | test('test multi expand', () => { 165 | expect(Path.parse('*(aa~,bb~).*').match(['aa12323', 'asdasd'])).toEqual(true) 166 | }) 167 | 168 | test('test group', () => { 169 | const node = Path.parse('*(phases.*.type,phases.*.steps.*.type)') 170 | expect(node.match('phases.0.steps.1.type')).toBeTruthy() 171 | }) 172 | 173 | test('test segments', () => { 174 | const node = Path.parse('a.0.b') 175 | expect(node.match(['a', 0, 'b'])).toEqual(true) 176 | }) 177 | 178 | test('nested group match', () => { 179 | expect( 180 | Path.parse('aa.*.*(bb,cc).dd.*(kk,oo).ee').match('aa.0.cc.dd.kk.ee') 181 | ).toEqual(true) 182 | }) 183 | 184 | test('group match with destructor', () => { 185 | expect(Path.parse('*([startDate,endDate],date,weak)').match('date')).toEqual( 186 | true 187 | ) 188 | expect(Path.parse('*({startDate,endDate},date,weak)').match('date')).toEqual( 189 | true 190 | ) 191 | expect(Path.parse('*([startDate,endDate],date,weak)').match('xxx')).toEqual( 192 | false 193 | ) 194 | expect(Path.parse('*({startDate,endDate},date,weak)').match('xxx')).toEqual( 195 | false 196 | ) 197 | expect( 198 | Path.parse('*([startDate,endDate],date,weak)').match('[startDate,endDate]') 199 | ).toEqual(true) 200 | expect( 201 | Path.parse('*({startDate,endDate},date,weak)').match('{startDate,endDate}') 202 | ).toEqual(true) 203 | }) 204 | 205 | test('all range match', () => { 206 | expect( 207 | Path.parse('array.*[:].*[:].*[:].bb').match('array.0.0.0.aa') 208 | ).toBeFalsy() 209 | }) 210 | 211 | match({ 212 | '*': [[], ['aa'], ['aa', 'bb', 'cc'], ['aa', 'dd', 'gg']], 213 | '*.a.b': [ 214 | ['c', 'a', 'b'], 215 | ['k', 'a', 'b'], 216 | ['m', 'a', 'b'], 217 | ], 218 | 'a.*.k': [ 219 | ['a', 'b', 'k'], 220 | ['a', 'd', 'k'], 221 | ['a', 'c', 'k'], 222 | ], 223 | 'a.*(b,d,m).k': [ 224 | ['a', 'b', 'k'], 225 | ['a', 'd', 'k'], 226 | ['a', 'm', 'k'], 227 | ], 228 | 'a.*(!b,d,m).*(!a,b)': [ 229 | ['a', 'o', 'k'], 230 | ['a', 'q', 'k'], 231 | ['a', 'c', 'k'], 232 | ], 233 | 'a.*(b.c.d,d,m).k': [ 234 | ['a', 'b', 'c', 'd', 'k'], 235 | ['a', 'd', 'k'], 236 | ['a', 'm', 'k'], 237 | ], 238 | 'a.*(b.*(c,k).d,d,m).k': [ 239 | ['a', 'b', 'c', 'd', 'k'], 240 | ['a', 'b', 'k', 'd', 'k'], 241 | ['a', 'd', 'k'], 242 | ['a', 'm', 'k'], 243 | ], 244 | 'a.b.*': [ 245 | ['a', 'b', 'c', 'd'], 246 | ['a', 'b', 'c'], 247 | ['a', 'b', 2, 'aaa', 3, 'bbb'], 248 | ], 249 | '*(step1,step2).*': [ 250 | ['step1', 'aa', 'bb'], 251 | ['step1', 'aa', 'bb', 'ccc', 'ddd'], 252 | ], 253 | 'dyanmic.*(!dynamic-1)': [ 254 | ['dyanmic', 'dynamic-2'], 255 | ['dyanmic', 'dynamic-3'], 256 | ], 257 | 't.0.value~': [['t', '0', 'value']], 258 | 'a.*[10:50].*(!a,b)': [ 259 | ['a', 49, 's'], 260 | ['a', 10, 's'], 261 | ['a', 50, 's'], 262 | ], 263 | 'a.*[10:].*(!a,b)': [ 264 | ['a', 49, 's'], 265 | ['a', 10, 's'], 266 | ['a', 50, 's'], 267 | ], 268 | 'a.*[].*(!a,b)': [ 269 | ['a', 49, 's'], 270 | ['a', 10, 's'], 271 | ['a', 50, 's'], 272 | ], 273 | 'a.*[:50].*(!a,b)': [ 274 | ['a', 49, 's'], 275 | ['a', 10, 's'], 276 | ['a', 50, 's'], 277 | ], 278 | 'a.*([[a.b.c]],[[c.b.d~]])': [ 279 | ['a', '[[a.b.c]]'], 280 | ['a', 'c.b.d~'], 281 | ], 282 | 'a.*(!k,d,m).k': [ 283 | ['a', 'u', 'k'], 284 | ['a', 'o', 'k'], 285 | ['a', 'p', 'k'], 286 | ], 287 | 'a\\.\\*\\[1\\]': [['a.*[1]']], 288 | '[[\\[aa,bb\\]]]': [['[aa,bb]']], 289 | '[[\\[aa,bb\\] ]]': [['[aa,bb] ']], 290 | '[[ \\[aa,bb~\\] ]]': [[' [aa,bb~] ']], 291 | 'aa.bb.*': [['aa', 'bb', 'ccc']], 292 | 'a.*': [ 293 | ['a', 'b'], 294 | ['a', 'b', 'c'], 295 | ], 296 | 'aa.*.*(bb,cc).dd': [['aa', '0', 'cc', 'dd']], 297 | 'aaa.products.0.*': [['aaa', 'products', '0', 'aaa']], 298 | 'aa~.ccc': [ 299 | ['aa', 'ccc'], 300 | ['aa12', 'ccc'], 301 | ], 302 | '*(aa~,bb~).*': [ 303 | ['aa12323', 'asdasd'], 304 | ['bb12222', 'asd'], 305 | ], 306 | '*(aa,bb,bb.aa)': [['bb', 'aa']], 307 | '*(!aa,bb,bb.aa)': [['xx'], ['yyy']], 308 | '*(!aaa)': [['bbb']], 309 | '*(!aaa,bbb)': [['ccc'], ['ggg']], 310 | '*([startDate,endDate],date,weak)': [['date']], 311 | }) 312 | 313 | unmatch({ 314 | 'a.*': [['a'], ['b']], 315 | '*(array)': [['array', '0']], 316 | 'aa.bb.*': [['aa', 'bb']], 317 | 'a.*.b': [['a', 'k', 'b', 'd']], 318 | '*(!aaa)': [['aaa']], 319 | 'dyanmic.*(!dynamic-1)': [['dyanmic', 'dynamic-1']], 320 | 'dyanmic.*(!dynamic-1.*)': [['dyanmic', 'dynamic-1', 'ccc']], 321 | a: [['c', 'b']], 322 | 'aa~.ccc': [['a', 'ccc'], ['aa'], ['aaasdd']], 323 | bb: [['bb', 'cc']], 324 | 'aa.*(cc,bb).*.aa': [['aa', 'cc', '0', 'bb']], 325 | }) 326 | ``` -------------------------------------------------------------------------------- /packages/next/src/array-table/index.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { 2 | Fragment, 3 | useState, 4 | useRef, 5 | useEffect, 6 | createContext, 7 | useContext, 8 | } from 'react' 9 | import { Table, Pagination, Select, Badge } from '@alifd/next' 10 | import { PaginationProps } from '@alifd/next/lib/pagination' 11 | import { TableProps, ColumnProps } from '@alifd/next/lib/table' 12 | import { SelectProps } from '@alifd/next/lib/select' 13 | import cls from 'classnames' 14 | import { GeneralField, FieldDisplayTypes, ArrayField } from '@formily/core' 15 | import { 16 | useField, 17 | observer, 18 | useFieldSchema, 19 | RecursionField, 20 | ReactFC, 21 | } from '@formily/react' 22 | import { isArr, isBool, isFn } from '@formily/shared' 23 | import { Schema } from '@formily/json-schema' 24 | import { usePrefixCls } from '../__builtins__' 25 | import { ArrayBase, ArrayBaseMixins, IArrayBaseProps } from '../array-base' 26 | 27 | interface ObservableColumnSource { 28 | field: GeneralField 29 | columnProps: ColumnProps 30 | schema: Schema 31 | display: FieldDisplayTypes 32 | name: string 33 | } 34 | 35 | interface IArrayTablePaginationProps extends Omit<PaginationProps, 'children'> { 36 | dataSource?: any[] 37 | children?: ( 38 | dataSource: any[], 39 | pagination: React.ReactNode 40 | ) => React.ReactElement 41 | } 42 | 43 | interface IStatusSelectProps extends SelectProps { 44 | pageSize?: number 45 | } 46 | 47 | export type ExtendTableProps = { 48 | pagination?: PaginationProps 49 | } & IArrayBaseProps & 50 | TableProps 51 | 52 | type ComposedArrayTable = ReactFC<ExtendTableProps> & 53 | ArrayBaseMixins & { 54 | Column?: ReactFC<ColumnProps> 55 | } 56 | 57 | interface PaginationAction { 58 | totalPage?: number 59 | pageSize?: number 60 | changePage?: (page: number) => void 61 | } 62 | 63 | const isColumnComponent = (schema: Schema) => { 64 | return schema['x-component']?.indexOf('Column') > -1 65 | } 66 | 67 | const isOperationsComponent = (schema: Schema) => { 68 | return schema['x-component']?.indexOf('Operations') > -1 69 | } 70 | 71 | const isAdditionComponent = (schema: Schema) => { 72 | return schema['x-component']?.indexOf('Addition') > -1 73 | } 74 | 75 | const useArrayTableSources = () => { 76 | const arrayField = useField() 77 | const schema = useFieldSchema() 78 | const parseSources = (schema: Schema): ObservableColumnSource[] => { 79 | if ( 80 | isColumnComponent(schema) || 81 | isOperationsComponent(schema) || 82 | isAdditionComponent(schema) 83 | ) { 84 | if (!schema['x-component-props']?.['dataIndex'] && !schema['name']) 85 | return [] 86 | const name = schema['x-component-props']?.['dataIndex'] || schema['name'] 87 | const field = arrayField.query(arrayField.address.concat(name)).take() 88 | const columnProps = 89 | field?.component?.[1] || schema['x-component-props'] || {} 90 | const display = field?.display || schema['x-display'] || 'visible' 91 | return [ 92 | { 93 | name, 94 | display, 95 | field, 96 | schema, 97 | columnProps, 98 | }, 99 | ] 100 | } else if (schema.properties) { 101 | return schema.reduceProperties((buf, schema) => { 102 | return buf.concat(parseSources(schema)) 103 | }, []) 104 | } 105 | } 106 | 107 | const parseArrayItems = (schema: Schema['items']) => { 108 | if (!schema) return [] 109 | const sources: ObservableColumnSource[] = [] 110 | const items = isArr(schema) ? schema : [schema] 111 | return items.reduce((columns, schema) => { 112 | const item = parseSources(schema) 113 | if (item) { 114 | return columns.concat(item) 115 | } 116 | return columns 117 | }, sources) 118 | } 119 | 120 | return parseArrayItems(schema.items) 121 | } 122 | 123 | const useArrayTableColumns = ( 124 | dataSource: any[], 125 | field: ArrayField, 126 | sources: ObservableColumnSource[] 127 | ): TableProps['columns'] => { 128 | return sources.reduce((buf, { name, columnProps, schema, display }, key) => { 129 | if (display !== 'visible') return buf 130 | if (!isColumnComponent(schema)) return buf 131 | return buf.concat({ 132 | ...columnProps, 133 | key, 134 | dataIndex: name, 135 | cell: (value: any, _: number, record: any) => { 136 | const index = dataSource?.indexOf(record) 137 | const children = ( 138 | <ArrayBase.Item 139 | key={index} 140 | index={index} 141 | record={() => field.value?.[index]} 142 | > 143 | <RecursionField schema={schema} name={index} onlyRenderProperties /> 144 | </ArrayBase.Item> 145 | ) 146 | return children 147 | }, 148 | }) 149 | }, []) 150 | } 151 | 152 | const useAddition = () => { 153 | const schema = useFieldSchema() 154 | return schema.reduceProperties((addition, schema, key) => { 155 | if (isAdditionComponent(schema)) { 156 | return <RecursionField schema={schema} name={key} /> 157 | } 158 | return addition 159 | }, null) 160 | } 161 | 162 | const schedulerRequest = { 163 | request: null, 164 | } 165 | 166 | const StatusSelect: ReactFC<IStatusSelectProps> = observer( 167 | ({ pageSize, ...props }) => { 168 | const field = useField<ArrayField>() 169 | const prefixCls = usePrefixCls('formily-array-table') 170 | const errors = field.errors 171 | const parseIndex = (address: string) => { 172 | return Number( 173 | address 174 | .slice(address.indexOf(field.address.toString()) + 1) 175 | .match(/(\d+)/)?.[1] 176 | ) 177 | } 178 | const options = props.dataSource?.map(({ label, value }) => { 179 | const hasError = errors.some(({ address }) => { 180 | const currentIndex = parseIndex(address) 181 | const startIndex = (value - 1) * pageSize 182 | const endIndex = value * pageSize 183 | return currentIndex >= startIndex && currentIndex <= endIndex 184 | }) 185 | return { 186 | label: hasError ? <Badge dot>{label}</Badge> : label, 187 | value, 188 | } 189 | }) 190 | 191 | return ( 192 | <Select 193 | {...props} 194 | value={props.value} 195 | onChange={props.onChange} 196 | dataSource={options} 197 | useVirtual 198 | className={cls(`${prefixCls}-status-select`, { 199 | 'has-error': errors?.length, 200 | })} 201 | /> 202 | ) 203 | }, 204 | { 205 | scheduler: (update) => { 206 | clearTimeout(schedulerRequest.request) 207 | schedulerRequest.request = setTimeout(() => { 208 | update() 209 | }, 100) 210 | }, 211 | } 212 | ) 213 | 214 | const PaginationContext = createContext<PaginationAction>({}) 215 | const usePagination = () => { 216 | return useContext(PaginationContext) 217 | } 218 | 219 | const ArrayTablePagination: ReactFC<IArrayTablePaginationProps> = ({ 220 | children, 221 | dataSource, 222 | ...props 223 | }) => { 224 | const [current, setCurrent] = useState(1) 225 | const prefixCls = usePrefixCls('formily-array-table') 226 | const pageSize = props.pageSize || 10 227 | const size = props.size || 'medium' 228 | const sources = dataSource || [] 229 | const startIndex = (current - 1) * pageSize 230 | const endIndex = startIndex + pageSize - 1 231 | const total = sources?.length || 0 232 | const totalPage = Math.ceil(total / pageSize) 233 | const pages = Array.from(new Array(totalPage)).map((_, index) => { 234 | const page = index + 1 235 | return { 236 | label: page, 237 | value: page, 238 | } 239 | }) 240 | const handleChange = (current: number) => { 241 | setCurrent(current) 242 | } 243 | 244 | useEffect(() => { 245 | if (totalPage > 0 && totalPage < current) { 246 | handleChange(totalPage) 247 | } 248 | }, [totalPage, current]) 249 | 250 | const renderPagination = () => { 251 | if (totalPage <= 1) return 252 | return ( 253 | <div className={`${prefixCls}-pagination`}> 254 | <StatusSelect 255 | value={current} 256 | pageSize={pageSize} 257 | onChange={handleChange} 258 | dataSource={pages} 259 | notFoundContent={false} 260 | /> 261 | <Pagination 262 | {...props} 263 | pageSize={pageSize} 264 | current={current} 265 | total={dataSource.length} 266 | size={size} 267 | pageSizeSelector={false} 268 | onChange={handleChange} 269 | /> 270 | </div> 271 | ) 272 | } 273 | 274 | return ( 275 | <Fragment> 276 | <PaginationContext.Provider 277 | value={{ totalPage, pageSize, changePage: handleChange }} 278 | > 279 | {children?.( 280 | dataSource?.slice(startIndex, endIndex + 1), 281 | renderPagination() 282 | )} 283 | </PaginationContext.Provider> 284 | </Fragment> 285 | ) 286 | } 287 | 288 | const omit = (props: any, keys?: string[]) => { 289 | return Object.keys(props) 290 | .filter((key) => !keys?.includes(key)) 291 | .reduce((buf, key) => { 292 | buf[key] = props[key] 293 | return buf 294 | }, {}) 295 | } 296 | 297 | export const ArrayTable: ComposedArrayTable = observer( 298 | (props: ExtendTableProps) => { 299 | const ref = useRef<HTMLDivElement>() 300 | const field = useField<ArrayField>() 301 | const prefixCls = usePrefixCls('formily-array-table') 302 | const dataSource = Array.isArray(field.value) ? field.value.slice() : [] 303 | const sources = useArrayTableSources() 304 | const columns = useArrayTableColumns(dataSource, field, sources) 305 | const pagination = isBool(props.pagination) ? {} : props.pagination 306 | const { onAdd, onCopy, onRemove, onMoveDown, onMoveUp } = props 307 | const addition = useAddition() 308 | 309 | return ( 310 | <ArrayTablePagination {...pagination} dataSource={dataSource}> 311 | {(dataSource, pager) => ( 312 | <div ref={ref} className={prefixCls}> 313 | <ArrayBase 314 | onAdd={onAdd} 315 | onCopy={onCopy} 316 | onRemove={onRemove} 317 | onMoveUp={onMoveUp} 318 | onMoveDown={onMoveDown} 319 | > 320 | <Table 321 | size="small" 322 | {...omit(props, ['value', 'onChange', 'pagination'])} 323 | columns={columns} 324 | dataSource={dataSource} 325 | /> 326 | <div style={{ marginTop: 5, marginBottom: 5 }}>{pager}</div> 327 | {sources.map((column, key) => { 328 | //专门用来承接对Column的状态管理 329 | if (!isColumnComponent(column.schema)) return 330 | return React.createElement(RecursionField, { 331 | name: column.name, 332 | schema: column.schema, 333 | onlyRenderSelf: true, 334 | key, 335 | }) 336 | })} 337 | {addition} 338 | </ArrayBase> 339 | </div> 340 | )} 341 | </ArrayTablePagination> 342 | ) 343 | } 344 | ) 345 | 346 | ArrayTable.displayName = 'ArrayTable' 347 | 348 | ArrayTable.Column = () => { 349 | return <Fragment /> 350 | } 351 | 352 | ArrayBase.mixin(ArrayTable) 353 | 354 | const Addition: ArrayBaseMixins['Addition'] = (props) => { 355 | const array = ArrayBase.useArray() 356 | const { totalPage = 0, pageSize = 10, changePage } = usePagination() 357 | return ( 358 | <ArrayBase.Addition 359 | {...props} 360 | onClick={(e) => { 361 | // 如果添加数据后将超过当前页,则自动切换到下一页 362 | const total = array?.field?.value.length || 0 363 | if (total === totalPage * pageSize + 1 && isFn(changePage)) { 364 | changePage(totalPage + 1) 365 | } 366 | props.onClick?.(e) 367 | }} 368 | /> 369 | ) 370 | } 371 | ArrayTable.Addition = Addition 372 | 373 | export default ArrayTable 374 | ``` -------------------------------------------------------------------------------- /packages/core/src/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | IValidatorRules, 3 | Validator, 4 | ValidatorTriggerType, 5 | } from '@formily/validator' 6 | import { FormPath } from '@formily/shared' 7 | import { 8 | Form, 9 | Field, 10 | LifeCycle, 11 | ArrayField, 12 | VoidField, 13 | ObjectField, 14 | Query, 15 | } from './models' 16 | 17 | export type NonFunctionPropertyNames<T> = { 18 | [K in keyof T]: T[K] extends (...args: any) => any ? never : K 19 | }[keyof T] 20 | 21 | export type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>> 22 | 23 | export type AnyFunction = (...args: any[]) => any 24 | 25 | export type JSXComponent = any 26 | 27 | export type LifeCycleHandler<T> = (payload: T, context: any) => void 28 | 29 | export type LifeCyclePayload<T> = ( 30 | params: { 31 | type: string 32 | payload: T 33 | }, 34 | context: any 35 | ) => void 36 | 37 | export enum LifeCycleTypes { 38 | /** 39 | * Form LifeCycle 40 | **/ 41 | 42 | ON_FORM_INIT = 'onFormInit', 43 | ON_FORM_MOUNT = 'onFormMount', 44 | ON_FORM_UNMOUNT = 'onFormUnmount', 45 | 46 | ON_FORM_INPUT_CHANGE = 'onFormInputChange', 47 | ON_FORM_VALUES_CHANGE = 'onFormValuesChange', 48 | ON_FORM_INITIAL_VALUES_CHANGE = 'onFormInitialValuesChange', 49 | 50 | ON_FORM_SUBMIT = 'onFormSubmit', 51 | ON_FORM_RESET = 'onFormReset', 52 | ON_FORM_SUBMIT_START = 'onFormSubmitStart', 53 | ON_FORM_SUBMITTING = 'onFormSubmitting', 54 | ON_FORM_SUBMIT_END = 'onFormSubmitEnd', 55 | ON_FORM_SUBMIT_VALIDATE_START = 'onFormSubmitValidateStart', 56 | ON_FORM_SUBMIT_VALIDATE_SUCCESS = 'onFormSubmitValidateSuccess', 57 | ON_FORM_SUBMIT_VALIDATE_FAILED = 'onFormSubmitValidateFailed', 58 | ON_FORM_SUBMIT_VALIDATE_END = 'onFormSubmitValidateEnd', 59 | ON_FORM_SUBMIT_SUCCESS = 'onFormSubmitSuccess', 60 | ON_FORM_SUBMIT_FAILED = 'onFormSubmitFailed', 61 | ON_FORM_VALIDATE_START = 'onFormValidateStart', 62 | ON_FORM_VALIDATING = 'onFormValidating', 63 | ON_FORM_VALIDATE_SUCCESS = 'onFormValidateSuccess', 64 | ON_FORM_VALIDATE_FAILED = 'onFormValidateFailed', 65 | ON_FORM_VALIDATE_END = 'onFormValidateEnd', 66 | 67 | ON_FORM_GRAPH_CHANGE = 'onFormGraphChange', 68 | ON_FORM_LOADING = 'onFormLoading', 69 | 70 | /** 71 | * Field LifeCycle 72 | **/ 73 | 74 | ON_FIELD_INIT = 'onFieldInit', 75 | ON_FIELD_INPUT_VALUE_CHANGE = 'onFieldInputValueChange', 76 | ON_FIELD_VALUE_CHANGE = 'onFieldValueChange', 77 | ON_FIELD_INITIAL_VALUE_CHANGE = 'onFieldInitialValueChange', 78 | 79 | ON_FIELD_SUBMIT = 'onFieldSubmit', 80 | ON_FIELD_SUBMIT_START = 'onFieldSubmitStart', 81 | ON_FIELD_SUBMITTING = 'onFieldSubmitting', 82 | ON_FIELD_SUBMIT_END = 'onFieldSubmitEnd', 83 | ON_FIELD_SUBMIT_VALIDATE_START = 'onFieldSubmitValidateStart', 84 | ON_FIELD_SUBMIT_VALIDATE_SUCCESS = 'onFieldSubmitValidateSuccess', 85 | ON_FIELD_SUBMIT_VALIDATE_FAILED = 'onFieldSubmitValidateFailed', 86 | ON_FIELD_SUBMIT_VALIDATE_END = 'onFieldSubmitValidateEnd', 87 | ON_FIELD_SUBMIT_SUCCESS = 'onFieldSubmitSuccess', 88 | ON_FIELD_SUBMIT_FAILED = 'onFieldSubmitFailed', 89 | ON_FIELD_VALIDATE_START = 'onFieldValidateStart', 90 | ON_FIELD_VALIDATING = 'onFieldValidating', 91 | ON_FIELD_VALIDATE_SUCCESS = 'onFieldValidateSuccess', 92 | ON_FIELD_VALIDATE_FAILED = 'onFieldValidateFailed', 93 | ON_FIELD_VALIDATE_END = 'onFieldValidateEnd', 94 | 95 | ON_FIELD_LOADING = 'onFieldLoading', 96 | ON_FIELD_RESET = 'onFieldReset', 97 | ON_FIELD_MOUNT = 'onFieldMount', 98 | ON_FIELD_UNMOUNT = 'onFieldUnmount', 99 | } 100 | 101 | export type HeartSubscriber = ({ 102 | type, 103 | payload, 104 | }: { 105 | type: string 106 | payload: any 107 | }) => void 108 | 109 | export interface INodePatch<T> { 110 | type: 'remove' | 'update' 111 | address: string 112 | oldAddress?: string 113 | payload?: T 114 | } 115 | 116 | export interface IHeartProps<Context> { 117 | lifecycles?: LifeCycle[] 118 | context?: Context 119 | } 120 | 121 | export interface IFieldFeedback { 122 | triggerType?: FieldFeedbackTriggerTypes 123 | type?: FieldFeedbackTypes 124 | code?: FieldFeedbackCodeTypes 125 | messages?: FeedbackMessage 126 | } 127 | 128 | export type IFormFeedback = IFieldFeedback & { 129 | path?: string 130 | address?: string 131 | } 132 | 133 | export interface ISearchFeedback { 134 | triggerType?: FieldFeedbackTriggerTypes 135 | type?: FieldFeedbackTypes 136 | code?: FieldFeedbackCodeTypes 137 | address?: FormPathPattern 138 | path?: FormPathPattern 139 | messages?: FeedbackMessage 140 | } 141 | 142 | export type FeedbackMessage = any[] 143 | 144 | export type IFieldUpdate = { 145 | pattern: FormPath 146 | callbacks: ((...args: any[]) => any)[] 147 | } 148 | 149 | export interface IFormRequests { 150 | validate?: number 151 | submit?: number 152 | loading?: number 153 | updates?: IFieldUpdate[] 154 | updateIndexes?: Record<string, number> 155 | } 156 | 157 | export type IFormFields = Record<string, GeneralField> 158 | 159 | export type FieldFeedbackTypes = 'error' | 'success' | 'warning' 160 | 161 | export type FieldFeedbackTriggerTypes = ValidatorTriggerType 162 | 163 | export type FieldFeedbackCodeTypes = 164 | | 'ValidateError' 165 | | 'ValidateSuccess' 166 | | 'ValidateWarning' 167 | | 'EffectError' 168 | | 'EffectSuccess' 169 | | 'EffectWarning' 170 | | (string & {}) 171 | 172 | export type FormPatternTypes = 173 | | 'editable' 174 | | 'readOnly' 175 | | 'disabled' 176 | | 'readPretty' 177 | | ({} & string) 178 | export type FormDisplayTypes = 'none' | 'hidden' | 'visible' | ({} & string) 179 | 180 | export type FormPathPattern = 181 | | string 182 | | number 183 | | Array<string | number> 184 | | FormPath 185 | | RegExp 186 | | (((address: Array<string | number>) => boolean) & { 187 | path: FormPath 188 | }) 189 | 190 | type OmitState<P> = Omit< 191 | P, 192 | | 'selfDisplay' 193 | | 'selfPattern' 194 | | 'originValues' 195 | | 'originInitialValues' 196 | | 'id' 197 | | 'address' 198 | | 'path' 199 | | 'lifecycles' 200 | | 'disposers' 201 | | 'requests' 202 | | 'fields' 203 | | 'graph' 204 | | 'heart' 205 | | 'indexes' 206 | | 'props' 207 | | 'displayName' 208 | | 'setState' 209 | | 'getState' 210 | | 'getFormGraph' 211 | | 'setFormGraph' 212 | | 'setFormState' 213 | | 'getFormState' 214 | > 215 | 216 | export type IFieldState = Partial< 217 | Pick< 218 | Field, 219 | NonFunctionPropertyNames<OmitState<Field<any, any, string, string>>> 220 | > 221 | > 222 | 223 | export type IVoidFieldState = Partial< 224 | Pick< 225 | VoidField, 226 | NonFunctionPropertyNames<OmitState<VoidField<any, any, string>>> 227 | > 228 | > 229 | 230 | export type IFormState<T extends Record<any, any> = any> = Pick< 231 | Form<T>, 232 | NonFunctionPropertyNames<OmitState<Form<{ [key: string]: any }>>> 233 | > 234 | 235 | export type IFormGraph = Record<string, IGeneralFieldState | IFormState> 236 | 237 | export interface IFormProps<T extends object = any> { 238 | values?: Partial<T> 239 | initialValues?: Partial<T> 240 | pattern?: FormPatternTypes 241 | display?: FormDisplayTypes 242 | hidden?: boolean 243 | visible?: boolean 244 | editable?: boolean 245 | disabled?: boolean 246 | readOnly?: boolean 247 | readPretty?: boolean 248 | effects?: (form: Form<T>) => void 249 | validateFirst?: boolean 250 | validatePattern?: FormPatternTypes[] 251 | validateDisplay?: FormDisplayTypes[] 252 | designable?: boolean 253 | } 254 | 255 | export type IFormMergeStrategy = 256 | | 'overwrite' 257 | | 'merge' 258 | | 'deepMerge' 259 | | 'shallowMerge' 260 | 261 | export interface IFieldFactoryProps< 262 | Decorator extends JSXComponent, 263 | Component extends JSXComponent, 264 | TextType = any, 265 | ValueType = any 266 | > extends IFieldProps<Decorator, Component, TextType, ValueType> { 267 | name: FormPathPattern 268 | basePath?: FormPathPattern 269 | } 270 | 271 | export interface IVoidFieldFactoryProps< 272 | Decorator extends JSXComponent, 273 | Component extends JSXComponent, 274 | TextType = any 275 | > extends IVoidFieldProps<Decorator, Component, TextType> { 276 | name: FormPathPattern 277 | basePath?: FormPathPattern 278 | } 279 | 280 | export interface IFieldRequests { 281 | validate?: number 282 | submit?: number 283 | loading?: number 284 | batch?: () => void 285 | } 286 | 287 | export interface IFieldCaches { 288 | value?: any 289 | initialValue?: any 290 | inputting?: boolean 291 | } 292 | 293 | export type FieldDisplayTypes = 'none' | 'hidden' | 'visible' | ({} & string) 294 | 295 | export type FieldPatternTypes = 296 | | 'editable' 297 | | 'readOnly' 298 | | 'disabled' 299 | | 'readPretty' 300 | | ({} & string) 301 | 302 | export type FieldValidatorContext = IValidatorRules & { 303 | field?: Field 304 | form?: Form 305 | value?: any 306 | } 307 | 308 | export type FieldValidator = Validator<FieldValidatorContext> 309 | 310 | export type FieldDataSource = { 311 | label?: any 312 | value?: any 313 | title?: any 314 | key?: any 315 | text?: any 316 | children?: FieldDataSource 317 | [key: string]: any 318 | }[] 319 | 320 | export type FieldComponent< 321 | Component extends JSXComponent, 322 | ComponentProps = any 323 | > = [Component] | [Component, ComponentProps] | boolean | any[] 324 | 325 | export type FieldDecorator< 326 | Decorator extends JSXComponent, 327 | ComponentProps = any 328 | > = [Decorator] | [Decorator, ComponentProps] | boolean | any[] 329 | 330 | export type FieldReaction = (field: Field) => void 331 | export interface IFieldProps< 332 | Decorator extends JSXComponent = any, 333 | Component extends JSXComponent = any, 334 | TextType = any, 335 | ValueType = any 336 | > { 337 | name: FormPathPattern 338 | basePath?: FormPathPattern 339 | title?: TextType 340 | description?: TextType 341 | value?: ValueType 342 | initialValue?: ValueType 343 | required?: boolean 344 | display?: FieldDisplayTypes 345 | pattern?: FieldPatternTypes 346 | hidden?: boolean 347 | visible?: boolean 348 | editable?: boolean 349 | disabled?: boolean 350 | readOnly?: boolean 351 | readPretty?: boolean 352 | dataSource?: FieldDataSource 353 | validateFirst?: boolean 354 | validatePattern?: FieldPatternTypes[] 355 | validateDisplay?: FieldDisplayTypes[] 356 | validator?: FieldValidator 357 | decorator?: FieldDecorator<Decorator> 358 | component?: FieldComponent<Component> 359 | reactions?: FieldReaction[] | FieldReaction 360 | content?: any 361 | data?: any 362 | } 363 | 364 | export interface IVoidFieldProps< 365 | Decorator extends JSXComponent = any, 366 | Component extends JSXComponent = any, 367 | TextType = any 368 | > { 369 | name: FormPathPattern 370 | basePath?: FormPathPattern 371 | title?: TextType 372 | description?: TextType 373 | display?: FieldDisplayTypes 374 | pattern?: FieldPatternTypes 375 | hidden?: boolean 376 | visible?: boolean 377 | editable?: boolean 378 | disabled?: boolean 379 | readOnly?: boolean 380 | readPretty?: boolean 381 | decorator?: FieldDecorator<Decorator> 382 | component?: FieldComponent<Component> 383 | reactions?: FieldReaction[] | FieldReaction 384 | content?: any 385 | data?: any 386 | } 387 | 388 | export interface IFieldResetOptions { 389 | forceClear?: boolean 390 | validate?: boolean 391 | } 392 | 393 | export type IGeneralFieldState = IFieldState & IVoidFieldState 394 | 395 | export type GeneralField = Field | VoidField | ArrayField | ObjectField 396 | 397 | export type DataField = Field | ArrayField | ObjectField 398 | export interface ISpliceArrayStateProps { 399 | startIndex?: number 400 | deleteCount?: number 401 | insertCount?: number 402 | } 403 | 404 | export interface IExchangeArrayStateProps { 405 | fromIndex?: number 406 | toIndex?: number 407 | } 408 | 409 | export interface IQueryProps { 410 | pattern: FormPathPattern 411 | base: FormPathPattern 412 | form: Form 413 | } 414 | 415 | export interface IModelSetter<P = any> { 416 | (setter: (state: P) => void): void 417 | (setter: Partial<P>): void 418 | (): void 419 | } 420 | 421 | export interface IModelGetter<P = any> { 422 | <Getter extends (state: P) => any>(getter: Getter): ReturnType<Getter> 423 | (): P 424 | } 425 | 426 | export type FieldMatchPattern = FormPathPattern | Query | GeneralField 427 | 428 | export interface IFieldStateSetter { 429 | (pattern: FieldMatchPattern, setter: (state: IFieldState) => void): void 430 | (pattern: FieldMatchPattern, setter: Partial<IFieldState>): void 431 | } 432 | 433 | export interface IFieldStateGetter { 434 | <Getter extends (state: IGeneralFieldState) => any>( 435 | pattern: FieldMatchPattern, 436 | getter: Getter 437 | ): ReturnType<Getter> 438 | (pattern: FieldMatchPattern): IGeneralFieldState 439 | } 440 | 441 | export interface IFieldActions { 442 | [key: string]: (...args: any[]) => any 443 | } 444 | ``` -------------------------------------------------------------------------------- /packages/core/docs/guide/form.md: -------------------------------------------------------------------------------- ```markdown 1 | # Form model 2 | 3 | Earlier I talked about the overall architecture of the Formily kernel, and also talked about MVVM. You should also be able to roughly understand what Formily's form model is. Let's take a deeper look at the specific domain logic of the form model, which is mainly biased. Concluding content, if you don't understand it the first time, you can go directly to the API documentation, and come back after reading it, you can deepen your understanding of formily. 4 | 5 | ## Combing 6 | 7 | The entire form model is very large and complicated. In fact, the core of the decomposition is the following sub-models: 8 | 9 | - Field management model 10 | - Field model 11 | - Data model 12 | - Linkage model 13 | - Path system 14 | 15 | Let's talk about how the form model is managed in detail below. 16 | 17 | ## Field Management Model 18 | 19 | The field management model mainly includes: 20 | 21 | - Field addition 22 | - Field query 23 | - Import field set 24 | - Export field set 25 | - Clear the field set 26 | 27 | #### Field addition 28 | 29 | The field is created mainly through the createField/createArrayField/createObjectField/createVoidField method. If the field already exists, it will not be created repeatedly 30 | 31 | #### Field query 32 | 33 | The query method is mainly used to query the field. The query method can pass in the path of the field or regular expression to match the field. 34 | 35 | Because the detailed rules of the field path are still more complicated, they will be explained in detail in the following [Path System](/api/entry/form-path) article. 36 | 37 | Then calling the query method will return a Query object. The Query object can have a forEach/map/reduce method that traverses all fields in batches, or a take method that takes only the first field that is queried, as well as direct reading of fields. The get method of properties, and the getIn method that can read field properties in depth, the difference between the two methods is that the former can have smart prompts, and the latter has no prompts, so it is recommended that users use the get method. 38 | 39 | #### Import field set 40 | 41 | The field set is imported mainly through setFormGraph. The input parameter format is a flat object format, the key is the absolute path of the field, and the value is the state of the field. Use this API to import the Immutable field state into the form in some scenarios that require time travel. In the model. 42 | 43 | #### Export field set 44 | 45 | The field set is mainly exported through getFormGraph. The export format is a flat object format, the key is the absolute path of the field, and the value is the state of the field, which is consistent with the imported field set input parameters. Because the returned data is an Immutable data, it is OK Completely persistent storage, convenient for time travel. 46 | 47 | #### Clear the field set 48 | 49 | The field set is cleared mainly through clearFormGraph. 50 | 51 | ## Field Model 52 | 53 | The field model mainly includes: 54 | 55 | - Field model, which is mainly responsible for managing the state of non-incremental fields, such as Input/Select/NumberPicker/DatePicker components 56 | - The ArrayField model is mainly responsible for managing the state of the auto-increment list field, and can add, delete, and move list items. 57 | - ObjectField model, which is mainly responsible for managing the state of auto-incremented object fields, and can add or delete the key of the object. 58 | - The VoidField model is mainly responsible for managing the state of the virtual field. The virtual field is a node that does not pollute the form data, but it can control the display and hiding of its child nodes, and the interactive mode. 59 | 60 | Because the field model is very complicated, it will be explained in detail in the following [Field Model](/guide/field) article. 61 | 62 | ## Data Model 63 | 64 | For the form data model, the previous version of Formily will more or less have some boundary problems. After reorganizing a version in 2.x, it really broke through the previous legacy problems. 65 | 66 | The data model mainly includes: 67 | 68 | - Form values (values) management 69 | - Form default value (initialValues) management 70 | - Field value (value) management 71 | - Field default value (initialValue) management 72 | - Value and default value selection merge strategy 73 | 74 | Form value management is actually the values attribute of an object structure, but it is an @formily/reactive observable attribute. At the same time, with the help of @formily/reactive's deep observer capability, it monitors any attribute changes, and if it changes, it will trigger The life cycle hook of onFormValuesChange. 75 | 76 | In the same way, the default value management is actually the initialValues property of an object structure. It will also deeply monitor property changes and trigger the onFormInitialValues life cycle hook. 77 | 78 | Field value management is reflected in the value attribute of each data type field. Formily will maintain a data path attribute called path for each field, and then read and write values are all read and write the values of the top-level form. This ensures that the value of the field and the value of the form are absolutely idempotent, and the default value of the field is the same. 79 | 80 | To sum up, the management of **values is all managed on the top-level form, and the value of the field and the value of the form are absolutely idempotent through path. ** 81 | 82 | <Alert> 83 | 84 | The difference between the value and the default value is actually whether the field will be reset to the default value state when the form is reset 85 | 86 | </Alert> 87 | 88 | #### Value and default value selection merge strategy 89 | 90 | Usually, in the process of business development, there is always a need for data echo. This data is generally used as an asynchronous default value. If it is used as a detail page, it is okay, but as an editing page, there will be some problems. : 91 | 92 | **There is a conflict** 93 | 94 | For example, the form value is `{xx:123}`, and the default form value is `{xx:321}`. The strategy here is: 95 | 96 | - If `xx` does not have a corresponding field model, it means it is just redundant data and cannot be modified by the user 97 | - If the form value is assigned first, and the default value is assigned later, then the default value directly overrides the form value. This scenario is suitable for asynchronous data echo scenarios. Different business states have different default data echoes, and the data is finally submitted.` {xx:321}` 98 | - If the default value is assigned first, and the form value is assigned later, the form value directly overrides the default value. This scenario is suitable for synchronizing the default value and finally submitting the data `{xx:123}` 99 | - If `xx` has a field model 100 | - If the form value is assigned first, the default value is assigned later 101 | - If the current field has been modified by the user (modified is true), then the default value cannot overwrite the form value, and finally submit the data `{xx:123}` 102 | - If the current field has not been modified by the user (modified is false), then the default value will directly override the field value. This scenario is suitable for asynchronous data echo scenarios. Different business states have different default data echoes, and the data is finally submitted `{xx:321}` 103 | - If the default value is assigned first, and the form value is assigned later, the form value directly overrides the default value. This scenario is suitable for synchronizing the default value and finally submitting the data `{xx:123}` 104 | 105 | **No conflicts** 106 | 107 | For example, the form value is `{xx:123}`, and the default form value is `{yy:321}`. The strategy here is to merge directly. 108 | 109 | To sum up, the selection and merging strategy of the value and the default value, the core is to see whether the field has been modified by the user, everything is subject to the user, if it has not been modified by the user, the order of assignment shall prevail.\*\* 110 | 111 | <Alert> 112 | 113 | The default value mentioned here can be assigned repeatedly, and it is also the question of whether to discard the value in the process of repeated assignment. 114 | 115 | </Alert> 116 | 117 | ## Validation model 118 | 119 | The core of the form verification model is to verify the validity of the data, and then manage the verification results, so the verification model mainly includes: 120 | 121 | - Validation rule management 122 | - Calibration result management 123 | 124 | Because the verification model belongs to the field model, it will be explained in detail in the following [Field Model](/guide/field#Verification Rules) 125 | 126 | ## Linkage model 127 | 128 | The core of the linkage model in formily1.x is the active linkage model, which is roughly expressed in one sentence: 129 | 130 | ```ts 131 | setFieldState(Subscribe(FormLifeCycle, Selector(Path)), TargetState) 132 | ``` 133 | 134 | The explanation is that any linkage is based on a certain life cycle hook of the form to trigger the state of the field under the specified path. Such a model can solve many problems, but it also has an obvious problem, which is the many-to-one linkage. In the scenario where you need to monitor changes in multiple fields at the same time to control the state of a field, the implementation cost is still relatively high for users, especially to achieve some calculator linkage requirements, and the amount of code increases sharply. Of course, for one-to-many scenarios, this model is the most efficient. 135 | 136 | Therefore, in formily 2.x, a passive linkage model is added to the active linkage model, which is also an expression: 137 | 138 | ```ts 139 | subscribe(Dependencies, Reactions) 140 | ``` 141 | 142 | Simplified a lot, the core is to respond to dependent data changes. The dependent data can be form model attributes or attributes of any field model. The response action can be to change the attributes of any field model or do other asynchronous actions. . Such a model is also a complete linkage model, but in a one-to-many scenario, the implementation cost will be higher than the active model. 143 | 144 | Therefore, the two linkage models require users to choose according to their own needs. 145 | 146 | ## Path system 147 | 148 | The path system is very important. The path system is used everywhere in almost the entire form model. It mainly provides the following capabilities for the form model: 149 | 150 | - It can be used to find any field from the field set, and it also supports batch search according to the rules 151 | - It can be used to express the model of the relationship between the fields. With the help of the path system, we can find the father of a certain field, can find the father, and can also realize the data inheritance ability of the tree level. Similarly, we can also find the data of a certain field. Adjacent node 152 | - It can be used to read and write field data, read and write data with deconstruction 153 | 154 | The entire path system is actually implemented based on the path DSL of @formily/path. If you want to know more about the path system, you can take a look at [FormPath API](/api/entry/form-path) in detail 155 | ``` -------------------------------------------------------------------------------- /packages/react/docs/guide/concept.md: -------------------------------------------------------------------------------- ```markdown 1 | # Core idea 2 | 3 | The architecture of @formily/react itself is not complicated, because it only provides a series of components and Hooks for users to use, but we still need to understand the following concepts: 4 | 5 | - Form context 6 | - Field context 7 | - Protocol context 8 | - Model binding 9 | - Protocol driven 10 | - Three development modes 11 | 12 | ## Form context 13 | 14 | From the [architecture diagram](/guide/architecture) we can see that FormProvider exists as a unified context for forms, and its position is very important. It is mainly used to create [Form](//core. formilyjs.org/api/models/form) instances are distributed to all sub-components, whether in built-in components or user-extended components, can be read through [useForm](/api/hooks/use-form) [ Form](//core.formilyjs.org/api/models/form) instance 15 | 16 | ## Field context 17 | 18 | From the [architecture diagram](/guide/architecture) we can see that whether it is Field/ArrayField/ObjectField/VoidField, a FieldContext will be issued to the subtree. We can read the current field model in the custom component, mainly Use [useField](/api/hooks/use-field) to read, which is very convenient for model mapping 19 | 20 | ## Protocol context 21 | 22 | From the [architecture diagram](/guide/architecture) we can see that [RecursionField](/api/components/recursion-field) will send a FieldSchemaContext to the subtree, and we can read the current field in the custom component The Schema description is mainly read using [useFieldSchema](/api/hooks/useFieldSchema). Note that this Hook can only be used in the [SchemaField](/api/components/SchemaField) and [RecursionField](/api/components/recursion-field) subtrees 23 | 24 | ## Model binding 25 | 26 | To understand model binding, you need to understand what [MVVM](//core.formilyjs.org/guide/mvvm) is. After understanding, let’s take a look at this picture: 27 | 28 |  29 | 30 | In Formily, @formily/core is ViewModel, Component and Decorator are View, @formily/react is the glue layer that binds ViewModel and View, and the binding of ViewModel and View is called model binding, which implements model binding. The main methods are [useField](/api/hooks/use-field), and [connect](/api/shared/connect) and [mapProps](/api/shared/map-props) can also be used. Note that Component only needs to support the value/onChange property to automatically realize the two-way binding of the data layer. 31 | 32 | ## JSON Schema Driver 33 | 34 | Protocol-driven rendering is the most expensive part of @formily/react, but after learning it, the benefits it brings to the business are also very high. A total of 4 core concepts need to be understood: 35 | 36 | - Schema 37 | - Recursive rendering 38 | - Protocol binding 39 | - Three development modes 40 | 41 | ### Schema 42 | 43 | Formily’s protocol driver is mainly based on the standard JSON Schema to drive rendering. At the same time, we have extended some `x-*` attributes to express the UI on top of the standard, so that the entire protocol can fully describe a complex form. Schema protocol, refer to [Schema](/api/shared/schema) API document 44 | 45 | ### Recursive rendering 46 | 47 | What is recursive rendering? Recursive rendering means that component A will continue to use component A to render content under certain conditions. Take a look at the following pseudo code: 48 | 49 | ```json 50 | {<---- RecursionField (condition: object; rendering right: RecursionField) 51 | "type":"object", 52 | "properties":{ 53 | "username":{ <---- RecursionField (condition: string; rendering right: RecursionField) 54 | "type":"string", 55 | "x-component":"Input" 56 | }, 57 | "phone":{ <---- RecursionField (condition: string; rendering right: RecursionField) 58 | "type":"string", 59 | "x-component":"Input", 60 | "x-validator":"phone" 61 | }, 62 | "email":{ <---- RecursionField (condition: string; rendering right: RecursionField) 63 | "type":"string", 64 | "x-component":"Input", 65 | "x-validator":"email" 66 | }, 67 | "contacts":{ <---- RecursionField (condition: array; rendering right: RecursionField) 68 | "type":"array", 69 | "x-component":"ArrayTable", 70 | "items":{ <---- RecursionField (condition: object; rendering rights: ArrayTable component) 71 | "type":"object", 72 | "properties":{ 73 | "username":{ <---- RecursionField (condition: string; rendering right: RecursionField) 74 | "type":"string", 75 | "x-component":"Input" 76 | }, 77 | "phone":{ <---- RecursionField (condition: string; rendering right: RecursionField) 78 | "type":"string", 79 | "x-component":"Input", 80 | "x-validator":"phone" 81 | }, 82 | "email":{ <---- RecursionField (condition: string; rendering right: RecursionField) 83 | "type":"string", 84 | "x-component":"Input", 85 | "x-validator":"email" 86 | }, 87 | } 88 | } 89 | } 90 | } 91 | } 92 | ``` 93 | 94 | @formily/react The entry point for recursive rendering is [SchemaField](/api/components/schema-field), but it actually uses [RecursionField](/api/components/recursion-field) to render internally, because of JSON-Schema It is a recursive structure, so [RecursionField](/api/components/recursion-field) will be parsed from the top-level Schema node when rendering. If it is a non-object and array type, it will directly render the specific component. If it is an object, it will traverse. properties Continue to use [RecursionField](/api/components/recursion-field) to render child Schema nodes. 95 | 96 | A special case here is the rendering of the array type auto-increment list, which requires the user to use [RecursionField](/api/components/recursion-field) in the custom component for recursive rendering, because the UI of the auto-increment list is very customized High, so the recursive rendering rights are handed over to the user to render, so the design can also make protocol-driven rendering more flexible. 97 | 98 | What is the difference between SchemaField and RecursionField? There are two main points: 99 | 100 | - SchemaField supports Markup grammar, it will parse Markup grammar in advance to generate [JSON Schema](/api/shared/schema) and transfer it to RecursionField for rendering, so RecursionField can only be rendered based on [JSON Schema](/api/shared/schema) 101 | - SchemaField renders the overall Schema protocol, while RecursionField renders the partial Schema protocol 102 | 103 | ### Protocol binding 104 | 105 | I talked about model binding, and protocol binding is the process of converting Schema protocol into model binding, because JSON-Schema protocol is a JSON string and can be stored offline, while model binding is a binding between memory The relationship is at the Runtime layer. For example, `x-component` is the string identifier of the component in the Schema, but the component in the model requires component reference, so the JSON string and the Runtime layer need to be converted. Then we can continue to improve the above model binding diagram: 106 | 107 |  108 | 109 | To sum up, in @formily/react, there are mainly two layers of binding relationships, Schema binding model, model binding component, the glue layer that realizes the binding is @formily/react, it should be noted that Schema binds the field model After that, the Schema is not perceptible in the field model. For example, if you want to modify the `enum`, you need to modify the `dataSource` attribute in the field model. In short, if you want to update the field model, refer to [Field](//core.formilyjs. org/api/models/field), you can refer to [Schema](/api/shared/schema) document if you want to understand the mapping relationship between Schema and field model 110 | 111 | ## Three development models 112 | 113 | From the [architecture diagram](/guide/architecture), we have actually seen that the entire @formily/react has three development modes, corresponding to different users: 114 | 115 | - JSX development model 116 | - JSON Schema development mode 117 | - Markup Schema development mode 118 | 119 | We can look at specific examples 120 | 121 | #### JSX development model 122 | 123 | This mode mainly uses Field/ArrayField/ObjectField/VoidField components 124 | 125 | ```tsx 126 | import React from 'react' 127 | import { createForm } from '@formily/core' 128 | import { FormProvider, Field } from '@formily/react' 129 | import { Input } from 'antd' 130 | 131 | const form = createForm() 132 | 133 | export default () => ( 134 | <FormProvider form={form}> 135 | <Field name="input" component={[Input, { placeholder: 'Please enter' }]} /> 136 | </FormProvider> 137 | ) 138 | ``` 139 | 140 | #### JSON Schema Development Mode 141 | 142 | This mode is to pass JSON Schema to the schema attribute of SchemaField 143 | 144 | ```tsx 145 | import React from 'react' 146 | import { createForm } from '@formily/core' 147 | import { FormProvider, createSchemaField } from '@formily/react' 148 | import { Input } from 'antd' 149 | 150 | const form = createForm() 151 | 152 | const SchemaField = createSchemaField({ 153 | components: { 154 | Input, 155 | }, 156 | }) 157 | 158 | export default () => ( 159 | <FormProvider form={form}> 160 | <SchemaField 161 | schema={{ 162 | type: 'object', 163 | properties: { 164 | input: { 165 | type: 'string', 166 | 'x-component': 'Input', 167 | 'x-component-props': { 168 | placeholder: 'Please enter', 169 | }, 170 | }, 171 | }, 172 | }} 173 | /> 174 | </FormProvider> 175 | ) 176 | ``` 177 | 178 | #### Markup Schema Development Mode 179 | 180 | This mode can be regarded as a Schema development mode that is more friendly to source code development, and it also uses the SchemaField component. 181 | 182 | Because it is difficult to get the best smart prompt experience in the JSX environment with JSON Schema, and it is inconvenient to maintain, the maintainability in the form of tags will be better, and the smart prompt is also very strong. 183 | 184 | Markup Schema mode mainly has the following characteristics: 185 | 186 | - Mainly rely on description tags such as SchemaField.String/SchemaField.Array/SchemaField.Object... to express Schema 187 | - Each description tag represents a Schema node, which is equivalent to JSON-Schema 188 | - SchemaField child nodes cannot insert UI elements at will, because SchemaField will only parse all the Schema description tags of the child nodes, and then convert them into JSON Schema, and finally give it to [RecursionField](/api/components/recursion-field) for rendering, if you want Insert UI elements, you can upload the `x-content` attribute in VoidDield to insert UI elements 189 | 190 | ```tsx 191 | import React from 'react' 192 | import { createForm } from '@formily/core' 193 | import { FormProvider, createSchemaField } from '@formily/react' 194 | import { Input } from 'antd' 195 | 196 | const form = createForm() 197 | 198 | const SchemaField = createSchemaField({ 199 | components: { 200 | Input, 201 | }, 202 | }) 203 | 204 | export default () => ( 205 | <FormProvider form={form}> 206 | <SchemaField> 207 | <SchemaField.String 208 | x-component="Input" 209 | x-component-props={{ placeholder: 'Please enter' }} 210 | /> 211 | <div>I will not be rendered</div> 212 | <SchemaField.Void x-content={<div>I will be rendered</div>} /> 213 | </SchemaField> 214 | </FormProvider> 215 | ) 216 | ``` 217 | ```