This is page 29 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/grid/src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { define, observable, batch, reaction } from '@formily/reactive' 2 | import { ChildListMutationObserver } from './observer' 3 | import { ResizeObserver } from '@juggle/resize-observer' 4 | export interface IGridOptions { 5 | maxRows?: number 6 | maxColumns?: number | number[] 7 | minColumns?: number | number[] 8 | maxWidth?: number | number[] 9 | minWidth?: number | number[] 10 | breakpoints?: number[] 11 | columnGap?: number 12 | rowGap?: number 13 | colWrap?: boolean 14 | strictAutoFit?: boolean 15 | shouldVisible?: (node: GridNode, grid: Grid<HTMLElement>) => boolean 16 | onDigest?: (grid: Grid<HTMLElement>) => void 17 | onInitialized?: (grid: Grid<HTMLElement>) => void 18 | } 19 | 20 | const SpanRegExp = /span\s*(\d+)/ 21 | 22 | const isValid = (value: any) => value !== undefined && value !== null 23 | 24 | const calcBreakpointIndex = (breakpoints: number[], width: number) => { 25 | if (Array.isArray(breakpoints)) { 26 | for (let i = 0; i < breakpoints.length; i++) { 27 | if (width <= breakpoints[i]) { 28 | return i 29 | } 30 | } 31 | } 32 | return -1 33 | } 34 | 35 | const calcFactor = <T>(value: T | T[], breakpointIndex: number): T => { 36 | if (Array.isArray(value)) { 37 | if (breakpointIndex === -1) return value[0] 38 | return value[breakpointIndex] ?? value[value.length - 1] 39 | } else { 40 | return value 41 | } 42 | } 43 | 44 | const parseGridNode = (elements: HTMLCollection): GridNode[] => { 45 | return Array.from(elements).reduce((buf, element: HTMLElement, index) => { 46 | const style = getComputedStyle(element) 47 | const visible = !(style.display === 'none') 48 | const origin = element.getAttribute('data-grid-span') 49 | const span = parseSpan(style.gridColumnStart) ?? 1 50 | const originSpan = Number(origin ?? span) 51 | const node: GridNode = { 52 | index, 53 | span, 54 | visible, 55 | originSpan, 56 | element, 57 | } 58 | if (!origin) { 59 | element.setAttribute('data-grid-span', String(span)) 60 | } 61 | return buf.concat(node) 62 | }, []) 63 | } 64 | 65 | const calcChildTotalColumns = (nodes: GridNode[], shadow = false) => 66 | nodes.reduce((buf, node) => { 67 | if (!shadow) { 68 | if (!node.visible) return buf 69 | } 70 | if (node.originSpan === -1) return buf + (node.span ?? 1) 71 | return buf + node.span 72 | }, 0) 73 | 74 | const calcChildOriginTotalColumns = (nodes: GridNode[], shadow = false) => 75 | nodes.reduce((buf, node) => { 76 | if (!shadow) { 77 | if (!node.visible) return buf 78 | } 79 | if (node.originSpan === -1) return buf + (node.span ?? 1) 80 | return buf + node.originSpan 81 | }, 0) 82 | 83 | const calcSatisfyColumns = ( 84 | width: number, 85 | maxColumns: number, 86 | minColumns: number, 87 | maxWidth: number, 88 | minWidth: number, 89 | gap: number 90 | ) => { 91 | const results = [] 92 | for (let columns = minColumns; columns <= maxColumns; columns++) { 93 | const innerWidth = width - (columns - 1) * gap 94 | const columnWidth = innerWidth / columns 95 | if (columnWidth >= minWidth && columnWidth <= maxWidth) { 96 | results.push(columns) 97 | } else if (columnWidth < minWidth) { 98 | results.push(Math.min(Math.floor(innerWidth / minWidth), maxColumns)) 99 | } else if (columnWidth > maxWidth) { 100 | results.push(Math.min(Math.floor(innerWidth / maxWidth), maxColumns)) 101 | } 102 | } 103 | return Math.max(...results) 104 | } 105 | 106 | const parseSpan = (gridColumnStart: string) => { 107 | return Number(String(gridColumnStart).match(SpanRegExp)?.[1] ?? 1) 108 | } 109 | 110 | const factor = <T>(value: T | T[], grid: Grid<HTMLElement>): T => 111 | isValid(value) ? calcFactor(value as any, grid.breakpoint) : value 112 | 113 | const resolveChildren = (grid: Grid<HTMLElement>) => { 114 | let walked = 0, 115 | shadowWalked = 0, 116 | rowIndex = 0, 117 | shadowRowIndex = 0 118 | if (!grid.ready) return 119 | grid.children = grid.children.map((node) => { 120 | const columnIndex = walked % grid.columns 121 | const shadowColumnIndex = shadowWalked % grid.columns 122 | const remainColumns = grid.columns - columnIndex 123 | const originSpan = node.originSpan 124 | const targetSpan = originSpan > grid.columns ? grid.columns : originSpan 125 | const span = grid.options.strictAutoFit 126 | ? targetSpan 127 | : targetSpan > remainColumns 128 | ? remainColumns 129 | : targetSpan 130 | const gridColumn = 131 | originSpan === -1 ? `span ${remainColumns} / -1` : `span ${span} / auto` 132 | if (node.element.style.gridColumn !== gridColumn) { 133 | node.element.style.gridColumn = gridColumn 134 | } 135 | if (node.visible) { 136 | walked += span 137 | } 138 | shadowWalked += span 139 | if (columnIndex === 0) { 140 | rowIndex++ 141 | } 142 | if (shadowColumnIndex == 0) { 143 | shadowRowIndex++ 144 | } 145 | node.shadowRow = shadowRowIndex 146 | node.shadowColumn = shadowColumnIndex + 1 147 | if (node.visible) { 148 | node.row = rowIndex 149 | node.column = columnIndex + 1 150 | } 151 | if (grid.options?.shouldVisible) { 152 | if (!grid.options.shouldVisible(node, grid)) { 153 | if (node.visible) { 154 | node.element.style.display = 'none' 155 | } 156 | node.visible = false 157 | } else { 158 | if (!node.visible) { 159 | node.element.style.display = '' 160 | } 161 | node.visible = true 162 | } 163 | } 164 | return node 165 | }) 166 | } 167 | 168 | const nextTick = (callback?: () => void) => Promise.resolve(0).then(callback) 169 | 170 | export type GridNode = { 171 | index?: number 172 | visible?: boolean 173 | column?: number 174 | shadowColumn?: number 175 | row?: number 176 | shadowRow?: number 177 | span?: number 178 | originSpan?: number 179 | element?: HTMLElement 180 | } 181 | export class Grid<Container extends HTMLElement> { 182 | options: IGridOptions 183 | width = 0 184 | height = 0 185 | container: Container 186 | children: GridNode[] = [] 187 | childTotalColumns = 0 188 | shadowChildTotalColumns = 0 189 | childOriginTotalColumns = 0 190 | shadowChildOriginTotalColumns = 0 191 | ready = false 192 | constructor(options?: IGridOptions) { 193 | this.options = { 194 | breakpoints: [720, 1280, 1920], 195 | columnGap: 8, 196 | rowGap: 4, 197 | minWidth: 100, 198 | colWrap: true, 199 | strictAutoFit: false, 200 | ...options, 201 | } 202 | define(this, { 203 | options: observable.shallow, 204 | width: observable.ref, 205 | height: observable.ref, 206 | ready: observable.ref, 207 | children: observable.ref, 208 | childOriginTotalColumns: observable.ref, 209 | shadowChildOriginTotalColumns: observable.ref, 210 | shadowChildTotalColumns: observable.ref, 211 | childTotalColumns: observable.ref, 212 | columns: observable.computed, 213 | templateColumns: observable.computed, 214 | gap: observable.computed, 215 | maxColumns: observable.computed, 216 | minColumns: observable.computed, 217 | maxWidth: observable.computed, 218 | minWidth: observable.computed, 219 | breakpoints: observable.computed, 220 | breakpoint: observable.computed, 221 | rowGap: observable.computed, 222 | columnGap: observable.computed, 223 | colWrap: observable.computed, 224 | }) 225 | } 226 | 227 | set breakpoints(breakpoints) { 228 | this.options.breakpoints = breakpoints 229 | } 230 | 231 | get breakpoints() { 232 | return this.options.breakpoints 233 | } 234 | 235 | get breakpoint() { 236 | return calcBreakpointIndex(this.options.breakpoints, this.width) 237 | } 238 | 239 | set maxWidth(maxWidth) { 240 | this.options.maxWidth = maxWidth 241 | } 242 | 243 | get maxWidth() { 244 | return factor(this.options.maxWidth, this) ?? Infinity 245 | } 246 | 247 | set minWidth(minWidth) { 248 | this.options.minWidth = minWidth 249 | } 250 | 251 | get minWidth() { 252 | return factor(this.options.minWidth, this) ?? 100 253 | } 254 | 255 | set maxColumns(maxColumns) { 256 | this.options.maxColumns = maxColumns 257 | } 258 | 259 | get maxColumns() { 260 | return factor(this.options.maxColumns, this) ?? Infinity 261 | } 262 | 263 | set maxRows(maxRows) { 264 | this.options.maxRows = maxRows 265 | } 266 | 267 | get maxRows() { 268 | return this.options.maxRows ?? Infinity 269 | } 270 | 271 | set minColumns(minColumns) { 272 | this.options.minColumns = minColumns 273 | } 274 | 275 | get minColumns() { 276 | return factor(this.options.minColumns, this) ?? 1 277 | } 278 | 279 | set rowGap(rowGap) { 280 | this.options.rowGap = rowGap 281 | } 282 | 283 | get rowGap() { 284 | return factor(this.options.rowGap, this) ?? 5 285 | } 286 | 287 | set columnGap(columnGap) { 288 | this.options.columnGap = columnGap 289 | } 290 | 291 | get columnGap() { 292 | return factor(this.options.columnGap, this) ?? 10 293 | } 294 | 295 | set colWrap(colWrap) { 296 | this.options.colWrap = colWrap 297 | } 298 | 299 | get colWrap() { 300 | return factor(this.options.colWrap, this) ?? true 301 | } 302 | 303 | get columns() { 304 | if (!this.ready) return 0 305 | 306 | const originTotalColumns = this.childOriginTotalColumns 307 | 308 | if (this.colWrap === false) { 309 | return originTotalColumns 310 | } 311 | 312 | const baseColumns = this.childSize 313 | 314 | const strictMaxWidthColumns = Math.round( 315 | this.width / (this.maxWidth + this.columnGap) 316 | ) 317 | 318 | const looseMaxWidthColumns = Math.min( 319 | originTotalColumns, 320 | strictMaxWidthColumns 321 | ) 322 | 323 | const maxWidthColumns = this.options.strictAutoFit 324 | ? strictMaxWidthColumns 325 | : looseMaxWidthColumns 326 | 327 | const strictMinWidthColumns = Math.round( 328 | this.width / (this.minWidth + this.columnGap) 329 | ) 330 | 331 | const looseMinWidthColumns = Math.min( 332 | originTotalColumns, 333 | strictMinWidthColumns 334 | ) 335 | 336 | const minWidthColumns = this.options.strictAutoFit 337 | ? strictMinWidthColumns 338 | : looseMinWidthColumns 339 | 340 | const minCalculatedColumns = Math.min( 341 | baseColumns, 342 | originTotalColumns, 343 | maxWidthColumns, 344 | minWidthColumns 345 | ) 346 | 347 | const maxCalculatedColumns = Math.max( 348 | baseColumns, 349 | originTotalColumns, 350 | maxWidthColumns, 351 | minWidthColumns 352 | ) 353 | 354 | const finalColumns = calcSatisfyColumns( 355 | this.width, 356 | maxCalculatedColumns, 357 | minCalculatedColumns, 358 | this.maxWidth, 359 | this.minWidth, 360 | this.columnGap 361 | ) 362 | if (finalColumns >= this.maxColumns) { 363 | return this.maxColumns 364 | } 365 | if (finalColumns <= this.minColumns) { 366 | return this.minColumns 367 | } 368 | return finalColumns 369 | } 370 | 371 | get rows() { 372 | return Math.ceil(this.childTotalColumns / this.columns) 373 | } 374 | 375 | get shadowRows() { 376 | return Math.ceil(this.shadowChildTotalColumns / this.columns) 377 | } 378 | 379 | get templateColumns() { 380 | if (!this.width) return '' 381 | if (this.maxWidth === Infinity) { 382 | return `repeat(${this.columns},minmax(0,1fr))` 383 | } 384 | if (this.options.strictAutoFit !== true) { 385 | const columnWidth = 386 | (this.width - (this.columns - 1) * this.columnGap) / this.columns 387 | if (columnWidth < this.minWidth || columnWidth > this.maxWidth) { 388 | return `repeat(${this.columns},minmax(0,1fr))` 389 | } 390 | } 391 | return `repeat(${this.columns},minmax(${this.minWidth}px,${this.maxWidth}px))` 392 | } 393 | 394 | get gap() { 395 | return `${this.rowGap}px ${this.columnGap}px` 396 | } 397 | 398 | get childSize() { 399 | return this.children.length 400 | } 401 | 402 | get fullnessLastColumn() { 403 | return this.columns === this.children[this.childSize - 1]?.span 404 | } 405 | 406 | connect = (container: Container) => { 407 | if (container) { 408 | this.container = container 409 | const initialize = batch.bound(() => { 410 | digest() 411 | this.ready = true 412 | }) 413 | const digest = batch.bound(() => { 414 | this.children = parseGridNode(this.container.children) 415 | this.childTotalColumns = calcChildTotalColumns(this.children) 416 | this.shadowChildTotalColumns = calcChildTotalColumns( 417 | this.children, 418 | true 419 | ) 420 | this.childOriginTotalColumns = calcChildOriginTotalColumns( 421 | this.children 422 | ) 423 | this.shadowChildOriginTotalColumns = calcChildOriginTotalColumns( 424 | this.children, 425 | true 426 | ) 427 | const rect = this.container.getBoundingClientRect() 428 | if (rect.width && rect.height) { 429 | this.width = rect.width 430 | this.height = rect.height 431 | } 432 | resolveChildren(this) 433 | nextTick(() => { 434 | this.options?.onDigest?.(this) 435 | }) 436 | if (!this.ready) { 437 | nextTick(() => { 438 | this.options?.onInitialized?.(this) 439 | }) 440 | } 441 | }) 442 | const mutationObserver = new ChildListMutationObserver(digest) 443 | // add requestAnimationFrame to smooth digest 444 | const smoothDigest = () => { 445 | requestAnimationFrame(() => { 446 | digest() 447 | }) 448 | } 449 | const resizeObserver = new ResizeObserver(smoothDigest) 450 | const dispose = reaction(() => ({ ...this.options }), digest) 451 | resizeObserver.observe(this.container) 452 | mutationObserver.observe(this.container, { 453 | attributeFilter: ['data-grid-span'], 454 | attributes: true, 455 | }) 456 | initialize() 457 | return () => { 458 | resizeObserver.unobserve(this.container) 459 | resizeObserver.disconnect() 460 | mutationObserver.disconnect() 461 | dispose() 462 | this.children = [] 463 | } 464 | } 465 | 466 | return () => {} 467 | } 468 | 469 | static id = (options: IGridOptions = {}) => 470 | JSON.stringify( 471 | [ 472 | 'maxRows', 473 | 'maxColumns', 474 | 'minColumns', 475 | 'maxWidth', 476 | 'minWidth', 477 | 'breakpoints', 478 | 'columnGap', 479 | 'rowGap', 480 | 'colWrap', 481 | 'strictAutoFit', 482 | ].map((key) => options[key]) 483 | ) 484 | } 485 | ``` -------------------------------------------------------------------------------- /packages/antd/docs/components/ArrayCards.zh-CN.md: -------------------------------------------------------------------------------- ```markdown 1 | # ArrayCards 2 | 3 | > 卡片列表,对于每行字段数量较多,联动较多的场景比较适合使用 ArrayCards 4 | > 5 | > 注意:该组件只适用于 Schema 场景 6 | 7 | ## Markup Schema 案例 8 | 9 | ```tsx 10 | import React from 'react' 11 | import { 12 | FormItem, 13 | Input, 14 | ArrayCards, 15 | FormButtonGroup, 16 | Submit, 17 | } from '@formily/antd' 18 | import { createForm } from '@formily/core' 19 | import { FormProvider, createSchemaField } from '@formily/react' 20 | 21 | const SchemaField = createSchemaField({ 22 | components: { 23 | FormItem, 24 | Input, 25 | ArrayCards, 26 | }, 27 | }) 28 | 29 | const form = createForm() 30 | 31 | export default () => { 32 | return ( 33 | <FormProvider form={form}> 34 | <SchemaField> 35 | <SchemaField.Array 36 | name="string_array" 37 | maxItems={3} 38 | x-decorator="FormItem" 39 | x-component="ArrayCards" 40 | x-component-props={{ 41 | title: '字符串数组', 42 | }} 43 | > 44 | <SchemaField.Void> 45 | <SchemaField.Void x-component="ArrayCards.Index" /> 46 | <SchemaField.String 47 | name="input" 48 | x-decorator="FormItem" 49 | title="Input" 50 | required 51 | x-component="Input" 52 | /> 53 | <SchemaField.Void x-component="ArrayCards.Remove" /> 54 | <SchemaField.Void x-component="ArrayCards.Copy" /> 55 | <SchemaField.Void x-component="ArrayCards.MoveUp" /> 56 | <SchemaField.Void x-component="ArrayCards.MoveDown" /> 57 | </SchemaField.Void> 58 | <SchemaField.Void 59 | x-component="ArrayCards.Addition" 60 | title="添加条目" 61 | /> 62 | </SchemaField.Array> 63 | <SchemaField.Array 64 | name="array" 65 | maxItems={3} 66 | x-decorator="FormItem" 67 | x-component="ArrayCards" 68 | x-component-props={{ 69 | title: '对象数组', 70 | }} 71 | > 72 | <SchemaField.Object> 73 | <SchemaField.Void x-component="ArrayCards.Index" /> 74 | <SchemaField.String 75 | name="input" 76 | x-decorator="FormItem" 77 | title="Input" 78 | required 79 | x-component="Input" 80 | /> 81 | <SchemaField.Void x-component="ArrayCards.Remove" /> 82 | <SchemaField.Void x-component="ArrayCards.MoveUp" /> 83 | <SchemaField.Void x-component="ArrayCards.MoveDown" /> 84 | </SchemaField.Object> 85 | <SchemaField.Void 86 | x-component="ArrayCards.Addition" 87 | title="添加条目" 88 | /> 89 | </SchemaField.Array> 90 | </SchemaField> 91 | <FormButtonGroup> 92 | <Submit onSubmit={console.log}>提交</Submit> 93 | </FormButtonGroup> 94 | </FormProvider> 95 | ) 96 | } 97 | ``` 98 | 99 | ## JSON Schema 案例 100 | 101 | ```tsx 102 | import React from 'react' 103 | import { 104 | FormItem, 105 | Input, 106 | ArrayCards, 107 | FormButtonGroup, 108 | Submit, 109 | } from '@formily/antd' 110 | import { createForm } from '@formily/core' 111 | import { FormProvider, createSchemaField } from '@formily/react' 112 | 113 | const SchemaField = createSchemaField({ 114 | components: { 115 | FormItem, 116 | Input, 117 | ArrayCards, 118 | }, 119 | }) 120 | 121 | const form = createForm() 122 | 123 | const schema = { 124 | type: 'object', 125 | properties: { 126 | string_array: { 127 | type: 'array', 128 | 'x-component': 'ArrayCards', 129 | maxItems: 3, 130 | 'x-decorator': 'FormItem', 131 | 'x-component-props': { 132 | title: '字符串数组', 133 | }, 134 | items: { 135 | type: 'void', 136 | properties: { 137 | index: { 138 | type: 'void', 139 | 'x-component': 'ArrayCards.Index', 140 | }, 141 | input: { 142 | type: 'string', 143 | 'x-decorator': 'FormItem', 144 | title: 'Input', 145 | required: true, 146 | 'x-component': 'Input', 147 | }, 148 | remove: { 149 | type: 'void', 150 | 'x-component': 'ArrayCards.Remove', 151 | }, 152 | moveUp: { 153 | type: 'void', 154 | 'x-component': 'ArrayCards.MoveUp', 155 | }, 156 | moveDown: { 157 | type: 'void', 158 | 'x-component': 'ArrayCards.MoveDown', 159 | }, 160 | }, 161 | }, 162 | properties: { 163 | addition: { 164 | type: 'void', 165 | title: '添加条目', 166 | 'x-component': 'ArrayCards.Addition', 167 | }, 168 | }, 169 | }, 170 | array: { 171 | type: 'array', 172 | 'x-component': 'ArrayCards', 173 | maxItems: 3, 174 | 'x-decorator': 'FormItem', 175 | 'x-component-props': { 176 | title: '对象数组', 177 | }, 178 | items: { 179 | type: 'object', 180 | properties: { 181 | index: { 182 | type: 'void', 183 | 'x-component': 'ArrayCards.Index', 184 | }, 185 | input: { 186 | type: 'string', 187 | 'x-decorator': 'FormItem', 188 | title: 'Input', 189 | required: true, 190 | 'x-component': 'Input', 191 | }, 192 | remove: { 193 | type: 'void', 194 | 'x-component': 'ArrayCards.Remove', 195 | }, 196 | moveUp: { 197 | type: 'void', 198 | 'x-component': 'ArrayCards.MoveUp', 199 | }, 200 | moveDown: { 201 | type: 'void', 202 | 'x-component': 'ArrayCards.MoveDown', 203 | }, 204 | }, 205 | }, 206 | properties: { 207 | addition: { 208 | type: 'void', 209 | title: '添加条目', 210 | 'x-component': 'ArrayCards.Addition', 211 | }, 212 | }, 213 | }, 214 | }, 215 | } 216 | 217 | export default () => { 218 | return ( 219 | <FormProvider form={form}> 220 | <SchemaField schema={schema} /> 221 | <FormButtonGroup> 222 | <Submit onSubmit={console.log}>提交</Submit> 223 | </FormButtonGroup> 224 | </FormProvider> 225 | ) 226 | } 227 | ``` 228 | 229 | ## Effects 联动案例 230 | 231 | ```tsx 232 | import React from 'react' 233 | import { 234 | FormItem, 235 | Input, 236 | ArrayCards, 237 | FormButtonGroup, 238 | Submit, 239 | } from '@formily/antd' 240 | import { createForm, onFieldChange, onFieldReact } from '@formily/core' 241 | import { FormProvider, createSchemaField } from '@formily/react' 242 | 243 | const SchemaField = createSchemaField({ 244 | components: { 245 | FormItem, 246 | Input, 247 | ArrayCards, 248 | }, 249 | }) 250 | 251 | const form = createForm({ 252 | effects: () => { 253 | //主动联动模式 254 | onFieldChange('array.*.aa', ['value'], (field, form) => { 255 | form.setFieldState(field.query('.bb'), (state) => { 256 | state.visible = field.value != '123' 257 | }) 258 | }) 259 | //被动联动模式 260 | onFieldReact('array.*.dd', (field) => { 261 | field.visible = field.query('.cc').get('value') != '123' 262 | }) 263 | }, 264 | }) 265 | 266 | export default () => { 267 | return ( 268 | <FormProvider form={form}> 269 | <SchemaField> 270 | <SchemaField.Array 271 | name="array" 272 | maxItems={3} 273 | x-component="ArrayCards" 274 | x-decorator="FormItem" 275 | x-component-props={{ 276 | title: '对象数组', 277 | }} 278 | > 279 | <SchemaField.Object> 280 | <SchemaField.Void x-component="ArrayCards.Index" /> 281 | <SchemaField.String 282 | name="aa" 283 | x-decorator="FormItem" 284 | title="AA" 285 | required 286 | description="AA输入123时隐藏BB" 287 | x-component="Input" 288 | /> 289 | <SchemaField.String 290 | name="bb" 291 | x-decorator="FormItem" 292 | title="BB" 293 | required 294 | x-component="Input" 295 | /> 296 | <SchemaField.String 297 | name="cc" 298 | x-decorator="FormItem" 299 | title="CC" 300 | required 301 | description="CC输入123时隐藏DD" 302 | x-component="Input" 303 | /> 304 | <SchemaField.String 305 | name="dd" 306 | x-decorator="FormItem" 307 | title="DD" 308 | required 309 | x-component="Input" 310 | /> 311 | <SchemaField.Void x-component="ArrayCards.Remove" /> 312 | <SchemaField.Void x-component="ArrayCards.MoveUp" /> 313 | <SchemaField.Void x-component="ArrayCards.MoveDown" /> 314 | </SchemaField.Object> 315 | <SchemaField.Void 316 | x-component="ArrayCards.Addition" 317 | title="添加条目" 318 | /> 319 | </SchemaField.Array> 320 | </SchemaField> 321 | <FormButtonGroup> 322 | <Submit onSubmit={console.log}>提交</Submit> 323 | </FormButtonGroup> 324 | </FormProvider> 325 | ) 326 | } 327 | ``` 328 | 329 | ## JSON Schema 联动案例 330 | 331 | ```tsx 332 | import React from 'react' 333 | import { 334 | FormItem, 335 | Input, 336 | ArrayCards, 337 | FormButtonGroup, 338 | Submit, 339 | } from '@formily/antd' 340 | import { createForm } from '@formily/core' 341 | import { FormProvider, createSchemaField } from '@formily/react' 342 | 343 | const SchemaField = createSchemaField({ 344 | components: { 345 | FormItem, 346 | Input, 347 | ArrayCards, 348 | }, 349 | }) 350 | 351 | const form = createForm() 352 | 353 | const schema = { 354 | type: 'object', 355 | properties: { 356 | array: { 357 | type: 'array', 358 | 'x-component': 'ArrayCards', 359 | maxItems: 3, 360 | title: '对象数组', 361 | items: { 362 | type: 'object', 363 | properties: { 364 | index: { 365 | type: 'void', 366 | 'x-component': 'ArrayCards.Index', 367 | }, 368 | aa: { 369 | type: 'string', 370 | 'x-decorator': 'FormItem', 371 | title: 'AA', 372 | required: true, 373 | 'x-component': 'Input', 374 | description: '输入123', 375 | }, 376 | bb: { 377 | type: 'string', 378 | title: 'BB', 379 | required: true, 380 | 'x-decorator': 'FormItem', 381 | 'x-component': 'Input', 382 | 'x-reactions': [ 383 | { 384 | dependencies: ['.aa'], 385 | when: "{{$deps[0] != '123'}}", 386 | fulfill: { 387 | schema: { 388 | title: 'BB', 389 | 'x-disabled': true, 390 | }, 391 | }, 392 | otherwise: { 393 | schema: { 394 | title: 'Changed', 395 | 'x-disabled': false, 396 | }, 397 | }, 398 | }, 399 | ], 400 | }, 401 | remove: { 402 | type: 'void', 403 | 'x-component': 'ArrayCards.Remove', 404 | }, 405 | moveUp: { 406 | type: 'void', 407 | 'x-component': 'ArrayCards.MoveUp', 408 | }, 409 | moveDown: { 410 | type: 'void', 411 | 'x-component': 'ArrayCards.MoveDown', 412 | }, 413 | }, 414 | }, 415 | properties: { 416 | addition: { 417 | type: 'void', 418 | title: '添加条目', 419 | 'x-component': 'ArrayCards.Addition', 420 | }, 421 | }, 422 | }, 423 | }, 424 | } 425 | 426 | export default () => { 427 | return ( 428 | <FormProvider form={form}> 429 | <SchemaField schema={schema} /> 430 | <FormButtonGroup> 431 | <Submit onSubmit={console.log}>提交</Submit> 432 | </FormButtonGroup> 433 | </FormProvider> 434 | ) 435 | } 436 | ``` 437 | 438 | ## API 439 | 440 | ### ArrayCards 441 | 442 | 扩展属性 443 | 444 | | 属性名 | 类型 | 描述 | 默认值 | 445 | | ---------- | ------------------------- | ------------ | ------ | 446 | | onAdd | `(index: number) => void` | 增加方法 | | 447 | | onRemove | `(index: number) => void` | 删除方法 | | 448 | | onCopy | `(index: number) => void` | 复制方法 | | 449 | | onMoveUp | `(index: number) => void` | 向上移动方法 | | 450 | | onMoveDown | `(index: number) => void` | 向下移动方法 | | 451 | 452 | 其余参考 https://ant.design/components/card-cn/ 453 | 454 | ### ArrayCards.Addition 455 | 456 | > 添加按钮 457 | 458 | 扩展属性 459 | 460 | | 属性名 | 类型 | 描述 | 默认值 | 461 | | ------------ | --------------------- | -------- | -------- | 462 | | title | ReactText | 文案 | | 463 | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | 464 | | defaultValue | `any` | 默认值 | | 465 | 466 | 其余参考 https://ant.design/components/button-cn/ 467 | 468 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 469 | 470 | 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 471 | 472 | ### ArrayCards.Copy 473 | 474 | > 复制按钮 475 | 476 | 扩展属性 477 | 478 | | 属性名 | 类型 | 描述 | 默认值 | 479 | | ------ | --------------------- | -------- | -------- | 480 | | title | ReactText | 文案 | | 481 | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | 482 | 483 | 其余参考 https://ant.design/components/button-cn/ 484 | 485 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 486 | 487 | 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 488 | 489 | ### ArrayCards.Remove 490 | 491 | > 删除按钮 492 | 493 | | 属性名 | 类型 | 描述 | 默认值 | 494 | | ------ | --------- | ---- | ------ | 495 | | title | ReactText | 文案 | | 496 | 497 | 其余参考 https://ant.design/components/icon-cn/ 498 | 499 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 500 | 501 | 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 502 | 503 | ### ArrayCards.MoveDown 504 | 505 | > 下移按钮 506 | 507 | | 属性名 | 类型 | 描述 | 默认值 | 508 | | ------ | --------- | ---- | ------ | 509 | | title | ReactText | 文案 | | 510 | 511 | 其余参考 https://ant.design/components/icon-cn/ 512 | 513 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 514 | 515 | 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 516 | 517 | ### ArrayCards.MoveUp 518 | 519 | > 上移按钮 520 | 521 | | 属性名 | 类型 | 描述 | 默认值 | 522 | | ------ | --------- | ---- | ------ | 523 | | title | ReactText | 文案 | | 524 | 525 | 其余参考 https://ant.design/components/icon-cn/ 526 | 527 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 528 | 529 | 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 530 | 531 | ### ArrayCards.Index 532 | 533 | > 索引渲染器 534 | 535 | 无属性 536 | 537 | ### ArrayCards.useIndex 538 | 539 | > 读取当前渲染行索引的 React Hook 540 | 541 | ### ArrayCards.useRecord 542 | 543 | > 读取当前渲染记录的 React Hook 544 | ``` -------------------------------------------------------------------------------- /packages/core/src/__tests__/effects.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | createForm, 3 | createEffectContext, 4 | onFieldChange, 5 | onFieldInit, 6 | onFieldInitialValueChange, 7 | onFieldInputValueChange, 8 | onFieldMount, 9 | onFieldReact, 10 | onFieldUnmount, 11 | onFieldValidateEnd, 12 | onFieldValidateStart, 13 | onFieldValidateFailed, 14 | onFieldValidateSuccess, 15 | onFieldValueChange, 16 | onFormInit, 17 | onFormInitialValuesChange, 18 | onFormInputChange, 19 | onFormMount, 20 | onFormReact, 21 | onFormReset, 22 | onFormSubmit, 23 | onFormSubmitEnd, 24 | onFormSubmitFailed, 25 | onFormSubmitStart, 26 | onFormSubmitSuccess, 27 | onFormSubmitValidateFailed, 28 | onFormSubmitValidateStart, 29 | onFormSubmitValidateSuccess, 30 | onFormSubmitValidateEnd, 31 | onFormUnmount, 32 | onFormValidateEnd, 33 | onFormValidateStart, 34 | onFormValidateFailed, 35 | onFormValidateSuccess, 36 | onFormValuesChange, 37 | isVoidField, 38 | } from '../' 39 | import { runEffects } from '../shared/effective' 40 | import { attach, sleep } from './shared' 41 | 42 | test('onFormInit/onFormMount/onFormUnmount', () => { 43 | const mount = jest.fn() 44 | const init = jest.fn() 45 | const unmount = jest.fn() 46 | const form = attach( 47 | createForm({ 48 | effects() { 49 | onFormInit(init) 50 | onFormMount(mount) 51 | onFormUnmount(unmount) 52 | }, 53 | }) 54 | ) 55 | expect(init).toBeCalled() 56 | expect(mount).toBeCalled() 57 | expect(unmount).not.toBeCalled() 58 | form.onUnmount() 59 | expect(unmount).toBeCalled() 60 | }) 61 | 62 | test('onFormValuesChange/onFormInitialValuesChange', () => { 63 | const valuesChange = jest.fn() 64 | const initialValuesChange = jest.fn() 65 | const form = attach( 66 | createForm({ 67 | effects() { 68 | onFormValuesChange(valuesChange) 69 | onFormInitialValuesChange(initialValuesChange) 70 | }, 71 | }) 72 | ) 73 | expect(valuesChange).not.toBeCalled() 74 | expect(initialValuesChange).not.toBeCalled() 75 | form.setValues({ 76 | aa: '123', 77 | }) 78 | expect(form.values.aa).toEqual('123') 79 | expect(valuesChange).toBeCalled() 80 | form.setInitialValues({ 81 | aa: '321', 82 | bb: '123', 83 | }) 84 | expect(form.values.aa).toEqual('321') 85 | expect(form.values.bb).toEqual('123') 86 | expect(initialValuesChange).toBeCalled() 87 | }) 88 | 89 | test('onFormInputChange', () => { 90 | const inputChange = jest.fn() 91 | const valuesChange = jest.fn() 92 | const form = attach( 93 | createForm({ 94 | effects() { 95 | onFormValuesChange(valuesChange) 96 | onFormInputChange(inputChange) 97 | }, 98 | }) 99 | ) 100 | const field = attach( 101 | form.createField({ 102 | name: 'aa', 103 | }) 104 | ) 105 | expect(inputChange).not.toBeCalled() 106 | expect(valuesChange).not.toBeCalled() 107 | field.setValue('123') 108 | expect(inputChange).not.toBeCalled() 109 | expect(valuesChange).toBeCalledTimes(1) 110 | field.onInput('123') 111 | expect(inputChange).toBeCalled() 112 | expect(valuesChange).toBeCalledTimes(1) 113 | field.onInput('321') 114 | expect(inputChange).toBeCalledTimes(2) 115 | expect(valuesChange).toBeCalledTimes(2) 116 | }) 117 | 118 | test('onFormReact', () => { 119 | const react = jest.fn() 120 | const form = attach( 121 | createForm({ 122 | effects() { 123 | onFormReact((form) => { 124 | if (form.values.aa) { 125 | react() 126 | } 127 | }) 128 | }, 129 | }) 130 | ) 131 | expect(react).not.toBeCalled() 132 | form.setValues({ aa: 123 }) 133 | expect(react).toBeCalled() 134 | form.onUnmount() 135 | 136 | // will not throw error 137 | const form2 = attach( 138 | createForm({ 139 | effects() { 140 | onFormReact() 141 | }, 142 | }) 143 | ) 144 | 145 | form2.onUnmount() 146 | }) 147 | 148 | test('onFormReset', async () => { 149 | const reset = jest.fn() 150 | const form = attach( 151 | createForm({ 152 | initialValues: { 153 | aa: 123, 154 | }, 155 | effects() { 156 | onFormReset(reset) 157 | }, 158 | }) 159 | ) 160 | 161 | const field = attach( 162 | form.createField({ 163 | name: 'aa', 164 | }) 165 | ) 166 | 167 | field.setValue('xxxx') 168 | 169 | expect(field.value).toEqual('xxxx') 170 | expect(form.values.aa).toEqual('xxxx') 171 | expect(reset).not.toBeCalled() 172 | await form.reset() 173 | expect(field.value).toEqual(123) 174 | expect(form.values.aa).toEqual(123) 175 | expect(reset).toBeCalled() 176 | }) 177 | 178 | test('onFormSubmit', async () => { 179 | const submit = jest.fn() 180 | const submitStart = jest.fn() 181 | const submitEnd = jest.fn() 182 | const submitSuccess = jest.fn() 183 | const submitFailed = jest.fn() 184 | const submitValidateStart = jest.fn() 185 | const submitValidateFailed = jest.fn() 186 | const submitValidateSuccess = jest.fn() 187 | const submitValidateEnd = jest.fn() 188 | const form = attach( 189 | createForm({ 190 | effects() { 191 | onFormSubmitStart(submitStart) 192 | onFormSubmit(submit) 193 | onFormSubmitEnd(submitEnd) 194 | onFormSubmitFailed(submitFailed) 195 | onFormSubmitSuccess(submitSuccess) 196 | onFormSubmitValidateStart(submitValidateStart) 197 | onFormSubmitValidateFailed(submitValidateFailed) 198 | onFormSubmitValidateSuccess(submitValidateSuccess) 199 | onFormSubmitValidateEnd(submitValidateEnd) 200 | }, 201 | }) 202 | ) 203 | 204 | const field = attach( 205 | form.createField({ 206 | name: 'aa', 207 | required: true, 208 | }) 209 | ) 210 | try { 211 | await form.submit() 212 | } catch {} 213 | expect(submitStart).toBeCalled() 214 | expect(submit).toBeCalled() 215 | expect(submitEnd).toBeCalled() 216 | expect(submitSuccess).not.toBeCalled() 217 | expect(submitFailed).toBeCalled() 218 | expect(submitValidateStart).toBeCalled() 219 | expect(submitValidateFailed).toBeCalled() 220 | expect(submitValidateSuccess).not.toBeCalled() 221 | expect(submitValidateEnd).toBeCalled() 222 | field.onInput('123') 223 | try { 224 | await form.submit() 225 | } catch (e) {} 226 | expect(submitStart).toBeCalledTimes(2) 227 | expect(submit).toBeCalledTimes(2) 228 | expect(submitEnd).toBeCalledTimes(2) 229 | expect(submitSuccess).toBeCalledTimes(1) 230 | expect(submitFailed).toBeCalledTimes(1) 231 | expect(submitValidateStart).toBeCalledTimes(2) 232 | expect(submitValidateFailed).toBeCalledTimes(1) 233 | expect(submitValidateSuccess).toBeCalledTimes(1) 234 | expect(submitValidateEnd).toBeCalledTimes(2) 235 | }) 236 | 237 | test('onFormValidate', async () => { 238 | const validateStart = jest.fn() 239 | const validateEnd = jest.fn() 240 | const validateFailed = jest.fn() 241 | const validateSuccess = jest.fn() 242 | const form = attach( 243 | createForm({ 244 | effects() { 245 | onFormValidateStart(validateStart) 246 | onFormValidateEnd(validateEnd) 247 | onFormValidateFailed(validateFailed) 248 | onFormValidateSuccess(validateSuccess) 249 | }, 250 | }) 251 | ) 252 | const field = attach( 253 | form.createField({ 254 | name: 'aa', 255 | required: true, 256 | }) 257 | ) 258 | try { 259 | await form.validate() 260 | } catch {} 261 | expect(validateStart).toBeCalled() 262 | expect(validateEnd).toBeCalled() 263 | expect(validateFailed).toBeCalled() 264 | expect(validateSuccess).not.toBeCalled() 265 | field.onInput('123') 266 | try { 267 | await form.validate() 268 | } catch {} 269 | expect(validateStart).toBeCalledTimes(2) 270 | expect(validateEnd).toBeCalledTimes(2) 271 | expect(validateFailed).toBeCalledTimes(1) 272 | expect(validateSuccess).toBeCalledTimes(1) 273 | }) 274 | 275 | test('onFieldChange', async () => { 276 | const fieldChange = jest.fn() 277 | const valueChange = jest.fn() 278 | const valueChange2 = jest.fn() 279 | const form = attach( 280 | createForm({ 281 | effects() { 282 | onFieldChange( 283 | 'aa', 284 | [ 285 | 'value', 286 | 'disabled', 287 | 'initialized', 288 | 'inputValue', 289 | 'loading', 290 | 'visible', 291 | 'editable', 292 | ], 293 | fieldChange 294 | ) 295 | onFieldChange('aa', valueChange) 296 | onFieldChange('aa', undefined, valueChange2) 297 | onFieldChange('aa') 298 | }, 299 | }) 300 | ) 301 | const field = attach( 302 | form.createField({ 303 | name: 'aa', 304 | }) 305 | ) 306 | expect(fieldChange).toBeCalledTimes(1) 307 | field.setValue('123') 308 | expect(fieldChange).toBeCalledTimes(2) 309 | field.onInput('321') 310 | expect(fieldChange).toBeCalledTimes(3) 311 | field.setLoading(true) 312 | expect(fieldChange).toBeCalledTimes(3) 313 | await sleep() 314 | expect(fieldChange).toBeCalledTimes(4) 315 | field.setPattern('disabled') 316 | expect(fieldChange).toBeCalledTimes(5) 317 | field.setDisplay('none') 318 | expect(fieldChange).toBeCalledTimes(6) 319 | form.onUnmount() 320 | expect(valueChange).toBeCalledTimes(4) 321 | expect(valueChange2).toBeCalledTimes(4) 322 | }) 323 | 324 | test('onFieldInit/onFieldMount/onFieldUnmount', () => { 325 | const fieldInit = jest.fn() 326 | const fieldMount = jest.fn() 327 | const fieldUnmount = jest.fn() 328 | const form = attach( 329 | createForm({ 330 | effects() { 331 | onFieldInit('aa', fieldInit) 332 | onFieldMount('aa', fieldMount) 333 | onFieldUnmount('aa', fieldUnmount) 334 | }, 335 | }) 336 | ) 337 | const field = attach( 338 | form.createField({ 339 | name: 'aa', 340 | }) 341 | ) 342 | expect(fieldInit).toBeCalledTimes(1) 343 | expect(fieldMount).toBeCalledTimes(1) 344 | expect(fieldUnmount).toBeCalledTimes(0) 345 | field.onUnmount() 346 | expect(fieldUnmount).toBeCalledTimes(1) 347 | }) 348 | 349 | test('onFieldInitialValueChange/onFieldValueChange/onFieldInputValueChange', () => { 350 | const fieldValueChange = jest.fn() 351 | const fieldInitialValueChange = jest.fn() 352 | const fieldInputValueChange = jest.fn() 353 | const notTrigger = jest.fn() 354 | const form = attach( 355 | createForm({ 356 | effects() { 357 | onFieldInitialValueChange('aa', fieldInitialValueChange) 358 | onFieldValueChange('aa', fieldValueChange) 359 | onFieldInputValueChange('aa', fieldInputValueChange) 360 | onFieldValueChange('xx', notTrigger) 361 | }, 362 | }) 363 | ) 364 | const field = attach( 365 | form.createField({ 366 | name: 'aa', 367 | }) 368 | ) 369 | field.setValue('123') 370 | expect(fieldValueChange).toBeCalledTimes(1) 371 | expect(fieldInitialValueChange).toBeCalledTimes(0) 372 | expect(fieldInputValueChange).toBeCalledTimes(0) 373 | field.setInitialValue('xxx') 374 | expect(fieldValueChange).toBeCalledTimes(2) 375 | expect(fieldInitialValueChange).toBeCalledTimes(1) 376 | expect(fieldInputValueChange).toBeCalledTimes(0) 377 | field.onInput('321') 378 | expect(fieldValueChange).toBeCalledTimes(3) 379 | expect(fieldInitialValueChange).toBeCalledTimes(1) 380 | expect(fieldInputValueChange).toBeCalledTimes(1) 381 | expect(notTrigger).toBeCalledTimes(0) 382 | }) 383 | 384 | test('onFieldReact', () => { 385 | const react = jest.fn() 386 | const form = attach( 387 | createForm({ 388 | effects() { 389 | onFieldReact('aa', (field) => { 390 | if (isVoidField(field)) return 391 | if (field.value) { 392 | react() 393 | } 394 | if (field.display === 'hidden') { 395 | react() 396 | } 397 | }) 398 | onFieldReact('aa', null) 399 | }, 400 | }) 401 | ) 402 | const field = attach( 403 | form.createField({ 404 | name: 'aa', 405 | }) 406 | ) 407 | expect(react).not.toBeCalled() 408 | form.setValues({ aa: 123 }) 409 | expect(react).toBeCalledTimes(1) 410 | field.setDisplay('hidden') 411 | expect(react).toBeCalledTimes(3) 412 | form.onUnmount() 413 | }) 414 | 415 | test('onFieldValidate', async () => { 416 | const validateStart = jest.fn() 417 | const validateFailed = jest.fn() 418 | const validateSuccess = jest.fn() 419 | const validateEnd = jest.fn() 420 | const form = attach( 421 | createForm({ 422 | effects() { 423 | onFieldValidateStart('aa', validateStart) 424 | onFieldValidateEnd('aa', validateEnd) 425 | onFieldValidateFailed('aa', validateFailed) 426 | onFieldValidateSuccess('aa', validateSuccess) 427 | }, 428 | }) 429 | ) 430 | const field = attach( 431 | form.createField({ 432 | name: 'aa', 433 | required: true, 434 | }) 435 | ) 436 | try { 437 | await field.validate() 438 | } catch {} 439 | expect(validateStart).toBeCalled() 440 | expect(validateFailed).toBeCalled() 441 | expect(validateSuccess).not.toBeCalled() 442 | expect(validateEnd).toBeCalled() 443 | field.setValue('123') 444 | try { 445 | await field.validate() 446 | } catch {} 447 | expect(validateStart).toBeCalledTimes(2) 448 | expect(validateFailed).toBeCalledTimes(1) 449 | expect(validateSuccess).toBeCalledTimes(1) 450 | expect(validateEnd).toBeCalledTimes(2) 451 | }) 452 | 453 | test('async use will throw error', async () => { 454 | const valueChange = jest.fn() 455 | let error 456 | const form = attach( 457 | createForm({ 458 | effects() { 459 | setTimeout(() => { 460 | try { 461 | onFieldValueChange('aa', valueChange) 462 | } catch (e) { 463 | error = e 464 | } 465 | }, 0) 466 | }, 467 | }) 468 | ) 469 | const aa = attach( 470 | form.createField({ 471 | name: 'aa', 472 | }) 473 | ) 474 | await sleep(10) 475 | aa.setValue('123') 476 | expect(valueChange).toBeCalledTimes(0) 477 | expect(error).not.toBeUndefined() 478 | }) 479 | 480 | test('effect context', async () => { 481 | const context = createEffectContext<number>() 482 | const context2 = createEffectContext<number>() 483 | const context3 = createEffectContext<number>(123) 484 | let results: any 485 | let error: any 486 | let error2: any 487 | const consumer = () => { 488 | results = context.consume() 489 | } 490 | const consumer2 = () => { 491 | setTimeout(() => { 492 | try { 493 | results = context2.consume() 494 | } catch (e) { 495 | error2 = e 496 | } 497 | }, 0) 498 | } 499 | attach( 500 | createForm({ 501 | effects() { 502 | context.provide(123) 503 | context3.provide() 504 | consumer() 505 | setTimeout(() => { 506 | try { 507 | context2.provide(123) 508 | } catch (e) { 509 | error = e 510 | } 511 | }, 0) 512 | consumer2() 513 | }, 514 | }) 515 | ) 516 | await sleep(10) 517 | expect(results).toEqual(123) 518 | expect(error).not.toBeUndefined() 519 | expect(error2).not.toBeUndefined() 520 | }) 521 | 522 | test('runEffects', () => { 523 | expect( 524 | runEffects(123, () => { 525 | onFormMount(() => {}) 526 | }).length 527 | ).toEqual(1) 528 | }) 529 | ``` -------------------------------------------------------------------------------- /packages/path/src/__tests__/accessor.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Path } from '../' 2 | 3 | const { getIn, setIn } = Path 4 | 5 | test('test getIn null parent', () => { 6 | const value = { aa: null } 7 | expect(getIn(value, 'aa')).toEqual(null) 8 | expect(getIn(value, 'aa.bb.cc')).toEqual(undefined) 9 | }) 10 | 11 | test('test getIn and setIn', () => { 12 | const value = { a: { b: { c: 2, d: 333 } } } 13 | expect(getIn(value, 'a.b.c')).toEqual(2) 14 | setIn(value, 'a.b.c', 1111) 15 | expect(getIn(value, 'a.b.c')).toEqual(1111) 16 | }) 17 | 18 | test('test getIn with destructor', () => { 19 | const value = { array: [{ aa: 123, bb: 321 }] } 20 | expect(getIn(value, 'array.0.[aa,bb]')).toEqual([123, 321]) 21 | }) 22 | 23 | test('test setIn auto create array', () => { 24 | const value = { array: null } 25 | setIn(value, 'array[0].bb[2]', 'hello world') 26 | expect(value).toEqual({ 27 | array: [ 28 | { 29 | bb: [undefined, undefined, 'hello world'], 30 | }, 31 | ], 32 | }) 33 | expect(getIn(undefined, 'aa.bb.cc')).toEqual(undefined) 34 | setIn(undefined, 'aa.bb.cc', 123) 35 | }) 36 | 37 | test('map', () => { 38 | const value = { map: new Map() } 39 | setIn(value, 'map.aa.bb.cc', 123) 40 | expect(getIn(value, 'map.aa.bb.cc')).toEqual(123) 41 | }) 42 | 43 | test('test setIn array properties', () => { 44 | const value = { array: [] } 45 | setIn(value, 'array.xxx', 'hello world') 46 | expect(value).toEqual({ array: [] }) 47 | }) 48 | 49 | test('test setIn dose not affect other items', () => { 50 | const value = { 51 | aa: [ 52 | { 53 | dd: [ 54 | { 55 | ee: '是', 56 | }, 57 | ], 58 | cc: '1111', 59 | }, 60 | ], 61 | } 62 | 63 | setIn(value, 'aa.1.dd.0.ee', '否') 64 | expect(value.aa[0]).toEqual({ 65 | dd: [ 66 | { 67 | ee: '是', 68 | }, 69 | ], 70 | cc: '1111', 71 | }) 72 | }) 73 | 74 | test('destruct getIn', () => { 75 | // getIn 通过解构表达式从扁平数据转为复合嵌套数据 76 | const value = { a: { b: { c: 2, d: 333 } } } 77 | expect(getIn({ a: { b: { kk: 2, mm: 333 } } }, 'a.b.{c:kk,d:mm}')).toEqual({ 78 | c: 2, 79 | d: 333, 80 | }) 81 | 82 | expect( 83 | getIn( 84 | { kk: 2, mm: 333 }, 85 | `{ 86 | a : { 87 | b : { 88 | c : kk, 89 | d : mm 90 | } 91 | } 92 | }` 93 | ) 94 | ).toEqual(value) 95 | expect(getIn({ bb: undefined, dd: undefined }, `[{aa:bb,cc:dd}]`)).toEqual([]) 96 | expect( 97 | getIn( 98 | { kk: undefined, mm: undefined }, 99 | `{ 100 | a : { 101 | b : { 102 | c : kk, 103 | d : mm 104 | } 105 | } 106 | }` 107 | ) 108 | ).toEqual({}) 109 | }) 110 | 111 | test('destruct setIn', () => { 112 | const value = { a: { b: { c: 2, d: 333 } } } 113 | // setIn 从复杂嵌套结构中解构数据出来对其做赋值处理 114 | expect( 115 | setIn( 116 | {}, 117 | `{ 118 | a : { 119 | b : { 120 | c, 121 | d 122 | } 123 | } 124 | }`, 125 | value 126 | ) 127 | ).toEqual({ c: 2, d: 333 }) 128 | 129 | expect( 130 | setIn( 131 | {}, 132 | ` 133 | [aa,bb] 134 | `, 135 | [123, 444] 136 | ) 137 | ).toEqual({ aa: 123, bb: 444 }) 138 | expect(setIn({}, 'aa.bb.ddd.[aa,bb]', [123, 444])).toEqual({ 139 | aa: { bb: { ddd: { aa: 123, bb: 444 } } }, 140 | }) 141 | 142 | expect(setIn({}, 'aa.bb.ddd.[{cc:aa,bb}]', [{ cc: 123, bb: 444 }])).toEqual({ 143 | aa: { bb: { ddd: { aa: 123, bb: 444 } } }, 144 | }) 145 | }) 146 | 147 | test('setIn with a.b.c.{aaa,bbb}', () => { 148 | expect(Path.setIn({}, 'a.b.c.{aaa,bbb}', { aaa: 123, bbb: 321 })).toEqual({ 149 | a: { b: { c: { aaa: 123, bbb: 321 } } }, 150 | }) 151 | }) 152 | 153 | test('getIn with a.b.c.{aaa,bbb}', () => { 154 | expect( 155 | Path.getIn({ a: { b: { c: { aaa: 123, bbb: 321 } } } }, 'a.b.c.{aaa,bbb}') 156 | ).toEqual({ aaa: 123, bbb: 321 }) 157 | }) 158 | 159 | test('setIn with a.b.c.{aaa,bbb} source has extra property', () => { 160 | expect( 161 | Path.setIn({ a: { b: { c: { kkk: 'ddd' } } } }, 'a.b.c.{aaa,bbb}', { 162 | aaa: 123, 163 | bbb: 321, 164 | }) 165 | ).toEqual({ a: { b: { c: { aaa: 123, bbb: 321, kkk: 'ddd' } } } }) 166 | }) 167 | 168 | test('getIn with a.b.c.{aaa,bbb} source has extra property', () => { 169 | expect( 170 | Path.getIn( 171 | { a: { b: { c: { aaa: 123, bbb: 321, kkk: 'ddd' } } } }, 172 | 'a.b.c.{aaa,bbb}' 173 | ) 174 | ).toEqual({ aaa: 123, bbb: 321 }) 175 | }) 176 | 177 | test('setIn with a.b.c.{aaa:ooo,bbb}', () => { 178 | expect( 179 | Path.setIn({ a: { b: { c: { kkk: 'ddd' } } } }, 'a.b.c.{aaa:ooo,bbb}', { 180 | aaa: 123, 181 | bbb: 321, 182 | }) 183 | ).toEqual({ a: { b: { c: { ooo: 123, bbb: 321, kkk: 'ddd' } } } }) 184 | }) 185 | 186 | test('getIn with a.b.c.{aaa:ooo,bbb}', () => { 187 | expect( 188 | Path.getIn( 189 | { a: { b: { c: { ooo: 123, bbb: 321, kkk: 'ddd' } } } }, 190 | 'a.b.c.{aaa:ooo,bbb}' 191 | ) 192 | ).toEqual({ aaa: 123, bbb: 321 }) 193 | }) 194 | 195 | test('setIn with a.b.c.[aaa,bbb]', () => { 196 | expect(Path.setIn({}, 'a.b.c.[aaa,bbb]', [123, 321])).toEqual({ 197 | a: { b: { c: { aaa: 123, bbb: 321 } } }, 198 | }) 199 | }) 200 | 201 | test('getIn with a.b.c.[aaa,bbb]', () => { 202 | expect( 203 | Path.getIn({ a: { b: { c: { aaa: 123, bbb: 321 } } } }, 'a.b.c.[aaa,bbb]') 204 | ).toEqual([123, 321]) 205 | }) 206 | 207 | test('setIn with a.b.c.[aaa,bbb] source has extra property', () => { 208 | expect( 209 | Path.setIn( 210 | { a: { b: { c: { kkk: 'ddd' } } } }, 211 | 'a.b.c.[aaa,bbb]', 212 | [123, 321] 213 | ) 214 | ).toEqual({ a: { b: { c: { aaa: 123, bbb: 321, kkk: 'ddd' } } } }) 215 | }) 216 | 217 | test('getIn with a.b.c.[aaa,bbb] source has extra property', () => { 218 | expect( 219 | Path.getIn( 220 | { a: { b: { c: { aaa: 123, bbb: 321, kkk: 'ddd' } } } }, 221 | 'a.b.c.[aaa,bbb]' 222 | ) 223 | ).toEqual([123, 321]) 224 | }) 225 | 226 | test('setIn with a.b.c.[{ddd,kkk:mmm},bbb]', () => { 227 | expect( 228 | Path.setIn({}, 'a.b.c.[{ddd,kkk:mmm},bbb]', [{ ddd: 123, kkk: 'hhh' }, 321]) 229 | ).toEqual({ a: { b: { c: { ddd: 123, bbb: 321, mmm: 'hhh' } } } }) 230 | }) 231 | 232 | test('getIn with a.b.c.[{ddd,kkk:mmm},bbb]', () => { 233 | expect( 234 | Path.getIn( 235 | { a: { b: { c: { ddd: 123, bbb: 321, mmm: 'hhh' } } } }, 236 | 'a.b.c.[{ddd,kkk:mmm},bbb]' 237 | ) 238 | ).toEqual([{ ddd: 123, kkk: 'hhh' }, 321]) 239 | }) 240 | 241 | test('setIn with a.b.c.{aaa:ooo,bbb:[ccc,ddd]}', () => { 242 | expect( 243 | Path.setIn( 244 | { a: { b: { c: { kkk: 'ddd' } } } }, 245 | 'a.b.c.{aaa:ooo,bbb:[ccc,ddd]}', 246 | { aaa: 123, bbb: [123, 321] } 247 | ) 248 | ).toEqual({ a: { b: { c: { ooo: 123, ccc: 123, ddd: 321, kkk: 'ddd' } } } }) 249 | }) 250 | 251 | test('getIn with a.b.c.{aaa:ooo,bbb:[ccc,ddd]}', () => { 252 | expect( 253 | Path.getIn( 254 | { a: { b: { c: { ooo: 123, ccc: 123, ddd: 321, kkk: 'ddd' } } } }, 255 | 'a.b.c.{aaa:ooo,bbb:[ccc,ddd]}' 256 | ) 257 | ).toEqual({ aaa: 123, bbb: [123, 321] }) 258 | }) 259 | 260 | test('existIn with a.b.c', () => { 261 | expect(Path.existIn({ a: { b: { c: 123123 } } }, 'a.b.c')).toEqual(true) 262 | expect(Path.existIn({ a: { b: { c: 123123 } } }, 'a.b.c.d')).toEqual(false) 263 | expect(Path.existIn({ a: 123 }, 'a.b.c.d')).toEqual(false) 264 | expect( 265 | Path.existIn( 266 | { a: { b: { c: { ooo: 123, ccc: 123, ddd: 321, kkk: 'ddd' } } } }, 267 | 'a.b.c.{aaa:ooo,bbb:[ccc,ddd]}' 268 | ) 269 | ).toEqual(true) 270 | expect( 271 | Path.existIn( 272 | { a: { b: { c: { ooo: 123, ccc: 123, kkk: 'ddd' } } } }, 273 | 'a.b.c.{aaa:ooo,bbb:[ccc,ddd]}' 274 | ) 275 | ).toEqual(false) 276 | expect(Path.existIn({ a: [{}] }, 'a.0')).toEqual(true) 277 | }) 278 | 279 | test('existIn with start Path', () => { 280 | expect(Path.existIn({ a: [{}] }, 'a.0', Path.parse('a'))).toEqual(false) 281 | expect(Path.existIn({ a: [{}] }, 'b.a.0', Path.parse('b'))).toEqual(true) 282 | }) 283 | 284 | test('deleteIn', () => { 285 | expect( 286 | Path.deleteIn({ a: { b: { c: { ooo: 123, ccc: 234 } } } }, 'a.b.c.ccc') 287 | ).toEqual({ a: { b: { c: { ooo: 123 } } } }) 288 | 289 | expect( 290 | Path.deleteIn({ a: { b: { c: { ooo: 123, ccc: 234 } } } }, null) 291 | ).toEqual({ a: { b: { c: { ooo: 123, ccc: 234 } } } }) 292 | 293 | expect( 294 | Path.deleteIn({ a: { b: { c: { ooo: 123, ccc: 234 } } } }, []) 295 | ).toEqual({ a: { b: { c: { ooo: 123, ccc: 234 } } } }) 296 | 297 | expect(Path.deleteIn({ a: { b: { c: 'c' } } }, 'a.b.c.ccc')).toEqual({ 298 | a: { b: { c: 'c' } }, 299 | }) 300 | 301 | expect(Path.deleteIn({ a: 1, b: 2 }, '{ a }')).toEqual({ b: 2 }) 302 | expect(Path.deleteIn([1, 2], '[0]')).toEqual([undefined, 2]) 303 | }) 304 | 305 | test('ensureIn', () => { 306 | expect(Path.parse('a.b').ensureIn({}, 'default')).toEqual('default') 307 | expect(Path.parse('a.b').ensureIn({ a: { b: 'value' } }, 'default')).toEqual( 308 | 'value' 309 | ) 310 | expect(Path.ensureIn({}, 'a.b.c', 'default')).toEqual('default') 311 | }) 312 | 313 | test('complex destructing', () => { 314 | expect( 315 | Path.setIn( 316 | {}, 317 | '{aa:{bb:{cc:destructor1,dd:[destructor2,destructor3],ee}}}', 318 | { 319 | aa: { 320 | bb: { 321 | cc: 123, 322 | dd: [333, 444], 323 | ee: 'abcde', 324 | }, 325 | }, 326 | } 327 | ) 328 | ).toEqual({ 329 | destructor1: 123, 330 | destructor2: 333, 331 | destructor3: 444, 332 | ee: 'abcde', 333 | }) 334 | expect( 335 | Path.getIn( 336 | { 337 | destructor1: 123, 338 | destructor2: 333, 339 | destructor3: 444, 340 | ee: 'abcde', 341 | }, 342 | '{aa:{bb:{cc:destructor1,dd:[destructor2,destructor3],ee}}}' 343 | ) 344 | ).toEqual({ 345 | aa: { 346 | bb: { 347 | cc: 123, 348 | dd: [333, 444], 349 | ee: 'abcde', 350 | }, 351 | }, 352 | }) 353 | }) 354 | 355 | test('test getIn with invalid value', () => { 356 | const value = { 357 | array: [null, undefined, { nil: null, undef: undefined }], 358 | nil: null, 359 | undef: undefined, 360 | } 361 | expect(getIn(value, 'array.0')).toBeNull() 362 | expect(getIn(value, 'array.1')).toBeUndefined() 363 | expect(getIn(value, 'array.2.nil')).toBeNull() 364 | expect(getIn(value, 'array.2.undef')).toBeUndefined() 365 | expect(getIn(value, 'nil')).toBeNull() 366 | expect(getIn(value, 'undef')).toBeUndefined() 367 | }) 368 | 369 | test('test setIn with invalid value', () => { 370 | const value = { 371 | a: 1, 372 | b: 2, 373 | array: [null, undefined, { nil: null, undef: undefined }], 374 | nil: null, 375 | undef: undefined, 376 | } 377 | setIn(value, 'a', null) 378 | setIn(value, 'b', undefined) 379 | // undefined 与 null 互转 380 | setIn(value, 'array.0', undefined) 381 | setIn(value, 'array.1', null) 382 | setIn(value, 'array.2.nil', undefined) 383 | setIn(value, 'array.2.undef', null) 384 | setIn(value, 'nil', undefined) 385 | setIn(value, 'undef', null) 386 | 387 | expect(getIn(value, 'a')).toBeNull() 388 | expect(getIn(value, 'b')).toBeUndefined() 389 | expect(getIn(value, 'array.0')).toBeUndefined() 390 | expect(getIn(value, 'array.1')).toBeNull() 391 | expect(getIn(value, 'array.2.nil')).toBeUndefined() 392 | expect(getIn(value, 'array.2.undef')).toBeNull() 393 | expect(getIn(value, 'nil')).toBeUndefined() 394 | expect(getIn(value, 'undef')).toBeNull() 395 | }) 396 | 397 | test('path arguments', () => { 398 | const path = new Path('a.b.c') 399 | expect(new Path(path).segments).toEqual(['a', 'b', 'c']) 400 | 401 | const matchPath = Path.match('a.b.c') 402 | expect(new Path(matchPath).segments).toEqual(['a', 'b', 'c']) 403 | 404 | expect(new Path(undefined).segments).toEqual([]) 405 | }) 406 | 407 | test('path methods', () => { 408 | const path = Path.parse('a.b.c') 409 | 410 | expect(path.concat(Path.parse('d.e')).segments).toEqual([ 411 | 'a', 412 | 'b', 413 | 'c', 414 | 'd', 415 | 'e', 416 | ]) 417 | 418 | expect(Path.parse(['a', 'b', 'c']).toString()).toEqual('a.b.c') 419 | expect(Path.parse(['a', 'b', 'c']).length).toEqual(3) 420 | 421 | const matchPath = Path.parse('*') 422 | const regexPath = Path.parse(/.+/) 423 | expect(() => matchPath.concat('a')).toThrowError() 424 | expect(() => regexPath.concat('a')).toThrowError() 425 | expect(() => matchPath.slice()).toThrowError() 426 | expect(() => regexPath.slice()).toThrowError() 427 | expect(() => matchPath.pop()).toThrowError() 428 | expect(() => regexPath.pop()).toThrowError() 429 | expect(() => matchPath.splice(0, 1)).toThrowError() 430 | expect(() => regexPath.splice(0, 1)).toThrowError() 431 | expect(() => matchPath.forEach(() => {})).toThrowError() 432 | expect(() => regexPath.forEach(() => {})).toThrowError() 433 | expect(() => matchPath.map(() => {})).toThrowError() 434 | expect(() => regexPath.map(() => {})).toThrowError() 435 | expect(() => matchPath.reduce((p) => p, '')).toThrowError() 436 | expect(() => regexPath.reduce((p) => p, '')).toThrowError() 437 | 438 | expect(path.slice().segments).toEqual(['a', 'b', 'c']) 439 | expect(path.push('d').segments).toEqual(['a', 'b', 'c', 'd']) 440 | expect(path.pop().segments).toEqual(['a', 'b']) 441 | expect(path.splice(0, 1).segments).toEqual(['b', 'c']) 442 | 443 | let key = '' 444 | path.forEach((p) => (key += p + '_')) 445 | expect(key).toEqual('a_b_c_') 446 | expect(path.map((p) => p)).toEqual(['a', 'b', 'c']) 447 | expect(path.reduce((str, p) => str + p, '')).toEqual('abc') 448 | expect(path.parent().segments).toEqual(['a', 'b']) 449 | 450 | expect(() => Path.parse('*').includes('*')).toThrowError() 451 | expect(() => Path.parse('*').includes('*')).toThrowError() 452 | expect(() => Path.parse('a.b').includes('*')).toThrowError() 453 | expect(Path.parse('*').includes('a.b')).toBeTruthy() 454 | expect(Path.parse('a.b').includes('a.b')).toBeTruthy() 455 | expect(Path.parse('a.b').includes('a.c')).toBeFalsy() 456 | expect(Path.parse('a.b').includes('a.b.c')).toBeFalsy() 457 | 458 | expect(Path.parse('a.b.c').transform(/[a-z]/, (...result) => result)).toEqual( 459 | ['a', 'b', 'c'] 460 | ) 461 | expect(Path.parse('a.b.c').transform(/[a-b]/, (...result) => result)).toEqual( 462 | ['a', 'b'] 463 | ) 464 | expect(Path.parse('a.b.c').transform('', null)).toEqual('') 465 | expect(() => Path.parse('*').transform('', () => {})).toThrowError() 466 | expect(Path.transform('a.b.c', /[a-z]/, (...result) => result)).toEqual([ 467 | 'a', 468 | 'b', 469 | 'c', 470 | ]) 471 | 472 | expect(Path.parse('a.b.c').match('*')).toBeTruthy() 473 | expect(() => Path.parse('*').match('*')).toThrowError() 474 | expect(Path.match('*')('a.b.c')).toBeTruthy() 475 | expect(Path.match('a.b')('a.b.c')).toBeFalsy() 476 | 477 | const matcher = Path.match('a.b.c') 478 | expect(Path.parse(matcher).segments).toEqual(['a', 'b', 'c']) 479 | }) 480 | ``` -------------------------------------------------------------------------------- /packages/json-schema/src/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | ISchema, 3 | SchemaEnum, 4 | SchemaProperties, 5 | SchemaReaction, 6 | SchemaTypes, 7 | SchemaKey, 8 | ISchemaTransformerOptions, 9 | Slot, 10 | } from './types' 11 | import { IFieldFactoryProps } from '@formily/core' 12 | import { map, each, isFn, instOf, FormPath, isStr } from '@formily/shared' 13 | import { compile, silent, shallowCompile, registerCompiler } from './compiler' 14 | import { transformFieldProps } from './transformer' 15 | import { 16 | reducePatches, 17 | registerPatches, 18 | registerPolyfills, 19 | enablePolyfills, 20 | } from './patches' 21 | import { 22 | registerVoidComponents, 23 | registerTypeDefaultComponents, 24 | } from './polyfills' 25 | import { SchemaNestedMap } from './shared' 26 | 27 | export class Schema< 28 | Decorator = any, 29 | Component = any, 30 | DecoratorProps = any, 31 | ComponentProps = any, 32 | Pattern = any, 33 | Display = any, 34 | Validator = any, 35 | Message = any, 36 | ReactionField = any 37 | > implements ISchema 38 | { 39 | parent?: Schema 40 | root?: Schema 41 | name?: SchemaKey 42 | title?: Message 43 | description?: Message 44 | default?: any 45 | readOnly?: boolean 46 | writeOnly?: boolean 47 | type?: SchemaTypes 48 | enum?: SchemaEnum<Message> 49 | const?: any 50 | multipleOf?: number 51 | maximum?: number 52 | exclusiveMaximum?: number 53 | minimum?: number 54 | exclusiveMinimum?: number 55 | maxLength?: number 56 | minLength?: number 57 | pattern?: string | RegExp 58 | maxItems?: number 59 | minItems?: number 60 | uniqueItems?: boolean 61 | maxProperties?: number 62 | minProperties?: number 63 | required?: string[] | boolean | string 64 | format?: string 65 | /** nested json schema spec **/ 66 | definitions?: Record< 67 | string, 68 | Schema< 69 | Decorator, 70 | Component, 71 | DecoratorProps, 72 | ComponentProps, 73 | Pattern, 74 | Display, 75 | Validator, 76 | Message 77 | > 78 | > 79 | properties?: Record< 80 | string, 81 | Schema< 82 | Decorator, 83 | Component, 84 | DecoratorProps, 85 | ComponentProps, 86 | Pattern, 87 | Display, 88 | Validator, 89 | Message 90 | > 91 | > 92 | items?: 93 | | Schema< 94 | Decorator, 95 | Component, 96 | DecoratorProps, 97 | ComponentProps, 98 | Pattern, 99 | Display, 100 | Validator, 101 | Message 102 | > 103 | | Schema< 104 | Decorator, 105 | Component, 106 | DecoratorProps, 107 | ComponentProps, 108 | Pattern, 109 | Display, 110 | Validator, 111 | Message 112 | >[] 113 | additionalItems?: Schema< 114 | Decorator, 115 | Component, 116 | DecoratorProps, 117 | ComponentProps, 118 | Pattern, 119 | Display, 120 | Validator, 121 | Message 122 | > 123 | patternProperties?: Record< 124 | string, 125 | Schema< 126 | Decorator, 127 | Component, 128 | DecoratorProps, 129 | ComponentProps, 130 | Pattern, 131 | Display, 132 | Validator, 133 | Message 134 | > 135 | > 136 | additionalProperties?: Schema< 137 | Decorator, 138 | Component, 139 | DecoratorProps, 140 | ComponentProps, 141 | Pattern, 142 | Display, 143 | Validator, 144 | Message 145 | >; 146 | 147 | //顺序描述 148 | ['x-index']?: number; 149 | //交互模式 150 | ['x-pattern']?: Pattern; 151 | //展示状态 152 | ['x-display']?: Display; 153 | //校验器 154 | ['x-validator']?: Validator; 155 | //装饰器 156 | ['x-decorator']?: Decorator; 157 | //装饰器属性 158 | ['x-decorator-props']?: DecoratorProps; 159 | //组件 160 | ['x-component']?: Component; 161 | //组件属性 162 | ['x-component-props']?: ComponentProps; 163 | 164 | ['x-reactions']?: SchemaReaction<ReactionField>[]; 165 | 166 | ['x-content']?: any; 167 | 168 | ['x-data']?: any; 169 | 170 | ['x-visible']?: boolean; 171 | 172 | ['x-hidden']?: boolean; 173 | 174 | ['x-disabled']?: boolean; 175 | 176 | ['x-editable']?: boolean; 177 | 178 | ['x-read-only']?: boolean; 179 | 180 | ['x-read-pretty']?: boolean; 181 | 182 | ['x-compile-omitted']?: string[]; 183 | 184 | ['x-slot-node']?: Slot; 185 | 186 | [key: `x-${string | number}` | symbol]: any 187 | 188 | _isJSONSchemaObject = true 189 | 190 | version = '2.0' 191 | 192 | constructor( 193 | json: ISchema< 194 | Decorator, 195 | Component, 196 | DecoratorProps, 197 | ComponentProps, 198 | Pattern, 199 | Display, 200 | Validator, 201 | Message 202 | >, 203 | parent?: Schema 204 | ) { 205 | if (parent) { 206 | this.parent = parent 207 | this.root = parent.root 208 | } else { 209 | this.root = this 210 | } 211 | return this.fromJSON(json) 212 | } 213 | 214 | addProperty = ( 215 | key: SchemaKey, 216 | schema: ISchema< 217 | Decorator, 218 | Component, 219 | DecoratorProps, 220 | ComponentProps, 221 | Pattern, 222 | Display, 223 | Validator, 224 | Message 225 | > 226 | ) => { 227 | this.properties = this.properties || {} 228 | this.properties[key] = new Schema(schema, this) 229 | this.properties[key].name = key 230 | return this.properties[key] 231 | } 232 | 233 | removeProperty = (key: SchemaKey) => { 234 | const schema = this.properties[key] 235 | delete this.properties[key] 236 | return schema 237 | } 238 | 239 | setProperties = ( 240 | properties: SchemaProperties< 241 | Decorator, 242 | Component, 243 | DecoratorProps, 244 | ComponentProps, 245 | Pattern, 246 | Display, 247 | Validator, 248 | Message 249 | > 250 | ) => { 251 | for (const key in properties) { 252 | this.addProperty(key, properties[key]) 253 | } 254 | return this 255 | } 256 | 257 | addPatternProperty = ( 258 | key: SchemaKey, 259 | schema: ISchema< 260 | Decorator, 261 | Component, 262 | DecoratorProps, 263 | ComponentProps, 264 | Pattern, 265 | Display, 266 | Validator, 267 | Message 268 | > 269 | ) => { 270 | if (!schema) return 271 | this.patternProperties = this.patternProperties || {} 272 | this.patternProperties[key] = new Schema(schema, this) 273 | this.patternProperties[key].name = key 274 | return this.patternProperties[key] 275 | } 276 | 277 | removePatternProperty = (key: SchemaKey) => { 278 | const schema = this.patternProperties[key] 279 | delete this.patternProperties[key] 280 | return schema 281 | } 282 | 283 | setPatternProperties = ( 284 | properties: SchemaProperties< 285 | Decorator, 286 | Component, 287 | DecoratorProps, 288 | ComponentProps, 289 | Pattern, 290 | Display, 291 | Validator, 292 | Message 293 | > 294 | ) => { 295 | if (!properties) return this 296 | for (const key in properties) { 297 | this.addPatternProperty(key, properties[key]) 298 | } 299 | return this 300 | } 301 | 302 | setAdditionalProperties = ( 303 | properties: ISchema< 304 | Decorator, 305 | Component, 306 | DecoratorProps, 307 | ComponentProps, 308 | Pattern, 309 | Display, 310 | Validator, 311 | Message 312 | > 313 | ) => { 314 | if (!properties) return 315 | this.additionalProperties = new Schema(properties) 316 | return this.additionalProperties 317 | } 318 | 319 | setItems = ( 320 | schema: 321 | | ISchema< 322 | Decorator, 323 | Component, 324 | DecoratorProps, 325 | ComponentProps, 326 | Pattern, 327 | Display, 328 | Validator, 329 | Message 330 | > 331 | | ISchema< 332 | Decorator, 333 | Component, 334 | DecoratorProps, 335 | ComponentProps, 336 | Pattern, 337 | Display, 338 | Validator, 339 | Message 340 | >[] 341 | ) => { 342 | if (!schema) return 343 | if (Array.isArray(schema)) { 344 | this.items = schema.map((item) => new Schema(item, this)) 345 | } else { 346 | this.items = new Schema(schema, this) 347 | } 348 | return this.items 349 | } 350 | 351 | setAdditionalItems = ( 352 | items: ISchema< 353 | Decorator, 354 | Component, 355 | DecoratorProps, 356 | ComponentProps, 357 | Pattern, 358 | Display, 359 | Validator, 360 | Message 361 | > 362 | ) => { 363 | if (!items) return 364 | this.additionalItems = new Schema(items, this) 365 | return this.additionalItems 366 | } 367 | 368 | findDefinitions = (ref: string) => { 369 | if (!ref || !this.root || !isStr(ref)) return 370 | if (ref.indexOf('#/') !== 0) return 371 | return FormPath.getIn(this.root, ref.substring(2).split('/')) 372 | } 373 | 374 | mapProperties = <T>( 375 | callback?: ( 376 | schema: Schema< 377 | Decorator, 378 | Component, 379 | DecoratorProps, 380 | ComponentProps, 381 | Pattern, 382 | Display, 383 | Validator, 384 | Message 385 | >, 386 | key: SchemaKey, 387 | index: number 388 | ) => T 389 | ): T[] => { 390 | return Schema.getOrderProperties(this).map(({ schema, key }, index) => { 391 | return callback(schema, key, index) 392 | }) 393 | } 394 | 395 | mapPatternProperties = <T>( 396 | callback?: ( 397 | schema: Schema< 398 | Decorator, 399 | Component, 400 | DecoratorProps, 401 | ComponentProps, 402 | Pattern, 403 | Display, 404 | Validator, 405 | Message 406 | >, 407 | key: SchemaKey, 408 | index: number 409 | ) => T 410 | ): T[] => { 411 | return Schema.getOrderProperties(this, 'patternProperties').map( 412 | ({ schema, key }, index) => { 413 | return callback(schema, key, index) 414 | } 415 | ) 416 | } 417 | 418 | reduceProperties = <P, R>( 419 | callback?: ( 420 | buffer: P, 421 | schema: Schema< 422 | Decorator, 423 | Component, 424 | DecoratorProps, 425 | ComponentProps, 426 | Pattern, 427 | Display, 428 | Validator, 429 | Message 430 | >, 431 | key: SchemaKey, 432 | index: number 433 | ) => R, 434 | predicate?: P 435 | ): R => { 436 | let results: any = predicate 437 | Schema.getOrderProperties(this, 'properties').forEach( 438 | ({ schema, key }, index) => { 439 | results = callback(results, schema, key, index) 440 | } 441 | ) 442 | return results 443 | } 444 | 445 | reducePatternProperties = <P, R>( 446 | callback?: ( 447 | buffer: P, 448 | schema: Schema< 449 | Decorator, 450 | Component, 451 | DecoratorProps, 452 | ComponentProps, 453 | Pattern, 454 | Display, 455 | Validator, 456 | Message 457 | >, 458 | key: SchemaKey, 459 | index: number 460 | ) => R, 461 | predicate?: P 462 | ): R => { 463 | let results: any = predicate 464 | Schema.getOrderProperties(this, 'patternProperties').forEach( 465 | ({ schema, key }, index) => { 466 | results = callback(results, schema, key, index) 467 | } 468 | ) 469 | return results 470 | } 471 | 472 | compile = (scope?: any) => { 473 | const schema = new Schema({}, this.parent) 474 | each(this, (value, key) => { 475 | if (isFn(value) && !key.includes('x-')) return 476 | if (key === 'parent' || key === 'root') return 477 | if (!SchemaNestedMap[key]) { 478 | schema[key] = value ? compile(value, scope) : value 479 | } else { 480 | schema[key] = value ? shallowCompile(value, scope) : value 481 | } 482 | }) 483 | return schema 484 | } 485 | 486 | fromJSON = ( 487 | json: ISchema< 488 | Decorator, 489 | Component, 490 | DecoratorProps, 491 | ComponentProps, 492 | Pattern, 493 | Display, 494 | Validator, 495 | Message 496 | > 497 | ) => { 498 | if (!json) return this 499 | if (Schema.isSchemaInstance(json)) return json 500 | each(reducePatches(json), (value, key) => { 501 | if (isFn(value) && !key.includes('x-')) return 502 | if (key === 'properties') { 503 | this.setProperties(value) 504 | } else if (key === 'patternProperties') { 505 | this.setPatternProperties(value) 506 | } else if (key === 'additionalProperties') { 507 | this.setAdditionalProperties(value) 508 | } else if (key === 'items') { 509 | this.setItems(value) 510 | } else if (key === 'additionalItems') { 511 | this.setAdditionalItems(value) 512 | } else if (key === '$ref') { 513 | this.fromJSON(this.findDefinitions(value)) 514 | } else { 515 | this[key] = value 516 | } 517 | }) 518 | return this 519 | } 520 | 521 | toJSON = ( 522 | recursion = true 523 | ): ISchema< 524 | Decorator, 525 | Component, 526 | DecoratorProps, 527 | ComponentProps, 528 | Pattern, 529 | Display, 530 | Validator, 531 | Message 532 | > => { 533 | const results = {} 534 | each(this, (value: any, key) => { 535 | if ( 536 | (isFn(value) && !key.includes('x-')) || 537 | key === 'parent' || 538 | key === 'root' 539 | ) 540 | return 541 | if (key === 'properties' || key === 'patternProperties') { 542 | if (!recursion) return 543 | results[key] = map(value, (item) => item?.toJSON?.()) 544 | } else if (key === 'additionalProperties' || key === 'additionalItems') { 545 | if (!recursion) return 546 | results[key] = value?.toJSON?.() 547 | } else if (key === 'items') { 548 | if (!recursion) return 549 | if (Array.isArray(value)) { 550 | results[key] = value.map((item) => item?.toJSON?.()) 551 | } else { 552 | results[key] = value?.toJSON?.() 553 | } 554 | } else { 555 | results[key] = value 556 | } 557 | }) 558 | return results 559 | } 560 | 561 | toFieldProps = ( 562 | options?: ISchemaTransformerOptions 563 | ): IFieldFactoryProps<any, any> => { 564 | return transformFieldProps(this, options) 565 | } 566 | 567 | static getOrderProperties = ( 568 | schema: ISchema = {}, 569 | propertiesName: keyof ISchema = 'properties' 570 | ) => { 571 | const orderProperties = [] 572 | const unorderProperties = [] 573 | for (const key in schema[propertiesName]) { 574 | const item = schema[propertiesName][key] 575 | const index = item['x-index'] 576 | if (!isNaN(index)) { 577 | orderProperties[index] = { schema: item, key } 578 | } else { 579 | unorderProperties.push({ schema: item, key }) 580 | } 581 | } 582 | return orderProperties.concat(unorderProperties).filter((item) => !!item) 583 | } 584 | 585 | static compile = (expression: any, scope?: any) => { 586 | return compile(expression, scope) 587 | } 588 | 589 | static shallowCompile = (expression: any, scope?: any) => { 590 | return shallowCompile(expression, scope) 591 | } 592 | 593 | static isSchemaInstance = (value: any): value is Schema => { 594 | return instOf(value, Schema) 595 | } 596 | 597 | static registerCompiler = registerCompiler 598 | 599 | static registerPatches = registerPatches 600 | 601 | static registerVoidComponents = registerVoidComponents 602 | 603 | static registerTypeDefaultComponents = registerTypeDefaultComponents 604 | 605 | static registerPolyfills = registerPolyfills 606 | 607 | static enablePolyfills = enablePolyfills 608 | 609 | static silent = silent 610 | } 611 | ``` -------------------------------------------------------------------------------- /packages/core/src/models/Field.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | isValid, 3 | isEmpty, 4 | toArr, 5 | FormPathPattern, 6 | isArr, 7 | } from '@formily/shared' 8 | import { 9 | ValidatorTriggerType, 10 | parseValidatorDescriptions, 11 | } from '@formily/validator' 12 | import { define, observable, batch, toJS, action } from '@formily/reactive' 13 | import { 14 | JSXComponent, 15 | LifeCycleTypes, 16 | IFieldFeedback, 17 | FeedbackMessage, 18 | IFieldCaches, 19 | IFieldRequests, 20 | FieldValidator, 21 | FieldDataSource, 22 | ISearchFeedback, 23 | IFieldProps, 24 | IFieldResetOptions, 25 | IFieldState, 26 | IModelSetter, 27 | IModelGetter, 28 | } from '../types' 29 | import { 30 | updateFeedback, 31 | queryFeedbacks, 32 | allowAssignDefaultValue, 33 | queryFeedbackMessages, 34 | getValuesFromEvent, 35 | createReactions, 36 | createStateSetter, 37 | createStateGetter, 38 | isHTMLInputEvent, 39 | setValidatorRule, 40 | batchValidate, 41 | batchSubmit, 42 | batchReset, 43 | setValidating, 44 | setSubmitting, 45 | setLoading, 46 | validateSelf, 47 | modifySelf, 48 | getValidFieldDefaultValue, 49 | initializeStart, 50 | initializeEnd, 51 | createChildrenFeedbackFilter, 52 | createReaction, 53 | } from '../shared/internals' 54 | import { Form } from './Form' 55 | import { BaseField } from './BaseField' 56 | import { IFormFeedback } from '../types' 57 | export class Field< 58 | Decorator extends JSXComponent = any, 59 | Component extends JSXComponent = any, 60 | TextType = any, 61 | ValueType = any 62 | > extends BaseField<Decorator, Component, TextType> { 63 | displayName = 'Field' 64 | 65 | props: IFieldProps<Decorator, Component, TextType, ValueType> 66 | 67 | loading: boolean 68 | validating: boolean 69 | submitting: boolean 70 | active: boolean 71 | visited: boolean 72 | selfModified: boolean 73 | modified: boolean 74 | inputValue: ValueType 75 | inputValues: any[] 76 | dataSource: FieldDataSource 77 | validator: FieldValidator 78 | feedbacks: IFieldFeedback[] 79 | caches: IFieldCaches = {} 80 | requests: IFieldRequests = {} 81 | constructor( 82 | address: FormPathPattern, 83 | props: IFieldProps<Decorator, Component, TextType, ValueType>, 84 | form: Form, 85 | designable: boolean 86 | ) { 87 | super() 88 | this.form = form 89 | this.props = props 90 | this.designable = designable 91 | initializeStart() 92 | this.locate(address) 93 | this.initialize() 94 | this.makeObservable() 95 | this.makeReactive() 96 | this.onInit() 97 | initializeEnd() 98 | } 99 | 100 | protected initialize() { 101 | this.initialized = false 102 | this.loading = false 103 | this.validating = false 104 | this.submitting = false 105 | this.selfModified = false 106 | this.active = false 107 | this.visited = false 108 | this.mounted = false 109 | this.unmounted = false 110 | this.inputValues = [] 111 | this.inputValue = null 112 | this.feedbacks = [] 113 | this.title = this.props.title 114 | this.description = this.props.description 115 | this.display = this.props.display 116 | this.pattern = this.props.pattern 117 | this.editable = this.props.editable 118 | this.disabled = this.props.disabled 119 | this.readOnly = this.props.readOnly 120 | this.readPretty = this.props.readPretty 121 | this.visible = this.props.visible 122 | this.hidden = this.props.hidden 123 | this.dataSource = this.props.dataSource 124 | this.validator = this.props.validator 125 | this.required = this.props.required 126 | this.content = this.props.content 127 | this.initialValue = this.props.initialValue 128 | this.value = this.props.value 129 | this.data = this.props.data 130 | this.decorator = toArr(this.props.decorator) 131 | this.component = toArr(this.props.component) 132 | } 133 | 134 | protected makeObservable() { 135 | if (this.designable) return 136 | define(this, { 137 | path: observable.ref, 138 | title: observable.ref, 139 | description: observable.ref, 140 | dataSource: observable.ref, 141 | selfDisplay: observable.ref, 142 | selfPattern: observable.ref, 143 | loading: observable.ref, 144 | validating: observable.ref, 145 | submitting: observable.ref, 146 | selfModified: observable.ref, 147 | modified: observable.ref, 148 | active: observable.ref, 149 | visited: observable.ref, 150 | initialized: observable.ref, 151 | mounted: observable.ref, 152 | unmounted: observable.ref, 153 | inputValue: observable.ref, 154 | inputValues: observable.ref, 155 | decoratorType: observable.ref, 156 | componentType: observable.ref, 157 | content: observable.ref, 158 | feedbacks: observable.ref, 159 | decoratorProps: observable, 160 | componentProps: observable, 161 | validator: observable.shallow, 162 | data: observable.shallow, 163 | component: observable.computed, 164 | decorator: observable.computed, 165 | errors: observable.computed, 166 | warnings: observable.computed, 167 | successes: observable.computed, 168 | valid: observable.computed, 169 | invalid: observable.computed, 170 | selfErrors: observable.computed, 171 | selfWarnings: observable.computed, 172 | selfSuccesses: observable.computed, 173 | selfValid: observable.computed, 174 | selfInvalid: observable.computed, 175 | validateStatus: observable.computed, 176 | value: observable.computed, 177 | initialValue: observable.computed, 178 | display: observable.computed, 179 | pattern: observable.computed, 180 | required: observable.computed, 181 | hidden: observable.computed, 182 | visible: observable.computed, 183 | disabled: observable.computed, 184 | readOnly: observable.computed, 185 | readPretty: observable.computed, 186 | editable: observable.computed, 187 | indexes: observable.computed, 188 | setDisplay: action, 189 | setTitle: action, 190 | setDescription: action, 191 | setDataSource: action, 192 | setValue: action, 193 | setPattern: action, 194 | setInitialValue: action, 195 | setLoading: action, 196 | setValidating: action, 197 | setFeedback: action, 198 | setSelfErrors: action, 199 | setSelfWarnings: action, 200 | setSelfSuccesses: action, 201 | setValidator: action, 202 | setRequired: action, 203 | setComponent: action, 204 | setComponentProps: action, 205 | setDecorator: action, 206 | setDecoratorProps: action, 207 | setData: action, 208 | setContent: action, 209 | validate: action, 210 | reset: action, 211 | onInit: batch, 212 | onInput: batch, 213 | onMount: batch, 214 | onUnmount: batch, 215 | onFocus: batch, 216 | onBlur: batch, 217 | }) 218 | } 219 | 220 | protected makeReactive() { 221 | if (this.designable) return 222 | this.disposers.push( 223 | createReaction( 224 | () => this.value, 225 | (value) => { 226 | this.notify(LifeCycleTypes.ON_FIELD_VALUE_CHANGE) 227 | if (isValid(value)) { 228 | if (this.selfModified && !this.caches.inputting) { 229 | validateSelf(this) 230 | } 231 | if (!isEmpty(value) && this.display === 'none') { 232 | this.caches.value = toJS(value) 233 | this.form.deleteValuesIn(this.path) 234 | } 235 | } 236 | } 237 | ), 238 | createReaction( 239 | () => this.initialValue, 240 | () => { 241 | this.notify(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE) 242 | } 243 | ), 244 | createReaction( 245 | () => this.display, 246 | (display) => { 247 | const value = this.value 248 | if (display !== 'none') { 249 | if (value === undefined && this.caches.value !== undefined) { 250 | this.setValue(this.caches.value) 251 | this.caches.value = undefined 252 | } 253 | } else { 254 | this.caches.value = toJS(value) ?? toJS(this.initialValue) 255 | this.form.deleteValuesIn(this.path) 256 | } 257 | if (display === 'none' || display === 'hidden') { 258 | this.setFeedback({ 259 | type: 'error', 260 | messages: [], 261 | }) 262 | } 263 | } 264 | ), 265 | createReaction( 266 | () => this.pattern, 267 | (pattern) => { 268 | if (pattern !== 'editable') { 269 | this.setFeedback({ 270 | type: 'error', 271 | messages: [], 272 | }) 273 | } 274 | } 275 | ) 276 | ) 277 | createReactions(this) 278 | } 279 | 280 | get selfErrors(): FeedbackMessage { 281 | return queryFeedbackMessages(this, { 282 | type: 'error', 283 | }) 284 | } 285 | 286 | get errors(): IFormFeedback[] { 287 | return this.form.errors.filter(createChildrenFeedbackFilter(this)) 288 | } 289 | 290 | get selfWarnings(): FeedbackMessage { 291 | return queryFeedbackMessages(this, { 292 | type: 'warning', 293 | }) 294 | } 295 | 296 | get warnings(): IFormFeedback[] { 297 | return this.form.warnings.filter(createChildrenFeedbackFilter(this)) 298 | } 299 | 300 | get selfSuccesses(): FeedbackMessage { 301 | return queryFeedbackMessages(this, { 302 | type: 'success', 303 | }) 304 | } 305 | 306 | get successes(): IFormFeedback[] { 307 | return this.form.successes.filter(createChildrenFeedbackFilter(this)) 308 | } 309 | 310 | get selfValid() { 311 | return !this.selfErrors.length 312 | } 313 | 314 | get valid() { 315 | return !this.errors.length 316 | } 317 | 318 | get selfInvalid() { 319 | return !this.selfValid 320 | } 321 | 322 | get invalid() { 323 | return !this.valid 324 | } 325 | 326 | get value(): ValueType { 327 | return this.form.getValuesIn(this.path) 328 | } 329 | 330 | get initialValue(): ValueType { 331 | return this.form.getInitialValuesIn(this.path) 332 | } 333 | 334 | get required() { 335 | const validators = isArr(this.validator) 336 | ? this.validator 337 | : parseValidatorDescriptions(this.validator) 338 | return validators.some((desc) => !!desc?.['required']) 339 | } 340 | 341 | get validateStatus() { 342 | if (this.validating) return 'validating' 343 | if (this.selfInvalid) return 'error' 344 | if (this.selfWarnings.length) return 'warning' 345 | if (this.selfSuccesses.length) return 'success' 346 | } 347 | 348 | set required(required: boolean) { 349 | if (this.required === required) return 350 | this.setValidatorRule('required', required) 351 | } 352 | 353 | set value(value: ValueType) { 354 | this.setValue(value) 355 | } 356 | 357 | set initialValue(initialValue: ValueType) { 358 | this.setInitialValue(initialValue) 359 | } 360 | 361 | set selfErrors(messages: FeedbackMessage) { 362 | this.setFeedback({ 363 | type: 'error', 364 | code: 'EffectError', 365 | messages, 366 | }) 367 | } 368 | 369 | set selfWarnings(messages: FeedbackMessage) { 370 | this.setFeedback({ 371 | type: 'warning', 372 | code: 'EffectWarning', 373 | messages, 374 | }) 375 | } 376 | 377 | set selfSuccesses(messages: FeedbackMessage) { 378 | this.setFeedback({ 379 | type: 'success', 380 | code: 'EffectSuccess', 381 | messages, 382 | }) 383 | } 384 | 385 | setDataSource = (dataSource?: FieldDataSource) => { 386 | this.dataSource = dataSource 387 | } 388 | 389 | setFeedback = (feedback?: IFieldFeedback) => { 390 | updateFeedback(this, feedback) 391 | } 392 | 393 | setSelfErrors = (messages?: FeedbackMessage) => { 394 | this.selfErrors = messages 395 | } 396 | 397 | setSelfWarnings = (messages?: FeedbackMessage) => { 398 | this.selfWarnings = messages 399 | } 400 | 401 | setSelfSuccesses = (messages?: FeedbackMessage) => { 402 | this.selfSuccesses = messages 403 | } 404 | 405 | setValidator = (validator?: FieldValidator) => { 406 | this.validator = validator 407 | } 408 | 409 | setValidatorRule = (name: string, value: any) => { 410 | setValidatorRule(this, name, value) 411 | } 412 | 413 | setRequired = (required?: boolean) => { 414 | this.required = required 415 | } 416 | 417 | setValue = (value?: ValueType) => { 418 | if (this.destroyed) return 419 | if (!this.initialized) { 420 | if (this.display === 'none') { 421 | this.caches.value = value 422 | return 423 | } 424 | value = getValidFieldDefaultValue(value, this.initialValue) 425 | if (!allowAssignDefaultValue(this.value, value) && !this.designable) { 426 | return 427 | } 428 | } 429 | this.form.setValuesIn(this.path, value) 430 | } 431 | 432 | setInitialValue = (initialValue?: ValueType) => { 433 | if (this.destroyed) return 434 | if (!this.initialized) { 435 | if ( 436 | !allowAssignDefaultValue(this.initialValue, initialValue) && 437 | !this.designable 438 | ) { 439 | return 440 | } 441 | } 442 | this.form.setInitialValuesIn(this.path, initialValue) 443 | } 444 | 445 | setLoading = (loading?: boolean) => { 446 | setLoading(this, loading) 447 | } 448 | 449 | setValidating = (validating?: boolean) => { 450 | setValidating(this, validating) 451 | } 452 | 453 | setSubmitting = (submitting?: boolean) => { 454 | setSubmitting(this, submitting) 455 | } 456 | 457 | setState: IModelSetter<IFieldState> = createStateSetter(this) 458 | 459 | getState: IModelGetter<IFieldState> = createStateGetter(this) 460 | 461 | onInput = async (...args: any[]) => { 462 | const isHTMLInputEventFromSelf = (args: any[]) => 463 | isHTMLInputEvent(args[0]) && 'currentTarget' in args[0] 464 | ? args[0]?.target === args[0]?.currentTarget 465 | : true 466 | const getValues = (args: any[]) => { 467 | if (args[0]?.target) { 468 | if (!isHTMLInputEvent(args[0])) return args 469 | } 470 | return getValuesFromEvent(args) 471 | } 472 | 473 | if (!isHTMLInputEventFromSelf(args)) return 474 | 475 | const values = getValues(args) 476 | const value = values[0] 477 | this.caches.inputting = true 478 | this.inputValue = value 479 | this.inputValues = values 480 | this.value = value 481 | this.modify() 482 | this.notify(LifeCycleTypes.ON_FIELD_INPUT_VALUE_CHANGE) 483 | this.notify(LifeCycleTypes.ON_FORM_INPUT_CHANGE, this.form) 484 | await validateSelf(this, 'onInput') 485 | this.caches.inputting = false 486 | } 487 | 488 | onFocus = async (...args: any[]) => { 489 | if (args[0]?.target) { 490 | if (!isHTMLInputEvent(args[0], false)) return 491 | } 492 | this.active = true 493 | this.visited = true 494 | await validateSelf(this, 'onFocus') 495 | } 496 | 497 | onBlur = async (...args: any[]) => { 498 | if (args[0]?.target) { 499 | if (!isHTMLInputEvent(args[0], false)) return 500 | } 501 | this.active = false 502 | await validateSelf(this, 'onBlur') 503 | } 504 | 505 | validate = (triggerType?: ValidatorTriggerType) => { 506 | return batchValidate(this, `${this.address}.**`, triggerType) 507 | } 508 | 509 | submit = <T>(onSubmit?: (values: any) => Promise<T> | void): Promise<T> => { 510 | return batchSubmit(this, onSubmit) 511 | } 512 | 513 | reset = (options?: IFieldResetOptions) => { 514 | return batchReset(this, `${this.address}.**`, options) 515 | } 516 | 517 | queryFeedbacks = (search?: ISearchFeedback): IFieldFeedback[] => { 518 | return queryFeedbacks(this, search) 519 | } 520 | 521 | modify = () => modifySelf(this) 522 | } 523 | ```