This is page 27 of 35. Use http://codebase.md/alibaba/formily?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .all-contributorsrc ├── .codecov.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ └── config.yml │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows │ ├── check-pr-title.yml │ ├── ci.yml │ ├── commitlint.yml │ ├── issue-open-check.yml │ ├── package-size.yml │ └── pr-welcome.yml ├── .gitignore ├── .prettierrc.js ├── .umirc.js ├── .vscode │ └── cspell.json ├── .yarnrc ├── CHANGELOG.md ├── commitlint.config.js ├── devtools │ ├── .eslintrc │ └── chrome-extension │ ├── .npmignore │ ├── assets │ │ └── img │ │ ├── loading.svg │ │ └── logo │ │ ├── 128x128.png │ │ ├── 16x16.png │ │ ├── 38x38.png │ │ ├── 48x48.png │ │ ├── error.png │ │ ├── gray.png │ │ └── scalable.png │ ├── config │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── LICENSE.md │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── components │ │ │ │ ├── FieldTree.tsx │ │ │ │ ├── filter.ts │ │ │ │ ├── LeftPanel.tsx │ │ │ │ ├── RightPanel.tsx │ │ │ │ ├── SearchBox.tsx │ │ │ │ └── Tabs.tsx │ │ │ ├── demo.tsx │ │ │ └── index.tsx │ │ └── extension │ │ ├── backend.ts │ │ ├── background.ts │ │ ├── content.ts │ │ ├── devpanel.tsx │ │ ├── devtools.tsx │ │ ├── inject.ts │ │ ├── manifest.json │ │ ├── popup.tsx │ │ └── views │ │ ├── devpanel.ejs │ │ ├── devtools.ejs │ │ └── popup.ejs │ ├── tsconfig.build.json │ └── tsconfig.json ├── docs │ ├── functions │ │ ├── contributors.ts │ │ └── npm-search.ts │ ├── guide │ │ ├── advanced │ │ │ ├── async.md │ │ │ ├── async.zh-CN.md │ │ │ ├── build.md │ │ │ ├── build.zh-CN.md │ │ │ ├── business-logic.md │ │ │ ├── business-logic.zh-CN.md │ │ │ ├── calculator.md │ │ │ ├── calculator.zh-CN.md │ │ │ ├── controlled.md │ │ │ ├── controlled.zh-CN.md │ │ │ ├── custom.md │ │ │ ├── custom.zh-CN.md │ │ │ ├── destructor.md │ │ │ ├── destructor.zh-CN.md │ │ │ ├── input.less │ │ │ ├── layout.md │ │ │ ├── layout.zh-CN.md │ │ │ ├── linkages.md │ │ │ ├── linkages.zh-CN.md │ │ │ ├── validate.md │ │ │ └── validate.zh-CN.md │ │ ├── contribution.md │ │ ├── contribution.zh-CN.md │ │ ├── form-builder.md │ │ ├── form-builder.zh-CN.md │ │ ├── index.md │ │ ├── index.zh-CN.md │ │ ├── issue-helper.md │ │ ├── issue-helper.zh-CN.md │ │ ├── learn-formily.md │ │ ├── learn-formily.zh-CN.md │ │ ├── quick-start.md │ │ ├── quick-start.zh-CN.md │ │ ├── scenes │ │ │ ├── dialog-drawer.md │ │ │ ├── dialog-drawer.zh-CN.md │ │ │ ├── edit-detail.md │ │ │ ├── edit-detail.zh-CN.md │ │ │ ├── index.less │ │ │ ├── login-register.md │ │ │ ├── login-register.zh-CN.md │ │ │ ├── more.md │ │ │ ├── more.zh-CN.md │ │ │ ├── query-list.md │ │ │ ├── query-list.zh-CN.md │ │ │ ├── step-form.md │ │ │ ├── step-form.zh-CN.md │ │ │ ├── tab-form.md │ │ │ ├── tab-form.zh-CN.md │ │ │ └── VerifyCode.tsx │ │ ├── upgrade.md │ │ └── upgrade.zh-CN.md │ ├── index.md │ ├── index.zh-CN.md │ └── site │ ├── Contributors.less │ ├── Contributors.tsx │ ├── QrCode.less │ ├── QrCode.tsx │ ├── Section.less │ ├── Section.tsx │ └── styles.less ├── global.config.ts ├── jest.config.js ├── lerna.json ├── LICENSE.md ├── package.json ├── packages │ ├── .eslintrc │ ├── antd │ │ ├── __tests__ │ │ │ ├── moment.spec.ts │ │ │ └── sideEffects.spec.ts │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── build-style.ts │ │ ├── create-style.ts │ │ ├── docs │ │ │ ├── components │ │ │ │ ├── ArrayCards.md │ │ │ │ ├── ArrayCards.zh-CN.md │ │ │ │ ├── ArrayCollapse.md │ │ │ │ ├── ArrayCollapse.zh-CN.md │ │ │ │ ├── ArrayItems.md │ │ │ │ ├── ArrayItems.zh-CN.md │ │ │ │ ├── ArrayTable.md │ │ │ │ ├── ArrayTable.zh-CN.md │ │ │ │ ├── ArrayTabs.md │ │ │ │ ├── ArrayTabs.zh-CN.md │ │ │ │ ├── Cascader.md │ │ │ │ ├── Cascader.zh-CN.md │ │ │ │ ├── Checkbox.md │ │ │ │ ├── Checkbox.zh-CN.md │ │ │ │ ├── DatePicker.md │ │ │ │ ├── DatePicker.zh-CN.md │ │ │ │ ├── Editable.md │ │ │ │ ├── Editable.zh-CN.md │ │ │ │ ├── Form.md │ │ │ │ ├── Form.zh-CN.md │ │ │ │ ├── FormButtonGroup.md │ │ │ │ ├── FormButtonGroup.zh-CN.md │ │ │ │ ├── FormCollapse.md │ │ │ │ ├── FormCollapse.zh-CN.md │ │ │ │ ├── FormDialog.md │ │ │ │ ├── FormDialog.zh-CN.md │ │ │ │ ├── FormDrawer.md │ │ │ │ ├── FormDrawer.zh-CN.md │ │ │ │ ├── FormGrid.md │ │ │ │ ├── FormGrid.zh-CN.md │ │ │ │ ├── FormItem.md │ │ │ │ ├── FormItem.zh-CN.md │ │ │ │ ├── FormLayout.md │ │ │ │ ├── FormLayout.zh-CN.md │ │ │ │ ├── FormStep.md │ │ │ │ ├── FormStep.zh-CN.md │ │ │ │ ├── FormTab.md │ │ │ │ ├── FormTab.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ ├── index.zh-CN.md │ │ │ │ ├── Input.md │ │ │ │ ├── Input.zh-CN.md │ │ │ │ ├── NumberPicker.md │ │ │ │ ├── NumberPicker.zh-CN.md │ │ │ │ ├── Password.md │ │ │ │ ├── Password.zh-CN.md │ │ │ │ ├── PreviewText.md │ │ │ │ ├── PreviewText.zh-CN.md │ │ │ │ ├── Radio.md │ │ │ │ ├── Radio.zh-CN.md │ │ │ │ ├── Reset.md │ │ │ │ ├── Reset.zh-CN.md │ │ │ │ ├── Select.md │ │ │ │ ├── Select.zh-CN.md │ │ │ │ ├── SelectTable.md │ │ │ │ ├── SelectTable.zh-CN.md │ │ │ │ ├── Space.md │ │ │ │ ├── Space.zh-CN.md │ │ │ │ ├── Submit.md │ │ │ │ ├── Submit.zh-CN.md │ │ │ │ ├── Switch.md │ │ │ │ ├── Switch.zh-CN.md │ │ │ │ ├── TimePicker.md │ │ │ │ ├── TimePicker.zh-CN.md │ │ │ │ ├── Transfer.md │ │ │ │ ├── Transfer.zh-CN.md │ │ │ │ ├── TreeSelect.md │ │ │ │ ├── TreeSelect.zh-CN.md │ │ │ │ ├── Upload.md │ │ │ │ └── Upload.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __builtins__ │ │ │ │ ├── hooks │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useClickAway.ts │ │ │ │ │ └── usePrefixCls.ts │ │ │ │ ├── index.ts │ │ │ │ ├── loading.ts │ │ │ │ ├── moment.ts │ │ │ │ ├── pickDataProps.ts │ │ │ │ ├── portal.tsx │ │ │ │ ├── render.ts │ │ │ │ └── sort.tsx │ │ │ ├── array-base │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-cards │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-collapse │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-items │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-table │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-tabs │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── cascader │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── checkbox │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── date-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── editable │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-button-group │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-collapse │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-dialog │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-drawer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-grid │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-item │ │ │ │ ├── animation.less │ │ │ │ ├── grid.less │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-layout │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ ├── style.ts │ │ │ │ └── useResponsiveFormLayout.ts │ │ │ ├── form-step │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-tab │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── index.ts │ │ │ ├── input │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── number-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── password │ │ │ │ ├── index.tsx │ │ │ │ ├── PasswordStrength.tsx │ │ │ │ └── style.ts │ │ │ ├── preview-text │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── radio │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── reset │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select-table │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ ├── style.ts │ │ │ │ ├── useCheckSlackly.tsx │ │ │ │ ├── useFilterOptions.tsx │ │ │ │ ├── useFlatOptions.tsx │ │ │ │ ├── useSize.tsx │ │ │ │ ├── useTitleAddon.tsx │ │ │ │ └── utils.ts │ │ │ ├── space │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── style.less │ │ │ ├── style.ts │ │ │ ├── submit │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── switch │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── time-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── transfer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── tree-select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ └── upload │ │ │ ├── index.tsx │ │ │ ├── placeholder.ts │ │ │ └── style.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── benchmark │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ └── index.tsx │ │ ├── template.ejs │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── core │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── docs │ │ │ ├── api │ │ │ │ ├── entry │ │ │ │ │ ├── ActionResponse.less │ │ │ │ │ ├── ActionResponse.tsx │ │ │ │ │ ├── createForm.md │ │ │ │ │ ├── createForm.zh-CN.md │ │ │ │ │ ├── FieldEffectHooks.md │ │ │ │ │ ├── FieldEffectHooks.zh-CN.md │ │ │ │ │ ├── FormChecker.md │ │ │ │ │ ├── FormChecker.zh-CN.md │ │ │ │ │ ├── FormEffectHooks.md │ │ │ │ │ ├── FormEffectHooks.zh-CN.md │ │ │ │ │ ├── FormHooksAPI.md │ │ │ │ │ ├── FormHooksAPI.zh-CN.md │ │ │ │ │ ├── FormPath.md │ │ │ │ │ ├── FormPath.zh-CN.md │ │ │ │ │ ├── FormValidatorRegistry.md │ │ │ │ │ └── FormValidatorRegistry.zh-CN.md │ │ │ │ └── models │ │ │ │ ├── ArrayField.md │ │ │ │ ├── ArrayField.zh-CN.md │ │ │ │ ├── Field.md │ │ │ │ ├── Field.zh-CN.md │ │ │ │ ├── Form.md │ │ │ │ ├── Form.zh-CN.md │ │ │ │ ├── ObjectField.md │ │ │ │ ├── ObjectField.zh-CN.md │ │ │ │ ├── Query.md │ │ │ │ ├── Query.zh-CN.md │ │ │ │ ├── VoidField.md │ │ │ │ └── VoidField.zh-CN.md │ │ │ ├── guide │ │ │ │ ├── architecture.md │ │ │ │ ├── architecture.zh-CN.md │ │ │ │ ├── field.md │ │ │ │ ├── field.zh-CN.md │ │ │ │ ├── form.md │ │ │ │ ├── form.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ ├── index.zh-CN.md │ │ │ │ ├── mvvm.md │ │ │ │ ├── mvvm.zh-CN.md │ │ │ │ ├── values.md │ │ │ │ └── values.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── array.spec.ts │ │ │ │ ├── effects.spec.ts │ │ │ │ ├── externals.spec.ts │ │ │ │ ├── field.spec.ts │ │ │ │ ├── form.spec.ts │ │ │ │ ├── graph.spec.ts │ │ │ │ ├── heart.spec.ts │ │ │ │ ├── internals.spec.ts │ │ │ │ ├── lifecycle.spec.ts │ │ │ │ ├── object.spec.ts │ │ │ │ ├── shared.ts │ │ │ │ └── void.spec.ts │ │ │ ├── effects │ │ │ │ ├── index.ts │ │ │ │ ├── onFieldEffects.ts │ │ │ │ └── onFormEffects.ts │ │ │ ├── global.d.ts │ │ │ ├── index.ts │ │ │ ├── models │ │ │ │ ├── ArrayField.ts │ │ │ │ ├── BaseField.ts │ │ │ │ ├── Field.ts │ │ │ │ ├── Form.ts │ │ │ │ ├── Graph.ts │ │ │ │ ├── Heart.ts │ │ │ │ ├── index.ts │ │ │ │ ├── LifeCycle.ts │ │ │ │ ├── ObjectField.ts │ │ │ │ ├── Query.ts │ │ │ │ ├── types.ts │ │ │ │ └── VoidField.ts │ │ │ ├── shared │ │ │ │ ├── checkers.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── effective.ts │ │ │ │ ├── externals.ts │ │ │ │ └── internals.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── element │ │ ├── .npmignore │ │ ├── build-style.ts │ │ ├── create-style.ts │ │ ├── docs │ │ │ ├── .vuepress │ │ │ │ ├── components │ │ │ │ │ ├── createCodeSandBox.js │ │ │ │ │ ├── dumi-previewer.vue │ │ │ │ │ └── highlight.js │ │ │ │ ├── config.js │ │ │ │ ├── enhanceApp.js │ │ │ │ ├── styles │ │ │ │ │ └── index.styl │ │ │ │ └── util.js │ │ │ ├── demos │ │ │ │ ├── guide │ │ │ │ │ ├── array-cards │ │ │ │ │ │ ├── effects-json-schema.vue │ │ │ │ │ │ ├── effects-markup-schema.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-collapse │ │ │ │ │ │ ├── effects-json-schema.vue │ │ │ │ │ │ ├── effects-markup-schema.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-items │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-table │ │ │ │ │ │ ├── effects-json-schema.vue │ │ │ │ │ │ ├── effects-markup-schema.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-tabs │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── cascader │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── checkbox │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── date-picker │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── editable │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-button-group.vue │ │ │ │ │ ├── form-collapse │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── form-dialog │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-drawer │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-grid │ │ │ │ │ │ ├── form.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── native.vue │ │ │ │ │ ├── form-item │ │ │ │ │ │ ├── bordered-none.vue │ │ │ │ │ │ ├── common.vue │ │ │ │ │ │ ├── feedback.vue │ │ │ │ │ │ ├── inset.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ ├── size.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-layout │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-step │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── form-tab │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── form.vue │ │ │ │ │ ├── input │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── input-number │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── password │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── preview-text │ │ │ │ │ │ ├── base.vue │ │ │ │ │ │ └── extend.vue │ │ │ │ │ ├── radio │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── reset │ │ │ │ │ │ ├── base.vue │ │ │ │ │ │ ├── force.vue │ │ │ │ │ │ └── validate.vue │ │ │ │ │ ├── select │ │ │ │ │ │ ├── json-schema-async.vue │ │ │ │ │ │ ├── json-schema-sync.vue │ │ │ │ │ │ ├── markup-schema-async-search.vue │ │ │ │ │ │ ├── markup-schema-async.vue │ │ │ │ │ │ ├── markup-schema-sync.vue │ │ │ │ │ │ ├── template-async.vue │ │ │ │ │ │ └── template-sync.vue │ │ │ │ │ ├── space │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── submit │ │ │ │ │ │ ├── base.vue │ │ │ │ │ │ └── loading.vue │ │ │ │ │ ├── switch │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── time-picker │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── transfer │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ └── upload │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ └── template.vue │ │ │ │ └── index.vue │ │ │ ├── guide │ │ │ │ ├── array-cards.md │ │ │ │ ├── array-collapse.md │ │ │ │ ├── array-items.md │ │ │ │ ├── array-table.md │ │ │ │ ├── array-tabs.md │ │ │ │ ├── cascader.md │ │ │ │ ├── checkbox.md │ │ │ │ ├── date-picker.md │ │ │ │ ├── editable.md │ │ │ │ ├── form-button-group.md │ │ │ │ ├── form-collapse.md │ │ │ │ ├── form-dialog.md │ │ │ │ ├── form-drawer.md │ │ │ │ ├── form-grid.md │ │ │ │ ├── form-item.md │ │ │ │ ├── form-layout.md │ │ │ │ ├── form-step.md │ │ │ │ ├── form-tab.md │ │ │ │ ├── form.md │ │ │ │ ├── index.md │ │ │ │ ├── input-number.md │ │ │ │ ├── input.md │ │ │ │ ├── password.md │ │ │ │ ├── preview-text.md │ │ │ │ ├── radio.md │ │ │ │ ├── reset.md │ │ │ │ ├── select.md │ │ │ │ ├── space.md │ │ │ │ ├── submit.md │ │ │ │ ├── switch.md │ │ │ │ ├── time-picker.md │ │ │ │ ├── transfer.md │ │ │ │ └── upload.md │ │ │ └── README.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __builtins__ │ │ │ │ ├── configs │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── shared │ │ │ │ │ ├── create-context.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── loading.ts │ │ │ │ │ ├── portal.ts │ │ │ │ │ ├── resolve-component.ts │ │ │ │ │ ├── transform-component.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ └── styles │ │ │ │ └── common.scss │ │ │ ├── array-base │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-cards │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-collapse │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-items │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-table │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-tabs │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── cascader │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── checkbox │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── date-picker │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── editable │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── el-form │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── el-form-item │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── form │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-button-group │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-collapse │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-dialog │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── form-drawer │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-grid │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-item │ │ │ │ ├── animation.scss │ │ │ │ ├── grid.scss │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ ├── style.ts │ │ │ │ └── var.scss │ │ │ ├── form-layout │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ ├── style.ts │ │ │ │ └── useResponsiveFormLayout.ts │ │ │ ├── form-step │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── form-tab │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── index.ts │ │ │ ├── input │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── input-number │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── password │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── preview-text │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── radio │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── reset │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── select │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── space │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── style.ts │ │ │ ├── submit │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── switch │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── time-picker │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── transfer │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ └── upload │ │ │ ├── index.ts │ │ │ └── style.ts │ │ ├── transformer.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── grid │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── index.ts │ │ │ └── observer.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── json-schema │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── schema.spec.ts.snap │ │ │ │ ├── compiler.spec.ts │ │ │ │ ├── patches.spec.ts │ │ │ │ ├── schema.spec.ts │ │ │ │ ├── server-validate.spec.ts │ │ │ │ ├── shared.spec.ts │ │ │ │ ├── transformer.spec.ts │ │ │ │ └── traverse.spec.ts │ │ │ ├── compiler.ts │ │ │ ├── global.d.ts │ │ │ ├── index.ts │ │ │ ├── patches.ts │ │ │ ├── polyfills │ │ │ │ ├── index.ts │ │ │ │ └── SPECIFICATION_1_0.ts │ │ │ ├── schema.ts │ │ │ ├── shared.ts │ │ │ ├── transformer.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── next │ │ ├── __tests__ │ │ │ ├── moment.spec.ts │ │ │ └── sideEffects.spec.ts │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── build-style.ts │ │ ├── create-style.ts │ │ ├── docs │ │ │ ├── components │ │ │ │ ├── ArrayCards.md │ │ │ │ ├── ArrayCards.zh-CN.md │ │ │ │ ├── ArrayCollapse.md │ │ │ │ ├── ArrayCollapse.zh-CN.md │ │ │ │ ├── ArrayItems.md │ │ │ │ ├── ArrayItems.zh-CN.md │ │ │ │ ├── ArrayTable.md │ │ │ │ ├── ArrayTable.zh-CN.md │ │ │ │ ├── Cascader.md │ │ │ │ ├── Cascader.zh-CN.md │ │ │ │ ├── Checkbox.md │ │ │ │ ├── Checkbox.zh-CN.md │ │ │ │ ├── DatePicker.md │ │ │ │ ├── DatePicker.zh-CN.md │ │ │ │ ├── DatePicker2.md │ │ │ │ ├── DatePicker2.zh-CN.md │ │ │ │ ├── Editable.md │ │ │ │ ├── Editable.zh-CN.md │ │ │ │ ├── Form.md │ │ │ │ ├── Form.zh-CN.md │ │ │ │ ├── FormButtonGroup.md │ │ │ │ ├── FormButtonGroup.zh-CN.md │ │ │ │ ├── FormCollapse.md │ │ │ │ ├── FormCollapse.zh-CN.md │ │ │ │ ├── FormDialog.md │ │ │ │ ├── FormDialog.zh-CN.md │ │ │ │ ├── FormDrawer.md │ │ │ │ ├── FormDrawer.zh-CN.md │ │ │ │ ├── FormGrid.md │ │ │ │ ├── FormGrid.zh-CN.md │ │ │ │ ├── FormItem.md │ │ │ │ ├── FormItem.zh-CN.md │ │ │ │ ├── FormLayout.md │ │ │ │ ├── FormLayout.zh-CN.md │ │ │ │ ├── FormStep.md │ │ │ │ ├── FormStep.zh-CN.md │ │ │ │ ├── FormTab.md │ │ │ │ ├── FormTab.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ ├── index.zh-CN.md │ │ │ │ ├── Input.md │ │ │ │ ├── Input.zh-CN.md │ │ │ │ ├── NumberPicker.md │ │ │ │ ├── NumberPicker.zh-CN.md │ │ │ │ ├── Password.md │ │ │ │ ├── Password.zh-CN.md │ │ │ │ ├── PreviewText.md │ │ │ │ ├── PreviewText.zh-CN.md │ │ │ │ ├── Radio.md │ │ │ │ ├── Radio.zh-CN.md │ │ │ │ ├── Reset.md │ │ │ │ ├── Reset.zh-CN.md │ │ │ │ ├── Select.md │ │ │ │ ├── Select.zh-CN.md │ │ │ │ ├── SelectTable.md │ │ │ │ ├── SelectTable.zh-CN.md │ │ │ │ ├── Space.md │ │ │ │ ├── Space.zh-CN.md │ │ │ │ ├── Submit.md │ │ │ │ ├── Submit.zh-CN.md │ │ │ │ ├── Switch.md │ │ │ │ ├── Switch.zh-CN.md │ │ │ │ ├── TimePicker.md │ │ │ │ ├── TimePicker.zh-CN.md │ │ │ │ ├── TimePicker2.md │ │ │ │ ├── TimePicker2.zh-CN.md │ │ │ │ ├── Transfer.md │ │ │ │ ├── Transfer.zh-CN.md │ │ │ │ ├── TreeSelect.md │ │ │ │ ├── TreeSelect.zh-CN.md │ │ │ │ ├── Upload.md │ │ │ │ └── Upload.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LESENCE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __builtins__ │ │ │ │ ├── empty.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useClickAway.ts │ │ │ │ │ └── usePrefixCls.ts │ │ │ │ ├── icons.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── loading.ts │ │ │ │ ├── mapSize.ts │ │ │ │ ├── mapStatus.ts │ │ │ │ ├── moment.ts │ │ │ │ ├── pickDataProps.ts │ │ │ │ ├── portal.tsx │ │ │ │ ├── render.ts │ │ │ │ └── toArray.ts │ │ │ ├── array-base │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-cards │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-collapse │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-items │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-table │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── cascader │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── checkbox │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── date-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── date-picker2 │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── editable │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form-button-group │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form-collapse │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-dialog │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-drawer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-grid │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form-item │ │ │ │ ├── animation.scss │ │ │ │ ├── grid.scss │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ ├── scss │ │ │ │ │ └── variable.scss │ │ │ │ └── style.ts │ │ │ ├── form-layout │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ ├── style.ts │ │ │ │ └── useResponsiveFormLayout.ts │ │ │ ├── form-step │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-tab │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── index.ts │ │ │ ├── input │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── main.scss │ │ │ ├── number-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── password │ │ │ │ ├── index.tsx │ │ │ │ ├── PasswordStrength.tsx │ │ │ │ └── style.ts │ │ │ ├── preview-text │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── radio │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── reset │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select-table │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ ├── style.ts │ │ │ │ ├── useCheckSlackly.tsx │ │ │ │ ├── useFilterOptions.tsx │ │ │ │ ├── useFlatOptions.tsx │ │ │ │ ├── useSize.tsx │ │ │ │ ├── useTitleAddon.tsx │ │ │ │ └── utils.ts │ │ │ ├── space │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── style.ts │ │ │ ├── submit │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── switch │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── time-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── time-picker2 │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── transfer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── tree-select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ └── upload │ │ │ ├── index.tsx │ │ │ ├── main.scss │ │ │ ├── placeholder.ts │ │ │ └── style.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── path │ │ ├── .npmignore │ │ ├── benchmark.ts │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── accessor.spec.ts │ │ │ │ ├── basic.spec.ts │ │ │ │ ├── match.spec.ts │ │ │ │ ├── parser.spec.ts │ │ │ │ └── share.spec.ts │ │ │ ├── contexts.ts │ │ │ ├── destructor.ts │ │ │ ├── index.ts │ │ │ ├── matcher.ts │ │ │ ├── parser.ts │ │ │ ├── shared.ts │ │ │ ├── tokenizer.ts │ │ │ ├── tokens.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── react │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── docs │ │ │ ├── api │ │ │ │ ├── components │ │ │ │ │ ├── ArrayField.md │ │ │ │ │ ├── ArrayField.zh-CN.md │ │ │ │ │ ├── ExpressionScope.md │ │ │ │ │ ├── ExpressionScope.zh-CN.md │ │ │ │ │ ├── Field.md │ │ │ │ │ ├── Field.zh-CN.md │ │ │ │ │ ├── FormConsumer.md │ │ │ │ │ ├── FormConsumer.zh-CN.md │ │ │ │ │ ├── FormProvider.md │ │ │ │ │ ├── FormProvider.zh-CN.md │ │ │ │ │ ├── ObjectField.md │ │ │ │ │ ├── ObjectField.zh-CN.md │ │ │ │ │ ├── RecordScope.md │ │ │ │ │ ├── RecordScope.zh-CN.md │ │ │ │ │ ├── RecordsScope.md │ │ │ │ │ ├── RecordsScope.zh-CN.md │ │ │ │ │ ├── RecursionField.md │ │ │ │ │ ├── RecursionField.zh-CN.md │ │ │ │ │ ├── SchemaField.md │ │ │ │ │ ├── SchemaField.zh-CN.md │ │ │ │ │ ├── VoidField.md │ │ │ │ │ └── VoidField.zh-CN.md │ │ │ │ ├── hooks │ │ │ │ │ ├── useExpressionScope.md │ │ │ │ │ ├── useExpressionScope.zh-CN.md │ │ │ │ │ ├── useField.md │ │ │ │ │ ├── useField.zh-CN.md │ │ │ │ │ ├── useFieldSchema.md │ │ │ │ │ ├── useFieldSchema.zh-CN.md │ │ │ │ │ ├── useForm.md │ │ │ │ │ ├── useForm.zh-CN.md │ │ │ │ │ ├── useFormEffects.md │ │ │ │ │ ├── useFormEffects.zh-CN.md │ │ │ │ │ ├── useParentForm.md │ │ │ │ │ └── useParentForm.zh-CN.md │ │ │ │ └── shared │ │ │ │ ├── connect.md │ │ │ │ ├── connect.zh-CN.md │ │ │ │ ├── context.md │ │ │ │ ├── context.zh-CN.md │ │ │ │ ├── mapProps.md │ │ │ │ ├── mapProps.zh-CN.md │ │ │ │ ├── mapReadPretty.md │ │ │ │ ├── mapReadPretty.zh-CN.md │ │ │ │ ├── observer.md │ │ │ │ ├── observer.zh-CN.md │ │ │ │ ├── Schema.md │ │ │ │ └── Schema.zh-CN.md │ │ │ ├── guide │ │ │ │ ├── architecture.md │ │ │ │ ├── architecture.zh-CN.md │ │ │ │ ├── concept.md │ │ │ │ ├── concept.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ └── index.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── expression.spec.tsx │ │ │ │ ├── field.spec.tsx │ │ │ │ ├── form.spec.tsx │ │ │ │ ├── schema.json.spec.tsx │ │ │ │ ├── schema.markup.spec.tsx │ │ │ │ └── shared.tsx │ │ │ ├── components │ │ │ │ ├── ArrayField.tsx │ │ │ │ ├── ExpressionScope.tsx │ │ │ │ ├── Field.tsx │ │ │ │ ├── FormConsumer.tsx │ │ │ │ ├── FormProvider.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── ObjectField.tsx │ │ │ │ ├── ReactiveField.tsx │ │ │ │ ├── RecordScope.tsx │ │ │ │ ├── RecordsScope.tsx │ │ │ │ ├── RecursionField.tsx │ │ │ │ ├── SchemaField.tsx │ │ │ │ └── VoidField.tsx │ │ │ ├── global.d.ts │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ ├── useAttach.ts │ │ │ │ ├── useExpressionScope.ts │ │ │ │ ├── useField.ts │ │ │ │ ├── useFieldSchema.ts │ │ │ │ ├── useForm.ts │ │ │ │ ├── useFormEffects.ts │ │ │ │ └── useParentForm.ts │ │ │ ├── index.ts │ │ │ ├── shared │ │ │ │ ├── connect.ts │ │ │ │ ├── context.ts │ │ │ │ ├── index.ts │ │ │ │ └── render.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── reactive │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── benchmark.ts │ │ ├── docs │ │ │ ├── api │ │ │ │ ├── action.md │ │ │ │ ├── action.zh-CN.md │ │ │ │ ├── autorun.md │ │ │ │ ├── autorun.zh-CN.md │ │ │ │ ├── batch.md │ │ │ │ ├── batch.zh-CN.md │ │ │ │ ├── define.md │ │ │ │ ├── define.zh-CN.md │ │ │ │ ├── hasCollected.md │ │ │ │ ├── hasCollected.zh-CN.md │ │ │ │ ├── markObservable.md │ │ │ │ ├── markObservable.zh-CN.md │ │ │ │ ├── markRaw.md │ │ │ │ ├── markRaw.zh-CN.md │ │ │ │ ├── model.md │ │ │ │ ├── model.zh-CN.md │ │ │ │ ├── observable.md │ │ │ │ ├── observable.zh-CN.md │ │ │ │ ├── observe.md │ │ │ │ ├── observe.zh-CN.md │ │ │ │ ├── raw.md │ │ │ │ ├── raw.zh-CN.md │ │ │ │ ├── react │ │ │ │ │ ├── observer.md │ │ │ │ │ └── observer.zh-CN.md │ │ │ │ ├── reaction.md │ │ │ │ ├── reaction.zh-CN.md │ │ │ │ ├── toJS.md │ │ │ │ ├── toJS.zh-CN.md │ │ │ │ ├── tracker.md │ │ │ │ ├── tracker.zh-CN.md │ │ │ │ ├── typeChecker.md │ │ │ │ ├── typeChecker.zh-CN.md │ │ │ │ ├── untracked.md │ │ │ │ ├── untracked.zh-CN.md │ │ │ │ └── vue │ │ │ │ ├── observer.md │ │ │ │ └── observer.zh-CN.md │ │ │ ├── guide │ │ │ │ ├── best-practice.md │ │ │ │ ├── best-practice.zh-CN.md │ │ │ │ ├── concept.md │ │ │ │ ├── concept.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ └── index.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── action.spec.ts │ │ │ │ ├── annotations.spec.ts │ │ │ │ ├── array.spec.ts │ │ │ │ ├── autorun.spec.ts │ │ │ │ ├── batch.spec.ts │ │ │ │ ├── collections-map.spec.ts │ │ │ │ ├── collections-set.spec.ts │ │ │ │ ├── collections-weakmap.spec.ts │ │ │ │ ├── collections-weakset.spec.ts │ │ │ │ ├── define.spec.ts │ │ │ │ ├── externals.spec.ts │ │ │ │ ├── hasCollected.spec.ts │ │ │ │ ├── observable.spec.ts │ │ │ │ ├── observe.spec.ts │ │ │ │ ├── tracker.spec.ts │ │ │ │ └── untracked.spec.ts │ │ │ ├── action.ts │ │ │ ├── annotations │ │ │ │ ├── box.ts │ │ │ │ ├── computed.ts │ │ │ │ ├── index.ts │ │ │ │ ├── observable.ts │ │ │ │ ├── ref.ts │ │ │ │ └── shallow.ts │ │ │ ├── array.ts │ │ │ ├── autorun.ts │ │ │ ├── batch.ts │ │ │ ├── checkers.ts │ │ │ ├── environment.ts │ │ │ ├── externals.ts │ │ │ ├── global.d.ts │ │ │ ├── handlers.ts │ │ │ ├── index.ts │ │ │ ├── internals.ts │ │ │ ├── model.ts │ │ │ ├── observable.ts │ │ │ ├── observe.ts │ │ │ ├── reaction.ts │ │ │ ├── tracker.ts │ │ │ ├── tree.ts │ │ │ ├── types.ts │ │ │ └── untracked.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── reactive-react │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ ├── useCompatEffect.ts │ │ │ │ ├── useCompatFactory.ts │ │ │ │ ├── useDidUpdate.ts │ │ │ │ ├── useForceUpdate.ts │ │ │ │ ├── useLayoutEffect.ts │ │ │ │ └── useObserver.ts │ │ │ ├── index.ts │ │ │ ├── observer.ts │ │ │ ├── shared │ │ │ │ ├── gc.ts │ │ │ │ ├── global.ts │ │ │ │ ├── immediate.ts │ │ │ │ └── index.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── reactive-test-cases-for-react18 │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.js │ │ │ └── MySlowList.js │ │ ├── template.ejs │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── reactive-vue │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ └── observer.spec.ts │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ └── useObserver.ts │ │ │ ├── index.ts │ │ │ ├── observer │ │ │ │ ├── collectData.ts │ │ │ │ ├── index.ts │ │ │ │ ├── observerInVue2.ts │ │ │ │ └── observerInVue3.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── shared │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ └── index.spec.ts │ │ │ ├── array.ts │ │ │ ├── case.ts │ │ │ ├── checkers.ts │ │ │ ├── clone.ts │ │ │ ├── compare.ts │ │ │ ├── defaults.ts │ │ │ ├── deprecate.ts │ │ │ ├── global.ts │ │ │ ├── index.ts │ │ │ ├── instanceof.ts │ │ │ ├── isEmpty.ts │ │ │ ├── merge.ts │ │ │ ├── middleware.ts │ │ │ ├── path.ts │ │ │ ├── string.ts │ │ │ ├── subscribable.ts │ │ │ └── uid.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── validator │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── parser.spec.ts │ │ │ │ ├── registry.spec.ts │ │ │ │ └── validator.spec.ts │ │ │ ├── formats.ts │ │ │ ├── index.ts │ │ │ ├── locale.ts │ │ │ ├── parser.ts │ │ │ ├── registry.ts │ │ │ ├── rules.ts │ │ │ ├── template.ts │ │ │ ├── types.ts │ │ │ └── validator.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ └── vue │ ├── .npmignore │ ├── bin │ │ ├── formily-vue-fix.js │ │ └── formily-vue-switch.js │ ├── docs │ │ ├── .vuepress │ │ │ ├── components │ │ │ │ ├── createCodeSandBox.js │ │ │ │ ├── dumi-previewer.vue │ │ │ │ └── highlight.js │ │ │ ├── config.js │ │ │ ├── enhanceApp.js │ │ │ └── styles │ │ │ └── index.styl │ │ ├── api │ │ │ ├── components │ │ │ │ ├── array-field.md │ │ │ │ ├── expression-scope.md │ │ │ │ ├── field.md │ │ │ │ ├── form-consumer.md │ │ │ │ ├── form-provider.md │ │ │ │ ├── object-field.md │ │ │ │ ├── recursion-field-with-component.md │ │ │ │ ├── recursion-field.md │ │ │ │ ├── schema-field-with-schema.md │ │ │ │ ├── schema-field.md │ │ │ │ └── void-field.md │ │ │ ├── hooks │ │ │ │ ├── use-field-schema.md │ │ │ │ ├── use-field.md │ │ │ │ ├── use-form-effects.md │ │ │ │ ├── use-form.md │ │ │ │ └── use-parent-form.md │ │ │ └── shared │ │ │ ├── connect.md │ │ │ ├── injections.md │ │ │ ├── map-props.md │ │ │ ├── map-read-pretty.md │ │ │ ├── observer.md │ │ │ └── schema.md │ │ ├── demos │ │ │ ├── api │ │ │ │ ├── components │ │ │ │ │ ├── array-field.vue │ │ │ │ │ ├── expression-scope.vue │ │ │ │ │ ├── field.vue │ │ │ │ │ ├── form-consumer.vue │ │ │ │ │ ├── form-provider.vue │ │ │ │ │ ├── object-field.vue │ │ │ │ │ ├── recursion-field-with-component.vue │ │ │ │ │ ├── recursion-field.vue │ │ │ │ │ ├── schema-field-with-schema.vue │ │ │ │ │ ├── schema-field.vue │ │ │ │ │ └── void-field.vue │ │ │ │ ├── hooks │ │ │ │ │ ├── use-field-schema.vue │ │ │ │ │ ├── use-field.vue │ │ │ │ │ ├── use-form-effects.vue │ │ │ │ │ ├── use-form.vue │ │ │ │ │ └── use-parent-form.vue │ │ │ │ └── shared │ │ │ │ ├── connect.vue │ │ │ │ ├── map-props.vue │ │ │ │ ├── map-read-pretty.vue │ │ │ │ └── observer.vue │ │ │ ├── index.vue │ │ │ └── questions │ │ │ ├── default-slot.vue │ │ │ ├── events.vue │ │ │ ├── named-slot.vue │ │ │ └── scoped-slot.vue │ │ ├── guide │ │ │ ├── architecture.md │ │ │ ├── concept.md │ │ │ └── README.md │ │ ├── questions │ │ │ └── README.md │ │ └── README.md │ ├── package.json │ ├── README.md │ ├── rollup.config.js │ ├── scripts │ │ ├── postinstall.js │ │ ├── switch-cli.js │ │ └── utils.js │ ├── src │ │ ├── __tests__ │ │ │ ├── expression.scope.spec.ts │ │ │ ├── field.spec.ts │ │ │ ├── form.spec.ts │ │ │ ├── schema.json.spec.ts │ │ │ ├── schema.markup.spec.ts │ │ │ ├── shared.spec.ts │ │ │ └── utils.spec.ts │ │ ├── components │ │ │ ├── ArrayField.ts │ │ │ ├── ExpressionScope.ts │ │ │ ├── Field.ts │ │ │ ├── FormConsumer.ts │ │ │ ├── FormProvider.ts │ │ │ ├── index.ts │ │ │ ├── ObjectField.ts │ │ │ ├── ReactiveField.ts │ │ │ ├── RecursionField.ts │ │ │ ├── SchemaField.ts │ │ │ └── VoidField.ts │ │ ├── global.d.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useAttach.ts │ │ │ ├── useField.ts │ │ │ ├── useFieldSchema.ts │ │ │ ├── useForm.ts │ │ │ ├── useFormEffects.ts │ │ │ ├── useInjectionCleaner.ts │ │ │ └── useParentForm.ts │ │ ├── index.ts │ │ ├── shared │ │ │ ├── connect.ts │ │ │ ├── context.ts │ │ │ ├── createForm.ts │ │ │ ├── fragment.ts │ │ │ ├── h.ts │ │ │ └── index.ts │ │ ├── types │ │ │ └── index.ts │ │ ├── utils │ │ │ ├── formatVNodeData.ts │ │ │ ├── getFieldProps.ts │ │ │ ├── getRawComponent.ts │ │ │ └── resolveSchemaProps.ts │ │ └── vue2-components.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── tsconfig.types.json ├── README.md ├── README.zh-cn.md ├── scripts │ ├── build-style │ │ ├── buildAllStyles.ts │ │ ├── copy.ts │ │ ├── helper.ts │ │ └── index.ts │ └── rollup.base.js ├── tsconfig.build.json ├── tsconfig.jest.json ├── tsconfig.json └── yarn.lock ``` # Files -------------------------------------------------------------------------------- /packages/antd/docs/components/ArrayItems.md: -------------------------------------------------------------------------------- ```markdown # ArrayItems > Self-increment list, suitable for simple self-increment editing scenes, or for scenes with high space requirements > > Note: This component is only applicable to Schema scenarios ## Markup Schema example ```tsx import React from 'react' import { FormItem, Input, Editable, Select, DatePicker, ArrayItems, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, DatePicker, Editable, Space, Input, Select, ArrayItems, }, }) const form = createForm() export default () => { return ( <FormProvider form={form}> <SchemaField> <SchemaField.Array name="string_array" title="string array" x-decorator="FormItem" x-component="ArrayItems" > <SchemaField.Void x-component="Space"> <SchemaField.Void x-decorator="FormItem" x-component="ArrayItems.SortHandle" /> <SchemaField.String x-decorator="FormItem" required name="input" x-component="Input" /> <SchemaField.Void x-decorator="FormItem" x-component="ArrayItems.Remove" /> <SchemaField.Void x-decorator="FormItem" x-component="ArrayItems.Copy" /> </SchemaField.Void> <SchemaField.Void x-component="ArrayItems.Addition" title="Add entry" /> </SchemaField.Array> <SchemaField.Array name="array" title="Object array" x-decorator="FormItem" x-component="ArrayItems" > <SchemaField.Object> <SchemaField.Void x-component="Space"> <SchemaField.Void x-decorator="FormItem" x-component="ArrayItems.SortHandle" /> <SchemaField.String x-decorator="FormItem" required title="date" name="date" x-component="DatePicker.RangePicker" x-component-props={{ style: { width: 160, }, }} /> <SchemaField.String x-decorator="FormItem" required title="input box" name="input" x-component="Input" /> <SchemaField.String x-decorator="FormItem" required title="select box" name="select" enum={[ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 }, ]} x-component="Select" x-component-props={{ style: { width: 160, }, }} /> <SchemaField.Void x-decorator="FormItem" x-component="ArrayItems.Remove" /> </SchemaField.Void> </SchemaField.Object> <SchemaField.Void x-component="ArrayItems.Addition" title="Add entry" /> </SchemaField.Array> <SchemaField.Array name="array2" title="Object array" x-decorator="FormItem" x-component="ArrayItems" x-component-props={{ style: { width: 300 } }} > <SchemaField.Object x-decorator="ArrayItems.Item"> <SchemaField.Void x-decorator="FormItem" x-component="ArrayItems.SortHandle" /> <SchemaField.String x-decorator="Editable" title="input box" name="input" x-component="Input" x-component-props={{ bordered: false }} /> <SchemaField.Object name="config" x-component="Editable.Popover" required title="Configure complex data" x-reactions={(field) => { field.title = field.value?.input || field.title }} > <SchemaField.String x-decorator="FormItem" required title="date" name="date" x-component="DatePicker.RangePicker" x-component-props={{ style: { width: '100%' } }} /> <SchemaField.String x-decorator="FormItem" required title="input box" name="input" x-component="Input" /> </SchemaField.Object> <SchemaField.Void x-decorator="FormItem" x-component="ArrayItems.Remove" /> </SchemaField.Object> <SchemaField.Void x-component="ArrayItems.Addition" title="Add entry" /> </SchemaField.Array> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormItem, Editable, Input, Select, Radio, DatePicker, ArrayItems, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Editable, DatePicker, Space, Radio, Input, Select, ArrayItems, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', title: 'String array', items: { type: 'void', 'x-component': 'Space', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, input: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, properties: { add: { type: 'void', title: 'Add entry', 'x-component': 'ArrayItems.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', title: 'Object array', items: { type: 'object', properties: { space: { type: 'void', 'x-component': 'Space', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, date: { type: 'string', title: 'Date', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { style: { width: 160, }, }, }, input: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Input', }, select: { type: 'string', title: 'drop-down box', enum: [ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 160, }, }, }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, }, }, properties: { add: { type: 'void', title: 'Add entry', 'x-component': 'ArrayItems.Addition', }, }, }, array2: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', 'x-component-props': { style: { width: 300 } }, title: 'Object array', items: { type: 'object', 'x-decorator': 'ArrayItems.Item', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, input: { type: 'string', title: 'input box', 'x-decorator': 'Editable', 'x-component': 'Input', 'x-component-props': { bordered: false, }, }, config: { type: 'object', title: 'Configure complex data', 'x-component': 'Editable.Popover', 'x-reactions': '{{(field)=>field.title = field.value && field.value.input || field.title}}', properties: { date: { type: 'string', title: 'Date', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { style: { width: 160, }, }, }, input: { type: 'string', title: 'input box', 'x-decorator': 'FormItem', 'x-component': 'Input', }, select: { type: 'string', title: 'drop-down box', enum: [ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 160, }, }, }, }, }, remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove', }, }, }, properties: { add: { type: 'void', title: 'Add entry', 'x-component': 'ArrayItems.Addition', }, }, }, }, } export default () => { return ( <FormProvider form={form}> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## Effects linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayItems, Editable, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Space, Editable, FormItem, Input, ArrayItems, }, }) const form = createForm({ effects: () => { //Active linkage mode onFieldChange('array.*.aa', ['value'], (field, form) => { form.setFieldState(field.query('.bb'), (state) => { state.visible = field.value != '123' }) }) //Passive linkage mode onFieldReact('array.*.dd', (field) => { field.visible = field.query('.cc').get('value') != '123' }) }, }) export default () => { return ( <FormProvider form={form}> <SchemaField> <SchemaField.Array name="array" title="Object array" maxItems={3} x-decorator="FormItem" x-component="ArrayItems" x-component-props={{ style: { width: 300, }, }} > <SchemaField.Object x-decorator="ArrayItems.Item"> <SchemaField.Void x-component="Space"> <SchemaField.Void x-decorator="FormItem" x-component="ArrayItems.SortHandle" /> <SchemaField.Void x-decorator="FormItem" x-component="ArrayItems.Index" /> </SchemaField.Void> <SchemaField.Void x-component="Editable.Popover" title="Configuration data" > <SchemaField.String name="aa" x-decorator="FormItem" title="AA" required description="AA hide BB when entering 123" x-component="Input" /> <SchemaField.String name="bb" x-decorator="FormItem" title="BB" required x-component="Input" /> <SchemaField.String name="cc" x-decorator="FormItem" title="CC" required description="Hide DD when CC enters 123" x-component="Input" /> <SchemaField.String name="dd" x-decorator="FormItem" title="DD" required x-component="Input" /> </SchemaField.Void> <SchemaField.Void x-component="Space"> <SchemaField.Void x-decorator="FormItem" x-component="ArrayItems.Remove" /> <SchemaField.Void x-decorator="FormItem" x-component="ArrayItems.MoveUp" /> <SchemaField.Void x-decorator="FormItem" x-component="ArrayItems.MoveDown" /> </SchemaField.Void> </SchemaField.Object> <SchemaField.Void x-component="ArrayItems.Addition" title="Add entry" /> </SchemaField.Array> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## JSON Schema linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayItems, Editable, FormButtonGroup, Submit, Space, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Space, Editable, FormItem, Input, ArrayItems, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', maxItems: 3, title: 'Object array', 'x-component-props': { style: { width: 300 } }, items: { type: 'object', 'x-decorator': 'ArrayItems.Item', properties: { left: { type: 'void', 'x-component': 'Space', properties: { sort: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.SortHandle', }, index: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Index', }, }, }, edit: { type: 'void', 'x-component': 'Editable.Popover', title: 'Configuration data', properties: { aa: { type: 'string', 'x-decorator': 'FormItem', title: 'AA', required: true, 'x-component': 'Input', description: 'Enter 123', }, bb: { type: 'string', title: 'BB', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-reactions': [ { dependencies: ['.aa'], when: "{{$deps[0] != '123'}}", fulfill: { schema: { title: 'BB', 'x-disabled': true, }, }, otherwise: { schema: { title: 'Changed', 'x-disabled': false, }, }, }, ], }, }, }, right: { type: 'void', 'x-component': 'Space', properties: { remove: { type: 'void', 'x-component': 'ArrayItems.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayItems.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayItems.MoveDown', }, }, }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayItems.Addition', }, }, }, }, } export default () => { return ( <FormProvider form={form}> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## API ### ArrayItems Extended attributes | Property name | Type | Description | Default value | | ------------- | ------------------------- | --------------- | ------------- | | onAdd | `(index: number) => void` | add method | | | onRemove | `(index: number) => void` | remove method | | | onCopy | `(index: number) => void` | copy method | | | onMoveUp | `(index: number) => void` | moveUp method | | | onMoveDown | `(index: number) => void` | moveDown method | | Other Inherit HTMLDivElement Props ### ArrayItems.Item > List block Inherit HTMLDivElement Props Extended attributes | Property name | Type | Description | Default value | | ------------- | ------------------- | --------------------- | ------------- | | type | `'card' \|'divide'` | card or dividing line | | ### ArrayItems.SortHandle > Drag handle Reference https://ant.design/components/icon-cn/ ### ArrayItems.Addition > Add button Extended attributes | Property name | Type | Description | Default value | | ------------- | -------------------- | ------------- | ------------- | | title | ReactText | Copywriting | | | method | `'push' \|'unshift'` | add method | `'push'` | | defaultValue | `any` | Default value | | Other references https://ant.design/components/button-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayItems.Copy > Copy button Extended attributes | Property name | Type | Description | Default value | | ------------- | -------------------- | ----------- | ------------- | | title | ReactText | Copywriting | | | method | `'push' \|'unshift'` | Copy method | `'push'` | Other references https://ant.design/components/button-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayItems.Remove > Delete button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayItems.MoveDown > Move down button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayItems.MoveUp > Move up button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayItems.Index > Index Renderer No attributes ### ArrayItems.useIndex > Read the React Hook of the current rendering row index ### ArrayItems.useRecord > Read the React Hook of the current rendering row ``` -------------------------------------------------------------------------------- /packages/core/src/__tests__/array.spec.ts: -------------------------------------------------------------------------------- ```typescript import { createForm } from '../' import { onFieldValueChange, onFormInitialValuesChange, onFormValuesChange, } from '../effects' import { DataField } from '../types' import { attach } from './shared' test('create array field', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) expect(array.value).toEqual([]) expect(array.push).toBeDefined() expect(array.pop).toBeDefined() expect(array.shift).toBeDefined() expect(array.unshift).toBeDefined() expect(array.move).toBeDefined() expect(array.moveDown).toBeDefined() expect(array.moveUp).toBeDefined() expect(array.insert).toBeDefined() expect(array.remove).toBeDefined() }) test('array field methods', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', value: [], }) ) array.push({ aa: 11 }, { bb: 22 }) expect(array.value).toEqual([{ aa: 11 }, { bb: 22 }]) array.pop() expect(array.value).toEqual([{ aa: 11 }]) array.unshift({ cc: 33 }) expect(array.value).toEqual([{ cc: 33 }, { aa: 11 }]) array.remove(1) expect(array.value).toEqual([{ cc: 33 }]) array.insert(1, { dd: 44 }, { ee: 55 }) expect(array.value).toEqual([{ cc: 33 }, { dd: 44 }, { ee: 55 }]) array.move(0, 2) expect(array.value).toEqual([{ dd: 44 }, { ee: 55 }, { cc: 33 }]) array.shift() expect(array.value).toEqual([{ ee: 55 }, { cc: 33 }]) array.moveDown(0) expect(array.value).toEqual([{ cc: 33 }, { ee: 55 }]) array.moveUp(1) expect(array.value).toEqual([{ ee: 55 }, { cc: 33 }]) array.move(1, 0) expect(array.value).toEqual([{ cc: 33 }, { ee: 55 }]) }) test('array field children state exchanges', () => { //注意:插入新节点,如果指定位置有节点,会丢弃,需要重新插入节点,主要是为了防止上一个节点状态对新节点状态产生污染 const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createField({ name: 'other', basePath: 'array', }) ) array.push({ value: 11 }, { value: 22 }) attach( form.createField({ name: 'value', basePath: 'array.0', }) ) attach( form.createField({ name: 'value', basePath: 'array.1', }) ) expect(array.value).toEqual([{ value: 11 }, { value: 22 }]) expect(form.query('array.0.value').get('value')).toEqual(11) expect(form.query('array.1.value').get('value')).toEqual(22) expect(Object.keys(form.fields).sort()).toEqual([ 'array', 'array.0.value', 'array.1.value', 'array.other', ]) array.pop() expect(array.value).toEqual([{ value: 11 }]) expect(form.query('array.0.value').get('value')).toEqual(11) expect(form.query('array.1.value').get('value')).toBeUndefined() array.unshift({ value: 33 }) attach( form.createField({ name: 'value', basePath: 'array.0', }) ) attach( form.createField({ name: 'value', basePath: 'array.1', }) ) expect(array.value).toEqual([{ value: 33 }, { value: 11 }]) expect(form.query('array.0.value').get('value')).toEqual(33) expect(form.query('array.1.value').get('value')).toEqual(11) array.remove(1) expect(array.value).toEqual([{ value: 33 }]) expect(form.query('array.0.value').get('value')).toEqual(33) expect(form.query('array.1.value').get('value')).toBeUndefined() array.insert(1, { value: 44 }, { value: 55 }) attach( form.createField({ name: 'value', basePath: 'array.1', }) ) attach( form.createField({ name: 'value', basePath: 'array.2', }) ) expect(array.value).toEqual([{ value: 33 }, { value: 44 }, { value: 55 }]) expect(form.query('array.0.value').get('value')).toEqual(33) expect(form.query('array.1.value').get('value')).toEqual(44) expect(form.query('array.2.value').get('value')).toEqual(55) array.move(0, 2) expect(array.value).toEqual([{ value: 44 }, { value: 55 }, { value: 33 }]) expect(form.query('array.0.value').get('value')).toEqual(44) expect(form.query('array.1.value').get('value')).toEqual(55) expect(form.query('array.2.value').get('value')).toEqual(33) array.move(2, 0) expect(array.value).toEqual([{ value: 33 }, { value: 44 }, { value: 55 }]) expect(form.query('array.0.value').get('value')).toEqual(33) expect(form.query('array.1.value').get('value')).toEqual(44) expect(form.query('array.2.value').get('value')).toEqual(55) }) test('array field move up/down then fields move', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createField({ name: 'value', basePath: 'array.0', }) ) attach( form.createField({ name: 'value', basePath: 'array.1', }) ) attach( form.createField({ name: 'value', basePath: 'array.2', }) ) attach( form.createField({ name: 'value', basePath: 'array.3', }) ) const line0 = form.fields['array.0.value'] const line1 = form.fields['array.1.value'] const line2 = form.fields['array.2.value'] const line3 = form.fields['array.3.value'] array.push({ value: '0' }, { value: '1' }, { value: '2' }, { value: '3' }) array.move(0, 3) // 1,2,3,0 expect(form.fields['array.0.value']).toBe(line1) expect(form.fields['array.1.value']).toBe(line2) expect(form.fields['array.2.value']).toBe(line3) expect(form.fields['array.3.value']).toBe(line0) array.move(3, 1) // 1,0,2,3 expect(form.fields['array.0.value']).toBe(line1) expect(form.fields['array.1.value']).toBe(line0) expect(form.fields['array.2.value']).toBe(line2) expect(form.fields['array.3.value']).toBe(line3) }) // 重现 issues #3932 , 补全 PR #3992 测试用例 test('lazy array field query each', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) const init = Array.from({ length: 6 }).map((_, i) => ({ value: i })) array.setValue(init) // page1: 0, 1 // page2: 2, 3 untouch // page3: 4, 5 init.forEach((item) => { const len = item.value //2, 3 if (len >= 2 && len <= 3) { } else { // 0, 1, 4, 5 attach( form.createField({ name: 'value', basePath: 'array.' + len, }) ) } }) array.insert(1, { value: '11' }) expect(() => form.query('*').take()).not.toThrowError() expect(Object.keys(form.fields)).toEqual([ 'array', 'array.0.value', 'array.5.value', 'array.2.value', 'array.6.value', ]) }) test('void children', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createField({ name: 'other', basePath: 'array', }) ) attach( form.createVoidField({ name: 0, basePath: 'array', }) ) const aaa = attach( form.createField({ name: 'aaa', basePath: 'array.0', value: 123, }) ) expect(aaa.value).toEqual(123) expect(array.value).toEqual([123]) }) test('exchange children', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createField({ name: 'other', basePath: 'array', }) ) attach( form.createField({ name: '0.aaa', basePath: 'array', value: '123', }) ) attach( form.createField({ name: '0.bbb', basePath: 'array', value: '321', }) ) attach( form.createField({ name: '1.bbb', basePath: 'array', value: 'kkk', }) ) expect(array.value).toEqual([{ aaa: '123', bbb: '321' }, { bbb: 'kkk' }]) array.move(0, 1) expect(array.value).toEqual([{ bbb: 'kkk' }, { aaa: '123', bbb: '321' }]) expect(form.query('array.0.aaa').take()).toBeUndefined() }) test('fault tolerance', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) const array2 = attach( form.createArrayField({ name: 'array2', value: [1, 2], }) ) array.setValue({} as any) array.push(11) expect(array.value).toEqual([11]) array.pop() expect(array.value).toEqual([]) array.remove(1) expect(array.value).toEqual([]) array.shift() expect(array.value).toEqual([]) array.unshift(1) expect(array.value).toEqual([1]) array.move(0, 1) expect(array.value).toEqual([1]) array.moveUp(1) expect(array.value).toEqual([1]) array.moveDown(1) expect(array.value).toEqual([1]) array.insert(1) expect(array.value).toEqual([1]) array2.move(1, 1) expect(array2.value).toEqual([1, 2]) array2.push(3) array2.moveUp(2) expect(array2.value).toEqual([1, 3, 2]) array2.moveUp(0) expect(array2.value).toEqual([3, 2, 1]) array2.moveDown(0) expect(array2.value).toEqual([2, 3, 1]) array2.moveDown(1) expect(array2.value).toEqual([2, 1, 3]) array2.moveDown(2) expect(array2.value).toEqual([3, 2, 1]) }) test('mutation fault tolerance', () => { const form = attach(createForm()) const pushArray = attach( form.createArrayField({ name: 'array1', }) ) const popArray = attach( form.createArrayField({ name: 'array2', }) ) const insertArray = attach( form.createArrayField({ name: 'array3', }) ) const removeArray = attach( form.createArrayField({ name: 'array4', }) ) const shiftArray = attach( form.createArrayField({ name: 'array5', }) ) const unshiftArray = attach( form.createArrayField({ name: 'array6', }) ) const moveArray = attach( form.createArrayField({ name: 'array7', }) ) const moveUpArray = attach( form.createArrayField({ name: 'array8', }) ) const moveDownArray = attach( form.createArrayField({ name: 'array9', }) ) pushArray.setValue({} as any) pushArray.push(123) expect(pushArray.value).toEqual([123]) popArray.setValue({} as any) popArray.pop() expect(popArray.value).toEqual({}) insertArray.setValue({} as any) insertArray.insert(0, 123) expect(insertArray.value).toEqual([123]) removeArray.setValue({} as any) removeArray.remove(0) expect(removeArray.value).toEqual({}) shiftArray.setValue({} as any) shiftArray.shift() expect(shiftArray.value).toEqual({}) unshiftArray.setValue({} as any) unshiftArray.unshift(123) expect(unshiftArray.value).toEqual([123]) moveArray.setValue({} as any) moveArray.move(0, 1) expect(moveArray.value).toEqual({}) moveUpArray.setValue({} as any) moveUpArray.moveUp(0) expect(moveUpArray.value).toEqual({}) moveDownArray.setValue({} as any) moveDownArray.moveDown(1) expect(moveDownArray.value).toEqual({}) }) test('array field move api with children', async () => { const form = attach(createForm()) attach( form.createField({ name: 'other', }) ) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createArrayField({ name: '0', basePath: 'array', }) ) attach( form.createArrayField({ name: '1', basePath: 'array', }) ) attach( form.createArrayField({ name: '2', basePath: 'array', }) ) attach( form.createArrayField({ name: 'name', basePath: 'array.2', }) ) await array.move(0, 2) expect(form.fields['array.0.name']).toBeUndefined() expect(form.fields['array.2.name']).toBeUndefined() expect(form.fields['array.1.name']).not.toBeUndefined() }) test('array field remove memo leak', async () => { const handler = jest.fn() const valuesChange = jest.fn() const initialValuesChange = jest.fn() const form = attach( createForm({ effects() { onFormValuesChange(valuesChange) onFormInitialValuesChange(initialValuesChange) onFieldValueChange('*', handler) }, }) ) const array = attach( form.createArrayField({ name: 'array', }) ) await array.push('') attach( form.createField({ name: '0', basePath: 'array', }) ) await array.remove(0) await array.push('') attach( form.createField({ name: '0', basePath: 'array', }) ) expect(handler).toBeCalledTimes(0) expect(valuesChange).toBeCalledTimes(4) expect(initialValuesChange).toBeCalledTimes(0) }) test('nest array remove', async () => { const form = attach(createForm()) const metrics = attach( form.createArrayField({ name: 'metrics', }) ) attach( form.createObjectField({ name: '0', basePath: 'metrics', }) ) attach( form.createObjectField({ name: '1', basePath: 'metrics', }) ) attach( form.createArrayField({ name: 'content', basePath: 'metrics.0', }) ) attach( form.createArrayField({ name: 'content', basePath: 'metrics.1', }) ) const obj00 = attach( form.createObjectField({ name: '0', basePath: 'metrics.0.content', }) ) const obj10 = attach( form.createObjectField({ name: '0', basePath: 'metrics.1.content', }) ) attach( form.createField({ name: 'attr', basePath: 'metrics.0.content.0', initialValue: '123', }) ) attach( form.createField({ name: 'attr', basePath: 'metrics.1.content.0', initialValue: '123', }) ) expect(obj00.indexes[0]).toBe(0) expect(obj00.index).toBe(0) expect(obj10.index).toBe(0) expect(obj10.indexes[0]).toBe(1) await (form.query('metrics.1.content').take() as any).remove(0) expect(form.fields['metrics.0.content.0.attr']).not.toBeUndefined() await metrics.remove(1) expect(form.fields['metrics.0.content.0.attr']).not.toBeUndefined() expect( form.initialValues.metrics?.[1]?.content?.[0]?.attr ).not.toBeUndefined() }) test('indexes: nest path need exclude incomplete number', () => { const form = attach(createForm()) const objPathIncludeNum = attach( form.createField({ name: 'attr', basePath: 'metrics.0.a.10.iconWidth50', }) ) expect(objPathIncludeNum.indexes.length).toBe(2) expect(objPathIncludeNum.indexes).toEqual([0, 10]) expect(objPathIncludeNum.index).toBe(10) }) test('incomplete insertion of array elements', async () => { const form = attach( createForm({ values: { array: [{ aa: 1 }, { aa: 2 }, { aa: 3 }], }, }) ) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createObjectField({ name: '0', basePath: 'array', }) ) attach( form.createField({ name: 'aa', basePath: 'array.0', }) ) attach( form.createObjectField({ name: '2', basePath: 'array', }) ) attach( form.createField({ name: 'aa', basePath: 'array.2', }) ) expect(form.fields['array.0.aa']).not.toBeUndefined() expect(form.fields['array.1.aa']).toBeUndefined() expect(form.fields['array.2.aa']).not.toBeUndefined() await array.unshift({}) expect(form.fields['array.0.aa']).toBeUndefined() expect(form.fields['array.1.aa']).not.toBeUndefined() expect(form.fields['array.2.aa']).toBeUndefined() expect(form.fields['array.3.aa']).not.toBeUndefined() }) test('void array items need skip data', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) const array2 = attach( form.createArrayField({ name: 'array2', }) ) attach( form.createVoidField({ name: '0', basePath: 'array', }) ) attach( form.createVoidField({ name: '0', basePath: 'array2', }) ) attach( form.createVoidField({ name: 'space', basePath: 'array.0', }) ) const select = attach( form.createField({ name: 'select', basePath: 'array.0.space', }) ) const select2 = attach( form.createField({ name: 'select2', basePath: 'array2.0', }) ) select.value = 123 select2.value = 123 expect(array.value).toEqual([123]) expect(array2.value).toEqual([123]) }) test('array field reset', () => { const form = attach(createForm()) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createObjectField({ name: '0', basePath: 'array', }) ) attach( form.createField({ name: 'input', initialValue: '123', basePath: 'array.0', }) ) form.reset('*', { forceClear: true }) expect(form.values).toEqual({ array: [] }) expect(array.value).toEqual([]) }) test('array field remove can not memory leak', async () => { const handler = jest.fn() const form = attach( createForm({ values: { array: [{ aa: 1 }, { aa: 2 }], }, effects() { onFieldValueChange('array.*.aa', handler) }, }) ) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createObjectField({ name: '0', basePath: 'array', }) ) attach( form.createField({ name: 'aa', basePath: 'array.0', }) ) attach( form.createObjectField({ name: '1', basePath: 'array', }) ) attach( form.createField({ name: 'aa', basePath: 'array.1', }) ) const bb = attach( form.createField({ name: 'bb', basePath: 'array.1', reactions: (field) => { field.visible = field.query('.aa').value() === '123' }, }) ) expect(bb.visible).toBeFalsy() await array.remove(0) form.query('array.0.aa').take((field) => { ;(field as DataField).value = '123' }) expect(bb.visible).toBeTruthy() expect(handler).toBeCalledTimes(1) }) test('array field patch values', async () => { const form = attach(createForm()) const arr = attach( form.createArrayField({ name: 'a', }) ) await arr.unshift({}) attach( form.createObjectField({ name: '0', basePath: 'a', }) ) attach( form.createField({ name: 'c', initialValue: 'A', basePath: 'a.0', }) ) expect(form.values).toEqual({ a: [{ c: 'A' }] }) await arr.unshift({}) attach( form.createObjectField({ name: '0', basePath: 'a', }) ) attach( form.createField({ name: 'c', initialValue: 'A', basePath: 'a.0', }) ) attach( form.createObjectField({ name: '1', basePath: 'a', }) ) attach( form.createField({ name: 'c', initialValue: 'A', basePath: 'a.1', }) ) expect(form.values).toEqual({ a: [{ c: 'A' }, { c: 'A' }] }) }) test('array remove with initialValues', async () => { const form = attach( createForm({ initialValues: { array: [{ a: 1 }, { a: 2 }], }, }) ) const array = attach( form.createArrayField({ name: 'array', }) ) attach( form.createObjectField({ name: '0', basePath: 'array', }) ) attach( form.createObjectField({ name: '1', basePath: 'array', }) ) attach( form.createField({ name: 'a', basePath: 'array.0', }) ) attach( form.createField({ name: 'a', basePath: 'array.1', }) ) expect(form.values).toEqual({ array: [{ a: 1 }, { a: 2 }] }) await array.remove(1) expect(form.values).toEqual({ array: [{ a: 1 }] }) expect(form.initialValues).toEqual({ array: [{ a: 1 }, { a: 2 }] }) await array.reset() attach( form.createObjectField({ name: '1', basePath: 'array', }) ) attach( form.createField({ name: 'a', basePath: 'array.0', }) ) attach( form.createField({ name: 'a', basePath: 'array.1', }) ) expect(form.values).toEqual({ array: [{ a: 1 }, { a: 2 }] }) expect(form.initialValues).toEqual({ array: [{ a: 1 }, { a: 2 }] }) }) test('records: find array fields', () => { const form = attach( createForm({ initialValues: { array: [{ a: 1 }, { a: 2 }], }, }) ) attach( form.createArrayField({ name: 'array', }) ) attach( form.createObjectField({ name: '0', basePath: 'array', }) ) attach( form.createObjectField({ name: '1', basePath: 'array', }) ) const field0 = attach( form.createField({ name: 'a', basePath: 'array.0', }) ) const field1 = attach( form.createField({ name: 'a', basePath: 'array.1', }) ) expect(field0.records.length).toBe(2) expect(field0.record).toEqual({ a: 1 }) expect(field1.record).toEqual({ a: 2 }) }) test('record: find array nest field record', () => { const form = attach( createForm({ initialValues: { array: [{ a: { b: { c: 1, d: 1 } } }, { a: { b: { c: 2, d: 2 } } }], }, }) ) attach( form.createArrayField({ name: 'array', }) ) attach( form.createObjectField({ name: '0', basePath: 'array', }) ) attach( form.createObjectField({ name: '1', basePath: 'array', }) ) attach( form.createObjectField({ name: 'a', basePath: 'array.0', }) ) attach( form.createObjectField({ name: 'a', basePath: 'array.1', }) ) attach( form.createObjectField({ name: 'b', basePath: 'array.0.a', }) ) attach( form.createObjectField({ name: 'b', basePath: 'array.1.a', }) ) const field0 = attach( form.createField({ name: 'c', basePath: 'array.0.a.b', }) ) const field1 = attach( form.createField({ name: 'c', basePath: 'array.1.a.b', }) ) const field2 = attach( form.createField({ name: 'cc', basePath: 'array.1.a.b.c', }) ) expect(field0.records.length).toBe(2) expect(field1.records.length).toBe(2) expect(field1.records).toEqual([ { a: { b: { c: 1, d: 1 } } }, { a: { b: { c: 2, d: 2 } } }, ]) expect(field0.record).toEqual({ c: 1, d: 1 }) expect(field1.record).toEqual({ c: 2, d: 2 }) expect(field2.record).toEqual({ c: 2, d: 2 }) }) test('record: find array field record', () => { const form = attach( createForm({ initialValues: { array: [1, 2, 3], }, }) ) attach( form.createArrayField({ name: 'array', }) ) const field = attach( form.createField({ basePath: 'array', name: '0', }) ) expect(field.records.length).toBe(3) expect(field.record).toEqual(1) }) test('record: find object field record', () => { const form = attach( createForm({ initialValues: { a: { b: { c: 1, d: 1, }, }, }, }) ) attach( form.createArrayField({ name: 'a', }) ) attach( form.createObjectField({ name: 'b', basePath: 'a', }) ) const fieldc = attach( form.createObjectField({ name: 'c', basePath: 'a.b', }) ) expect(fieldc.records).toEqual(undefined) expect(fieldc.record).toEqual({ c: 1, d: 1, }) }) test('record: find form fields', () => { const form = attach( createForm({ initialValues: { array: [{ a: 1 }, { a: 2 }], }, }) ) const array = attach( form.createArrayField({ name: 'array', }) ) expect(array.record).toEqual({ array: [{ a: 1 }, { a: 2 }] }) }) ``` -------------------------------------------------------------------------------- /docs/guide/index.md: -------------------------------------------------------------------------------- ```markdown # Introduction ## Problem As we all know, the form scene has always been the most complex scene in the front-end and back-end fields. What is the main complexity of it? - There are a lot of fields, how can the performance not deteriorate with the increase of the number of fields? - Field association logic is complex, how to implement complex linkage logic more simply? How to ensure that the form performance is not affected when the field is associated with the field? - One-to-Many (asynchronous) - Many-to-One (asynchronous) - Many-to-Many (asynchronous) - Complex form data management - Form value conversion logic is complex (front and back formats are inconsistent) - The logic of merging synchronous and asynchronous default values is complicated - Cross-form data communication, how to keep the performance from deteriorating with the increase in the number of fields? - Complex form state management - Focusing on the self-incrementing list scenario, how to make the array data move, and the field status can follow the move during the deletion process? - Scene reuse of forms - Query list - Dialog/Drawer form - Step form - Tab form - Dynamic rendering requirements are very strong - Field configuration allows non-professional front-ends to quickly build complex forms - Cross-terminal rendering, a JSON Schema, multi-terminal adaptation - How to describe the layout in the form protocol? - Vertical layout - Horizontal layout - Grid layout - Flexible layout - Free layout - How to describe the logic in the form protocol? So many problems, how to solve them, think about it, But we still have to find a solution,Not only to solve but also to solve elegantly, The Alibaba digital supply chain team, after experiencing a lot of middle and back-office practice and exploration, finally precipitated **Formily form solution**. All the problems mentioned above, after going through UForm to Formily1.x, until Formily2.x finally achieved the degree of **elegant solution**. So how does Formily 2.x solve these problems? ## Solution In order to solve the above problems, we can further refine the problem and come up with a breakthrough direction. ### Accurate Rendering In the React scenario, to realize a form requirement, most of them use setState to realize field data collection. because form data needs to be collected and some linkage requirements are realized.This implementation is very simple and the mental cost is very low, but it also introduces performance problems, because each input will cause all fields to be rendered in full. Although there is diff at the DOM update level, diff also has a computational cost, which wastes a lot of computational resources. In terms of time complexity, the initial rendering of the form is O(n), and the field input is also O(n), which is obviously unreasonable. Historical experience is always helpful to mankind. Decades ago, humans created the MVVM design pattern. The core of this design pattern is to abstract the view model and consume it at the DSL template layer.SL uses a certain dependency collection mechanism, and then uniformly schedules in the view model to ensure that each input is accurately rendered. This is the industrial-grade GUI form! It just so happened that the github community abstracted a state management solution called Mobx for such MVVM models. The core capabilities of [Mobx](https://github.com/mobxjs/mobx) are its dependency tracking mechanism and the abstraction capabilities of responsive models. Therefore, with the help of Mobx, the O(n) problem in the form field input process can be completely solved, and it can be solved very elegantly. However, during the implementation of Formily 2.x, it was discovered that Mobx still has some problems that are not compatible with Formily's core ideas. In the end, we only can reinvent one wheel,[@formily/reactive](https://reactive.formilyjs.org) which continues the core idea of Mobx. Mention here [react-hook-form](https://github.com/react-hook-form/react-hook-form) , Very popular, known as the industry’s top performance form solution, let’s take a look at its simplest case: ```tsx pure import React from 'react' import ReactDOM from 'react-dom' import { useForm } from 'react-hook-form' function App() { const { register, handleSubmit, errors } = useForm() // initialize the hook const onSubmit = (data) => { console.log(data) } return ( <form onSubmit={handleSubmit(onSubmit)}> <input name="firstname" ref={register} /> {/* register an input */} <input name="lastname" ref={register({ required: true })} /> {errors.lastname && 'Last name is required.'} <input name="age" ref={register({ pattern: /\d+/ })} /> {errors.age && 'Please enter number for age.'} <input type="submit" /> </form> ) } ReactDOM.render(<App />, document.getElementById('root')) ``` Although the value management achieves accurate rendering, when the verification is triggered, the form will still be rendered in full. Because of the update of the errors state, the overall controlled rendering is necessary to achieve synchronization. This is only the full rendering of the verification meeting. In fact, there is linkage. To achieve linkage with react-hook-form, it also requires overall controlled rendering to achieve linkage. Therefore, if you want to truly achieve accurate rendering, it must be Reactive! ### Domain Model As mentioned in the previous question, the linkage of forms is very complicated, including various relationships between fields. Let’s imagine that most form linkages are basically linkages triggered based on the values of certain fields. However, actual business requirements may be sophisticated. It is not only necessary to trigger linkage based on certain field values, but also based on other side-effect values, such as application status, server data status, page URL, internal data of a UI component of a field, and current Other data status of the field itself, some special asynchronous events, etc. Use a picture to describe:  As you can see from the above figure, in order to achieve a linkage relationship, the core is to associate certain state attributes of the field with certain data. Some data here can be external data or own data. For example, the display/hide of a field is associated with certain data, the value of a field is associated with certain data, and the disabling/editing of a field is associated with certain data. Here are three examples. We have actually abstracted it. One of the simplest Field model: ```typescript interface Field { value: any visible: boolean disabled: boolean } ``` Of course, does the Field model only have these 3 attributes? Definitely not, if we want to express a field, then the path of the field must have, Because we want to describe the entire form tree structure, at the same time, we also need to manage the properties of the field corresponding to the UI component. For example, Input and Select have their properties. For example, the placeholder of Input is associated with some data, or the drop-down option of Select is associated with some data, so you can understand it. So, our Field model can look like this: ```typescript interface Field { path: string[] value: any visible: boolean disabled: boolean component: [Component, ComponentProps] } ``` We have added the component attribute, which represents the UI component and UI component attribute corresponding to the field, so that the ability to associate certain data with the field component attribute, or even the field component, is realized. Are there any more? Of course, there are also, such as the outer package container of the field, usually we call it FormItem, which is mainly responsible for the interactive style of the field, such as the field title, the style of error prompts, etc., If we want to include more linkage, such as the linkage between certain data and FormItem, then we have to add the outer package container. There are many other attributes, which are not listed here. From the above ideas, we can see that in order to solve the linkage problem, no matter how abstract we are, the field model will eventually be abstracted. It contains all the states related to the field. As long as these states are manipulated, linkage can be triggered. Regarding accurate rendering, we have determined that we can choose a Reactive solution similar to Mobx. Although it is a reinvention of a wheel, the Reactive model is still very suitable for abstract responsive models. So based on the ability of Reactive, Formily, after constant trial and error and correction, finally designed a truly elegant form model. Such a form model solves the problem of the form domain, so it is also called a domain model. With such a domain model, we can make the linkage of the form enumerable and predictable, which also lays a solid foundation for the linkage of the protocol description to be discussed later. ### Path System The field model in the form domain model was mentioned earlier. If the design is more complete, it is not only a field model, but also a form model as the top-level model. The top-level model manages all the field models, and each field has its own Path. How to find these fields? The linkage relationship mentioned earlier is more of a passive dependency, but in some scenarios, we just need to modify the state of a field based on an asynchronous event action. Here is how to find a field elegantly. The same It has also undergone a lot of trial and error and correction. Formily's original path system @formily/path solves this problem very well. It not only makes the field lookup elegant, but it can also deal with the disgusting problem of inconsistent front-end and back-end data structures through destructuring expressions. ### Life Cycle With the help of Mobx and the path system, we have created a relatively complete form scheme, but after this abstraction, our scheme is like a black box, and the outside world cannot perceive the internal state flow process of the scheme. If you want to implement some logic in a certain process stage, you cannot achieve it. So, here we need another concept, the life cycle. As long as we expose the entire form life cycle as an event hook to the outside world, we can achieve an abstract but flexible form solution. ### Protocol Driven If you want to implement a dynamically configurable form, you must make the form structure serializable. There are many ways to serialize, which can be a UI description protocol based on the UI, or a data description protocol based on the data. Because the form itself is to maintain a copy of data, it is natural that for the form scenario, the data protocol is the most suitable. To describe the data structure, [JSON-Schema](https://json-schema.org/) is now the most popular in the industry. Because the JSON Schema protocol itself has many verification-related attributes, this is naturally associated with form verification. Is the UI description protocol really not suitable for describing forms? No, the UI description protocol is suitable for more general UI expressions. Of course, the description form is not a problem, but it will be more front-end protocol. On the contrary, JSON-Schema is expressible at the back-end model layer, and is more versatile in describing data. Therefore, the two protocols have their own strengths, but in the field of pure forms, JSON-Schema will be more domain-oriented. So, if we choose JSON-Schema, how do we describe the UI and how do we describe the logic? It is not realistic to simply describe the data and output the form pages available for actual business. The solution of [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form) is that data is data and UI is UI. The advantage of this is that each protocol is a very pure protocol, but it brings a large maintenance cost and understanding cost. To develop a form, users need to constantly switch between the two protocols mentally. Therefore, if you look at such a split from a technical perspective, it is very reasonable, but from a product perspective, the split is to throw the cost to the user. Therefore, Formily's form protocol will be more inclined to expand on JSON-Schema. So, how to expand? In order not to pollute the standard JSON-Schema attributes, we uniformly express the extended attributes in the x-\* format: ```json { "type": "string", "title": "String", "description": "This is a string", "x-component": "Input", "x-component-props": { "placeholder": "please enter" } } ``` In this way, the UI protocol and the data protocol are mixed together. As long as there is a unified extension agreement, the responsibilities of the two protocols can still be guaranteed to be single. Then, what if you want to wrap a UI container on certain fields? Here, Formily defines a new schema type called `void`. No stranger to void, there is also void element in W3C specification, and void keyword in js. The former represents virtual elements, and the latter represents virtual pointers. Therefore, in JSON Schema, void is introduced to represent a virtual data node, which means that the node does not occupy the actual data structure. So, we can do this: ```json { "type": "void", "title": "card", "description": "This is a card", "x-component": "Card", "properties": { "string": { "type": "string", "title": "String", "description": "This is a string", "x-component": "Input", "x-component-props": { "placeholder": "please enter" } } } } ``` In this way, a UI container can be described. Because the UI container can be described, we can easily encapsulate a scene-based component, such as FormStep. So how do we describe the linkage between fields? For example, one field needs to control the display and hide of another field. We can do this: ```json { "type": "object", "properties": { "source": { "type": "string", "title": "Source", "x-component": "Input", "x-component-props": { "placeholder": "please enter" } }, "target": { "type": "string", "title": "Target", "x-component": "Input", "x-component-props": { "placeholder": "please enter" }, "x-reactions": [ { "dependencies": ["source"], "when": "{{$deps[0] == '123'}}", "fulfill": { "state": { "visible": true } }, "otherwise": { "state": { "visible": false } } } ] } } } ``` The target field is described with the help of `x-reactions`, which depends on the value of the source field. If the value is `'123'`, the target field is displayed, otherwise it is hidden. This linkage method is a passive linkage. What if we want to achieve active linkage ? It can be like this: ```json { "type": "object", "properties": { "source": { "type": "string", "title": "Source", "x-component": "Input", "x-component-props": { "placeholder": "please enter" }, "x-reactions": [ { "when": "{{$self.value == '123'}}", "target": "target", "fulfill": { "state": { "visible": true } }, "otherwise": { "state": { "visible": false } } } ] }, "target": { "type": "string", "title": "Target", "x-component": "Input", "x-component-props": { "placeholder": "please enter" } } } } ``` Just change the location of `x-reactions`, put it on the source field, and then specify a target. It can be seen that our linkage is actually based on: - condition - Condition-satisfied action - Unsatisfied action To achieve. Because the internal state management uses the [@formily/reactive](https://reactive.formilyjs.org) solution similar to Mobx, Formily easily realizes passive and active linkage scenarios, covering most business needs. Therefore, our form can be described by protocol, and it can be configurable no matter how complicated the layout is or the linkage is very complicated. ### Layered Architecture I talked about the solutions to various problems at the beginning, so how do we design now to make Formily more self-consistent and elegant?  This picture mainly divides Formily into the kernel layer, UI bridge layer, extended component layer, and configuration application layer. The kernel layer is UI-independent. It ensures that the logic and state of user management are not coupled to any framework. This has several advantages: - Logic and UI framework are decoupled, and framework-level migration will be done in the future, without extensive refactoring of business code. - The learning cost is uniform. If the user uses @formily/react, the business will be migrated to @formily/vue in the future, and the user does not need to learn again. JSON Schema exists independently and is consumed by the UI bridging layer, ensuring the absolute consistency of protocol drivers under different UI frameworks, and there is no need to repeatedly implement protocol parsing logic. Extend the component layer to provide a series of form scene components to ensure that users can use it out of the box. No need to spend a lot of time for secondary development. ## Competitive Product Comparison ```tsx /** * inline: true */ import React from 'react' import { Table, Tooltip } from 'antd' import { QuestionCircleOutlined } from '@ant-design/icons' const text = (content, tooltips) => { if (tooltips) { return ( <div> {content} <Tooltip title={tooltips}> <QuestionCircleOutlined style={{ marginLeft: 3 }} /> </Tooltip> </div> ) } return content } const dataSource = [ { feature: 'Custom component access cost', antd: '4.x low access cost', fusion: 'high', formik: 'low', finalForm: 'low', schemaForm: text('high', 'Because of coupling bootstrap'), hookForm: text('high', 'Because of coupling React Ref'), 'formily1.x': 'low', 'formily2.x': 'low', }, { feature: 'performance', antd: text( '4.x performance is better', 'Only solved the value synchronization and accurate rendering' ), fusion: 'bad', formik: 'bad', finalForm: text( 'better', 'But only solved the value synchronization and accurate rendering' ), schemaForm: 'bad', hookForm: text( 'good', 'But only solved the value synchronization and accurate rendering' ), 'formily1.x': text( 'excellent', 'Can solve the precise rendering in the linkage process' ), 'formily2.x': text( 'excellent', 'Can solve the precise rendering in the linkage process' ), }, { feature: 'Whether to support dynamic rendering', antd: 'no', fusion: 'no', formik: 'no', finalForm: 'no', schemaForm: 'yes', hookForm: 'no', 'formily1.x': 'yes', 'formily2.x': 'yes', }, { feature: 'Whether to use out of the box', antd: 'yes', fusion: 'yes', formik: 'no', finalForm: 'no', schemaForm: 'yes', hookForm: 'no', 'formily1.x': 'yes', 'formily2.x': 'yes', }, { feature: 'Whether to support cross-terminal', antd: 'no', fusion: 'no', formik: 'no', finalForm: 'no', schemaForm: 'no', hookForm: 'no', 'formily1.x': 'yes', 'formily2.x': 'yes', }, { feature: 'Development efficiency', antd: 'general', fusion: 'generalv', formik: 'general', finalForm: 'general', schemaForm: text( 'low', 'Source code development requires manual maintenance of JSON' ), hookForm: 'general', 'formily1.x': 'high', 'formily2.x': 'high', }, { feature: 'Learning cost', antd: 'easy', fusion: 'easy', formik: 'easy', finalForm: 'hard', schemaForm: 'hard', hookForm: 'easy', 'formily1.x': 'very hard', 'formily2.x': text('hard', 'The concept is drastically reduced'), }, { feature: 'View code maintainability', antd: text('bad', 'Lots of conditional expressions'), fusion: text('bad', 'Lots of conditional expressions'), formik: text('bad', 'Lots of conditional expressions'), finalForm: text('bad', 'Lots of conditional expressions'), schemaForm: 'good', hookForm: text('bad', 'Lots of conditional expressions'), 'formily1.x': 'good', 'formily2.x': 'good', }, { feature: 'Scenario-based packaging capabilities', antd: 'no', fusion: 'no', formik: 'no', finalForm: 'no', schemaForm: 'yes', hookForm: 'no', 'formily1.x': 'yes', 'formily2.x': 'yes', }, { feature: 'Whether to support form preview', antd: 'no', fusion: 'yes', formik: 'no', finalForm: 'no', schemaForm: 'no', hookForm: 'no', 'formily1.x': 'yes', 'formily2.x': 'yes', }, ] export default () => { return ( <Table dataSource={dataSource} pagination={false} bordered scroll={{ x: 1600 }} size="small" > <Table.Column title="ability" dataIndex="feature" width={160} /> <Table.Column title="Ant Design Form" dataIndex="antd" width={160} /> <Table.Column title="Fusion Form" dataIndex="fusion" width={160} /> <Table.Column title="Formik" dataIndex="formik" width={160} /> <Table.Column title="React Final Form" dataIndex="finalForm" width={160} /> <Table.Column title="React Schema Form" dataIndex="schemaForm" width={160} /> <Table.Column title="React Hook Form" dataIndex="hookForm" width={160} /> <Table.Column title="Formily1.x" dataIndex="formily1.x" width={160} /> <Table.Column title="Formily2.x" dataIndex="formily2.x" width={160} /> </Table> ) } ``` ## Core Advantages - high performance - Out of the box - Linkage logic to achieve high efficiencyv - Cross-terminal capability, logic can be cross-frame, cross-terminal reuse - Dynamic rendering capability ## Core Disadvantage - The learning cost is relatively high. Although 2.x has already converged a large number of concepts, there is still a certain learning cost. ## Who is using it? - Alibaba - Tencent - ByteDance ## Q/A Q: Now that I have Vue, why do I still need to provide @formily/vue? Answer: Vue is a UI framework. The problem it solves is a wider range of UI problems. Although its reactive ability is outstanding in form scenarios, at least it is more convenient than native React to write forms, but if it is in more complex form scenarios , We still need to do a lot of abstraction and encapsulation, so @formily/vue is to help you do these abstract encapsulation things, really let you develop super-complex form applications efficiently and conveniently. Q: What is the biggest advantage of Formily2.x compared to 1.x? Answer: The cost of learning, yes, the core is to allow users to understand Formily more quickly. We have tried our best to avoid all kinds of obscure logic and boundary problems during the 2.x design process. Q: What is the browser compatibility of Formily 2.x? Answer: IE is not supported, because the implementation of Reactive strongly relies on Proxy. ``` -------------------------------------------------------------------------------- /packages/core/docs/api/models/Field.md: -------------------------------------------------------------------------------- ```markdown --- order: 1 --- # Field Call the Field model returned by [createField](/api/models/form#createfield). All model attributes are listed below. If the attribute is writable, then we can directly refer to it to modify the attribute, and @formily/reactive will respond to trigger the UI update. ## Attributes | Property | Description | Type | Read-only or not | Default value | | -------------- | --------------------------------------------------- | -------------------------------------------------- | ---------------- | ------------- | | initialized | Has the field been initialized | Boolean | No | `false` | | mounted | Is the field mounted | Boolean | No | `false` | | unmounted | Is the field unmounted | Boolean | No | `false` | | address | Field node path | [FormPath](/api/entry/form-path) | Yes | | | path | Field data path | [FormPath](/api/entry/form-path) | Yes | | | title | Field Title | [FieldMessage](#fieldmessage) | No | `""` | | description | Field description | [FieldMessage](#fieldmessage) | No | `""` | | loading | Field loading status | Boolean | No | `false` | | validating | Is the field being validated | Boolean | No | `false` | | modified | Whether the field tree has been manually modified | Boolean | No | `false` | | selfModified | Whether the field has been manually modified | Boolean | No | `false` | | active | Is the field active | Boolean | No | `false` | | visited | Whether the field has been visited | Boolean | No | `false` | | inputValue | Field input value | Any | No | `null` | | inputValues | Field input value collection | Array | No | `[]` | | dataSource | Field data source | Array | No | `[]` | | validator | Field validator | [FieldValidator](#fieldvalidator) | No | `null` | | decorator | field decorator | Any[] | No | `null` | | component | Field component | Any[] | No | `null` | | feedbacks | Field feedback information | [IFieldFeedback](#ifieldfeedback)[] | No | `[]` | | parent | Parent field | [GeneralField](#generalfield) | yes | `null` | | errors | Field all error message(include children) | [IFormFeedback](/api/models/form/#iformfeedback)[] | Yes | `[]` | | warnings | Field all warning message(include children) | [IFormFeedback](/api/models/form/#iformfeedback)[] | Yes | `[]` | | successes | Field all success message(include children) | [IFormFeedback](/api/models/form/#iformfeedback)[] | Yes | `[]` | | valid | Is the all field valid(include children) | Boolean | Yes | `true` | | invalid | Is the all field illegal(include children) | Boolean | Yes | `false` | | value | Field value | Any | No | | | initialValue | Field default value | Any | No | | | display | Field display status | [FieldDisplayTypes](#fielddisplaytypes) | No | `"visible"` | | pattern | Field interaction mode | [FieldPatternTypes](#fieldpatterntypes) | No | `"editable"` | | required | Is the field required | Boolean | No | `false` | | hidden | Whether the field is hidden | Boolean | No | `false` | | visible | Whether the field is displayed | Boolean | No | `true` | | disabled | Whether the field is disabled | Boolean | No | `false` | | readOnly | Is the field read-only | Boolean | No | `false` | | readPretty | Whether the field is in the reading state | Boolean | No | `false` | | editable | Field is editable | Boolean | No | `true` | | validateStatus | Field validation status | [FieldValidateStatus](#fieldvalidatestatus) | yes | `null` | | content | Field content, usually as a child node | any | No | `null` | | data | Field extends properties | Object | No | `null` | | selfErrors | Field own error message | [FieldMessage](#fieldmessage)[] | No | `[]` | | selfWarnings | Field own warning message | [FieldMessage](#fieldmessage)[] | No | `[]` | | selfSuccesses | Success message of the field itself | [FieldMessage](#fieldmessage)[] | No | `[]` | | selfValid | Is the field valid | Boolean | Yes | `true` | | selfInvalid | Is the field itself illegal | Boolean | Yes | `false` | | indexes | collection of field numeric indexes | Number | yes | `-` | | index | field numeric index, take the last index of indexes | Number | Yes | `-` | #### explain in detail **active** Trigger onFocus is true, trigger onBlur is false **visited** Triggered onFocus will always be true **inputValue** Trigger the value collected by onInput **inputValues** Trigger the multi-parameter values collected by onInput **hidden** When true, display is hidden, when false, display is visible **visible** When true, display is visible, when false, display is none ## Method ### setTitle #### Description Set field title #### Signature ```ts interface setTitle { (title?: FieldMessage): void } ``` FieldMessage Reference [FieldMessage](#fieldmessage) ### setDescription #### Description Set field description information #### Signature ```ts interface setDescription { (title?: FieldMessage): void } ``` FieldMessage Reference [FieldMessage](#fieldmessage) ### setDataSource #### Description Set field data source #### Signature ```ts interface setDataSource { (dataSource?: FieldDataSource): void } ``` FieldDataSource Reference [FieldDataSource](#fielddatasource) ### setFeedback #### Description Set field message feedback #### Signature ```ts interface setFeedback { (feedback?: IFieldFeedback): void } ``` IFieldFeedback Reference [IFieldFeedback](#ifieldfeedback) ### setSelfErrors #### Description Set the field error message, here is a feedback update with EffectError as the code, mainly to prevent pollution of the checker result, if you want to force overwrite, you can use setFeedback #### Signature ```ts interface setSelfErrors { (messages?: FieldMessage[]): void } ``` ### setSelfWarnings #### Description Set the field warning information, here is a feedback update with EffectWarning as the code, mainly to prevent pollution of the checker result, if you want to force overwrite, you can use setFeedback #### Signature ```ts interface setSelfWarning { (messages?: FieldMessage[]): void } ``` ### setSelfSuccesses #### Description Set the field success information, here is a feedback update with EffectSuccess as the code, mainly to prevent pollution of the checker result, if you want to force overwrite, you can use setFeedback #### Signature ```ts interface setSelfSuccesses { (messages?: FieldMessage[]): void } ``` ### setValidator #### Description Set field validator #### Signature ```ts interface setValidator { (validator?: FieldValidator): void } ``` FieldValidator Reference [FieldValidator](#fieldvalidator) ### setRequired #### Description Whether the setting field is required #### Signature ```ts interface setRequired { (required?: boolean): void } ``` ### setValidatorRule #### 描述 Set the field validator according to the rules, similar to setRequired #### 签名 ```ts interface setValidatorRule { (ruleName?: string, ruleValue: any): void } ``` ### setValue #### Description Set field value #### Signature ```ts interface setValue { (value?: FieldValue): void } ``` FieldValue Reference [FieldValue](#fieldvalue) ### setInitialValue #### Description Set field default value #### Signature ```ts interface setInitialValue { (initialValue?: FieldValue): void } ``` FieldValue Reference [FieldValue](#fieldvalue) ### setDisplay #### Description Set field display status #### Signature ```ts interface setDisplay { (display?: FieldDisplayTypes): void } ``` FieldDisplayTypes Reference [FieldDisplayTypes](#fielddisplaytypes) ### setPattern #### Description Set field interaction mode #### Signature ```ts interface setPattern { (pattern?: FieldPatternTypes): void } ``` FieldPatternTypes Reference [FieldPatternTypes](#fieldpatterntypes) ### setLoading #### Description Set field loading status #### Signature ```ts interface setLoading { (loading?: boolean): void } ``` ### setValidating #### Description Set field verification status #### Signature ```ts interface setValidating { (validating?: boolean): void } ``` ### setComponent #### Description Set field component #### Signature ```ts interface setComponent { (component?: FieldComponent, props?: any): void } ``` FieldComponent Reference [FieldComponent](#fieldcomponent) ### setComponentProps #### Description Set field component properties #### Signature ```ts interface setComponentProps { (props?: any): void } ``` ### setDecorator #### Description Set field decorator #### Signature ```ts interface setDecorator { (decorator?: FieldDecorator, props?: any): void } ``` FieldDecorator Reference [FieldDecorator](#fielddecorator) ### setDecoratorProps #### Description Set field decorator properties #### Signature ```ts interface setDecoratorProps { (props?: any): void } ``` ### setState #### Description Set field status #### Signature ```ts interface setState { (state: IFieldState): void (callback: (state: IFieldState) => void): void } ``` IFieldState Reference [IFieldState](#ifieldstate) ### getState #### Description Get field status #### Signature ```ts interface getState<T> { (): IFieldState (callback: (state: IFieldState) => T): T } ``` IFieldState Reference [IFieldState](#ifieldstate) ### setData #### Description set field data #### Signature ```ts interface setData { (data: any): void } ``` ### setContent #### Description set field content #### Signature ```ts interface setContent { (content: any): void } ``` ### onInit #### Description Trigger field initialization, no need to call manually #### Signature ```ts interface onInit { (): void } ``` ### onMount #### Description Trigger field mount #### Signature ```ts interface onMount { (): void } ``` ### onUnmount #### Description Trigger field unloading #### Signature ```ts interface onUnmount { (): void } ``` ### onInput #### Description Trigger field entry #### Signature ```ts interface onInput { (...args: any[]): Promise<void> } ``` ### onFocus #### Description Trigger field focus #### Signature ```ts interface onFocus { (...args: any[]): Promise<void> } ``` ### onBlur #### Description Trigger field out of focus #### Signature ```ts interface onBlur { (...args: any[]): Promise<void> } ``` ### submit #### describe Trigger field submission (including all sub-nodes, this API is mainly used in sub-form scenarios) #### sign ```ts interface submit<T> { (): Promise<Field['value']> (onSubmit?: (values: Field['value']) => Promise<T> | void): Promise<T> } ``` ### validate #### Description Trigger field verification(Contains all sub-nodes, this API is mainly used in sub-form scenarios) #### Signature ```ts interface validate { (triggerType?: 'onInput' | 'onFocus' | 'onBlur'): Promise<IValidateResults> } ``` IValidateResults Reference [IValidateResults](#ivalidateresults) ### reset #### Description Trigger field reset(Contains all sub-nodes, this API is mainly used in sub-form scenarios), if verification is set, then the returned result is the verification result #### Signature ```ts interface reset { (options?: IFieldResetOptions): Promise<IValidateResults> } ``` IFieldResetOptions Reference [IFieldResetOptions](#ifieldresetoptions) IValidateResults Reference [IValidateResults](#ivalidateresults) ### query #### Description Query field, you can query adjacent fields based on the current field #### Signature ```ts interface query { (pattern: FormPathPattern): Query } ``` FormPathPattern API Reference [FormPath](/api/entry/form-path#formpathpattern) Query object API reference [Query](/api/models/query) ### queryFeedbacks #### Description Query the feedback information of the current field #### Signature ```ts interface queryFeedbacks { (search: ISearchFeedback): IFieldFeedback[] } ``` ISearchFeedback Reference [ISearchFeedback](/api/models/field#isearchfeedback) IFieldFeedback Reference [IFieldFeedback](#ifieldfeedback) ### dispose #### Description Release observer, no need to release manually by default #### Signature ```ts interface dispose { (): void } ``` ### destroy #### Description Release observer, and remove current field model #### Signature ```ts interface destroy { (): void } ``` ### match #### Description Match fields based on path #### Signature ```ts interface match { (pattern: FormPathPattern): boolean } ``` FormPathPattern API Reference [FormPath](/api/entry/form-path#formpathpattern) ### inject #### Description Inject executable methods into field models #### Signature ```ts interface inject { (actions: Record<string, (...args: any[]) => any>): void } ``` ### invoke #### Description Invoke an executable method injected by the field model via inject #### Signature ```ts interface invoke { (name: string, ...args: any[]): any } ``` ## Types of <Alert> Note: If you want to manually consume the type, just export it directly from the package module </Alert> ### FieldValidator Field validator, the type is more complicated and needs to be digested carefully by the user ```ts //String format validator type ValidatorFormats = | 'url' | 'email' | 'ipv6' | 'ipv4' | 'number' | 'integer' | 'idcard' | 'qq' | 'phone' | 'money' | 'zh' | 'date' | 'zip' | (string & {}) //Other format validators need to be registered through registerValidateFormats //Object type verification result interface IValidateResult { type: 'error' | 'warning' | 'success' | (string & {}) message: string } //Object validator interface IValidatorRules<Context = any> { triggerType?: 'onInput' | 'onFocus' | 'onBlur' format?: ValidatorFormats validator?: ValidatorFunction<Context> required?: boolean pattern?: RegExp | string max?: number maximum?: number exclusiveMaximum?: number exclusiveMinimum?: number minimum?: number min?: number len?: number whitespace?: boolean enum?: any[] const?: any multipleOf?: number uniqueItems?: boolean maxProperties?: number minProperties?: number maxItems?: number maxLength?: number minItems?: number minLength?: number message?: string [key: string]: any //Other attributes need to be registered through registerValidateRules } //Function type validator check result type type ValidatorFunctionResponse = null | string | boolean | IValidateResult //Functional validator type ValidatorFunction<Context = any> = ( value: any, rule: IValidatorRules<Context>, ctx: Context ) => ValidatorFunctionResponse | Promise<ValidatorFunctionResponse> | null //Non-array validator type ValidatorDescription = | ValidatorFormats | ValidatorFunction<Context> | IValidatorRules<Context> //Array type validator type MultiValidator<Context = any> = ValidatorDescription<Context>[] type FieldValidator<Context = any> = | ValidatorDescription<Context> | MultiValidator<Context> ``` ### FieldMessage ```ts type FieldMessage = string | JSXElement ``` If under the UI framework that supports JSX, we can directly pass the Node of JSX, otherwise, we can only pass the string ### FieldDataSource ```ts type FieldDataSource<ValueType> = Array<{ label: string | JSXElement value: ValueType [key: string]: any }> ``` The field data source is actually an array. The form of the content is determined by the user, but we recommend that users express the data source in the form of label/value. It should be noted here that if it is to be used in the UI framework, it is not set directly. To be effective, the dataSource property must be bound to a specific UI component to be effective. For example, using @formily/react, if you want to bind the state, you can use the connect function, or you can directly get the field instance through useField in the component. consumption. ### FieldValue The field value type is actually the `Any` type, but it is important to mention that if it is a mandatory array type in ArrayField, it is a mandatory object type in ObjectField. ### FieldComponent ```ts type FieldComponent = string | JSXComponentConstructor ``` Field component, if we use it in a framework that supports JSX, FieldComponent recommends to store the JSX component reference directly, otherwise it can store a component identification string and distribute it during actual rendering. ### FieldDecorator ```ts type FieldDecorator = string | JSXComponentConstructor ``` Field decorator, if we use it in a framework that supports JSX, FieldDecorator recommends to store the JSX component reference directly, otherwise it can store a component identification string and distribute it during actual rendering. ### FieldReaction ```ts type FieldReaction = (field: GeneralField) => void ``` ### FieldDisplayTypes ```ts type FieldDisplayTypes = 'none' | 'hidden' | 'visible' ``` ### FieldPatternTypes ```ts type FieldPatternTypes = 'editable' | 'disabled' | 'readOnly' | 'readPretty' ``` ### FieldValidateStatus ```ts type FieldValidateStatus = 'error' | 'warning' | 'success' | 'validating' ``` ### GeneralField ```ts type GeneralField = Field | VoidField | ArrayField | ObjectField ``` VoidField Reference [VoidField](/api/models/void-field) ArrayField Reference [ArrayField](/api/models/array-field) ObjectField Reference [ObjectField](/api/models/object-field) ### IFieldFeedback ```ts interface IFieldFeedback { triggerType?: 'onInput' | 'onFocus' | 'onBlur' //Verify the trigger type type?: 'error' | 'success' | 'warning' //feedback type code?: //Feedback code | 'ValidateError' | 'ValidateSuccess' | 'ValidateWarning' | 'EffectError' | 'EffectSuccess' | 'EffectWarning' messages?: string[] //Feedback message } ``` ### ISearchFeedback ```ts interface ISearchFeedback { triggerType?: 'onInput' | 'onFocus' | 'onBlur' //Verify the trigger type type?: 'error' | 'success' | 'warning' //feedback type code?: //Feedback code | 'ValidateError' | 'ValidateSuccess' | 'ValidateWarning' | 'EffectError' | 'EffectSuccess' | 'EffectWarning' address?: FormPathPattern path?: FormPathPattern messages?: string[] } ``` ### IFieldState ```ts interface IFieldState { hidden?: boolean visible?: boolean editable?: boolean readOnly?: boolean disabled?: boolean readPretty?: boolean title?: any description?: any loading?: boolean validating?: boolean modified?: boolean active?: boolean visited?: boolean inputValue?: FieldValue inputValues?: any[] initialized?: boolean dataSource?: FieldDataSource mounted?: boolean unmounted?: boolean validator?: FieldValidator decorator?: FieldDecorator component?: FieldComponent readonly parent?: GeneralField errors?: FieldMessage[] warnings?: FieldMessage[] successes?: FieldMessage[] readonly valid?: boolean readonly invalid?: boolean value?: FieldValue initialValue?: FieldValue display?: FieldDisplayTypes pattern?: FieldPatternTypes required?: boolean readonly validateStatus?: 'error' | 'success' | 'warning' | 'validating' } ``` ### IGeneralFieldState ```ts type IGeneralFieldState = IFieldState & IVoidFieldState ``` IVoidFieldState Reference [IVoidFieldState](/api/models/void-field#ivoidfieldstate) ### IFieldResetOptions ```ts interface IFieldResetOptions { forceClear?: boolean //Whether to force clear validate?: boolean //Whether to verify } ``` ### IValidateResults ```ts interface IValidateResults { error?: string[] warning?: string[] success?: string[] } ``` > Formily Typescript type convention > > - Simple non-object data types or Union data types use type to define the type, and cannot start with an uppercase `I` character > - Simple object types use interface to define the type uniformly, and start with an uppercase `I` character. If there are combinations of different interfaces (Intersection or Extends), use type to define the type, and also start with an uppercase `I` character ``` -------------------------------------------------------------------------------- /packages/shared/src/__tests__/index.spec.ts: -------------------------------------------------------------------------------- ```typescript import moment from 'moment' import { Map as ImmutableMap } from 'immutable' import { isEqual } from '../compare' import { toArr, every, move, some, findIndex, find, includes, map, reduce, } from '../array' import { clone, shallowClone } from '../clone' import { lowerCase } from '../case' import { deprecate } from '../deprecate' import { globalThisPolyfill } from '../global' import { isValid, isEmpty } from '../isEmpty' import { stringLength } from '../string' import { Subscribable } from '../subscribable' import { lazyMerge, merge } from '../merge' import { instOf } from '../instanceof' import { isFn, isHTMLElement, isNumberLike, isReactElement, isMap, isWeakMap, isWeakSet, isSet, } from '../checkers' import { defaults } from '../defaults' import { applyMiddleware } from '../middleware' const sleep = (d = 100) => new Promise((resolve) => setTimeout(resolve, d)) describe('array', () => { test('toArr', () => { expect(isEqual(toArr([123]), [123])).toBeTruthy() expect(isEqual(toArr(123), [123])).toBeTruthy() expect(isEqual(toArr(null), [])).toBeTruthy() }) test('some', () => { const values1 = [1, 2, 3, 4, 5] const values2 = [] const values3 = { a: 1, b: 2, c: 3 } const values4 = {} expect(some(values1, (item) => item === 3)).toBeTruthy() expect(some(values1, (item) => item === 6)).toBeFalsy() expect(some(values2, () => true)).toBeFalsy() expect(some(values2, () => false)).toBeFalsy() expect(some(values3, (item) => item === 3)).toBeTruthy() expect(some(values3, (item) => item === 6)).toBeFalsy() expect(some(values4, () => true)).toBeFalsy() expect(some(values4, () => false)).toBeFalsy() }) test('every', () => { const values1 = [1, 2, 3, 4, 5] const values2 = [] const values3 = { a: 1, b: 2, c: 3 } const values4 = {} expect(every(values1, (item) => item < 6)).toBeTruthy() expect(every(values1, (item) => item < 3)).toBeFalsy() expect(every(values2, () => true)).toBeTruthy() expect(every(values2, () => false)).toBeTruthy() expect(every(values2, () => false)).toBeTruthy() expect(every(values3, (item) => item < 6)).toBeTruthy() expect(every(values3, (item) => item < 3)).toBeFalsy() expect(every(values4, () => false)).toBeTruthy() expect(every(values4, () => false)).toBeTruthy() }) test('findIndex', () => { const value = [1, 2, 3, 4, 5] expect( isEqual( findIndex(value, (item) => item > 3), 3 ) ).toBeTruthy() expect( isEqual( findIndex(value, (item) => item < 3, true), 1 ) ).toBeTruthy() expect( isEqual( findIndex(value, (item) => item > 6), -1 ) ).toBeTruthy() }) test('find', () => { const value = [1, 2, 3, 4, 5] expect( isEqual( find(value, (item) => item > 3), 4 ) ).toBeTruthy() expect( isEqual( find(value, (item) => item < 3, true), 2 ) ).toBeTruthy() expect( isEqual( find(value, (item) => item > 6), void 0 ) ).toBeTruthy() }) test('includes', () => { const value = [1, 2, 3, 4, 5] expect(includes(value, 3)).toBeTruthy() expect(includes(value, 6)).toBeFalsy() expect(includes('some test string', 'test')).toBeTruthy() expect(includes('some test string', 'test2')).toBeFalsy() }) test('map', () => { const value = [1, 2, 3, 4, 5] const stringVal = 'some test string' const obj = { k1: 'v1', k2: 'v2' } expect( isEqual( map(value, (item) => item + 1, true), [6, 5, 4, 3, 2] ) ).toBeTruthy() expect( isEqual( map(stringVal, (item) => item), stringVal.split('') ) ).toBeTruthy() expect( isEqual( map(obj, (item) => `${item}-copy`), { k1: 'v1-copy', k2: 'v2-copy' } ) ).toBeTruthy() }) test('reduce', () => { const value = [1, 2, 3, 4, 5] expect( isEqual( reduce(value, (acc, item) => acc + item, 0, true), 15 ) ).toBeTruthy() }) }) describe('case', () => { test('lowercase', () => { expect(lowerCase('SOME_UPPER_CASE_TEXT')).toEqual('some_upper_case_text') expect(lowerCase('')).toEqual('') }) }) describe('compare', () => { // base expect(isEqual('some test string', 'some test string')).toBeTruthy() // array expect( isEqual([{ k1: 'v1' }, { k2: 'v2' }], [{ k1: 'v1' }, { k2: 'v2' }]) ).toBeTruthy() expect(isEqual([{ k1: 'v1' }, { k2: 'v2' }], [{ k1: 'v1' }])).toBeFalsy() // moment const momentA = moment('2019-11-11', 'YYYY-MM-DD') const momentB = moment('2019-11-10', 'YYYY-MM-DD') expect(isEqual(momentA, {})).toBeFalsy() expect(isEqual(momentA, moment('2019-11-11', 'YYYY-MM-DD'))).toBeTruthy() expect(isEqual(momentA, momentB)).toBeFalsy() // immutable const immutableA = ImmutableMap({ key: 'val' }) const immutableB = ImmutableMap({ key1: 'val1' }) expect(isEqual(immutableA, {})).toBeFalsy() expect(isEqual(immutableA, immutableB)).toBeFalsy() // schema // todo // date const dateA = new Date('2019-11-11') const dateB = new Date('2019-11-10') expect(isEqual(dateA, {})).toBeFalsy() expect(isEqual(dateA, dateB)).toBeFalsy() expect(isEqual(dateA, new Date('2019-11-11'))).toBeTruthy() // regexp const regexpA = new RegExp(/test/) const regexpB = new RegExp(/test2/) expect(isEqual(regexpA, {})).toBeFalsy() expect(isEqual(regexpA, new RegExp(/test/))).toBeTruthy() expect(isEqual(regexpA, regexpB)).toBeFalsy() // URL const urlA = new URL('https://formilyjs.org/') const urlB = new URL('https://www.taobao.com') const urlC = new URL('https://formilyjs.org/') expect(isEqual(urlA, urlC)).toBeTruthy() expect(isEqual(urlA, urlB)).toBeFalsy() // object const objA = { key: 'val' } const objB = { key2: 'val2', key3: 'val3' } const objC = { key2: 'val2' } expect(isEqual(objA, { key: 'val' })).toBeTruthy() expect(isEqual(objA, objB)).toBeFalsy() expect(isEqual(objA, objC)).toBeFalsy() expect(isEqual([11, 22], [33, 44])).toBeFalsy() expect(isEqual([11, 22], {})).toBeFalsy() expect(isEqual(new URL('https://aa.test'), {})).toBeFalsy() expect(instOf(new URL('https://aa.test'), 'URL')).toBeTruthy() expect(instOf(new Date(), 'Date')).toBeTruthy() expect( isEqual(new URL('https://aa.test'), new URL('https://aa.test')) ).toBeTruthy() expect( isEqual( { $$typeof: true, _owner: true, aaa: 123, }, { $$typeof: true, _owner: true, aaa: 123, } ) ).toBeTruthy() expect( isEqual( { $$typeof: true, _owner: true, aaa: 123, }, { $$typeof: true, _owner: true, bbb: 123, } ) ).toBeFalsy() expect( isEqual( { $$typeof: true, _owner: true, aaa: 123, }, { $$typeof: true, _owner: true, aaa: 333, } ) ).toBeFalsy() }) describe('clone and compare', () => { test('clone form data', () => { let dd = new Map() dd.set('aaa', { bb: 123 }) let ee = new WeakMap() ee.set({}, 1) let ff = new WeakSet() ff.add({}) let gg = new Set() gg.add(3) let a = { aa: 123123, bb: [{ bb: 111 }, { bb: 222 }], cc: () => { // eslint-disable-next-line no-console console.log('123') }, dd, ee, ff, gg, } let cloned = clone(a) expect(isEqual(cloned, a)).toBeTruthy() expect(a === cloned).toBeFalsy() expect(a.bb[0] === cloned.bb[0]).toBeFalsy() expect(a.dd === cloned.dd).toBeTruthy() expect(a.dd.get('aaa') === cloned.dd.get('aaa')).toBeTruthy() expect(a.cc === cloned.cc).toBeTruthy() expect(a.ee === cloned.ee).toBeTruthy() expect(a.ff === cloned.ff).toBeTruthy() expect(a.gg === cloned.gg).toBeTruthy() expect( clone({ aa: { _isAMomentObject: true, }, bb: { _isJSONSchemaObject: true, }, cc: { $$typeof: true, _owner: true, }, dd: { _isBigNumber: true, }, }) ).toEqual({ aa: { _isAMomentObject: true, }, bb: { _isJSONSchemaObject: true, }, cc: { $$typeof: true, _owner: true, }, dd: { _isBigNumber: true, }, }) expect( clone({ toJS() { return 123 }, }) ).toEqual(123) expect( clone({ toJSON() { return 123 }, }) ).toEqual(123) }) test('native clone', () => { const map = new Map() map.set('key', 123) expect(clone(map) === map).toBeTruthy() const weakMap = new WeakMap() const key = {} weakMap.set(key, 123) expect(clone(weakMap) === weakMap).toBeTruthy() const weakSet = new WeakSet() const key2 = {} weakMap.set(key2, 123) expect(clone(weakSet) === weakSet).toBeTruthy() const set = new Set() expect(clone(set) === set).toBeTruthy() const date = new Date() expect(clone(date) === date).toBeTruthy() // @ts-ignore const file = new File([''], 'filename') expect(clone(file) === file).toBeTruthy() const url = new URL('https://test.com') expect(clone(url) === url).toBeTruthy() const regexp = /\d+/ expect(clone(regexp) === regexp).toBeTruthy() const promise = Promise.resolve(1) expect(clone(promise) === promise).toBeTruthy() }) test('shallowClone', () => { expect(shallowClone({ aa: 123 })).toEqual({ aa: 123 }) expect(shallowClone([123])).toEqual([123]) expect(shallowClone(/\d+/)).toEqual(/\d+/) expect(shallowClone({ _isAMomentObject: true })).toEqual({ _isAMomentObject: true, }) expect( shallowClone({ _isBigNumber: true, }) ).toEqual({ _isBigNumber: true, }) expect( shallowClone({ _isJSONSchemaObject: true, }) ).toEqual({ _isJSONSchemaObject: true, }) expect( shallowClone({ $$typeof: true, _owner: true, }) ).toEqual({ $$typeof: true, _owner: true, }) expect( shallowClone({ toJS() { return 123 }, }).toJS() ).toEqual(123) expect( shallowClone({ toJSON() { return 123 }, }).toJSON() ).toEqual(123) expect(shallowClone(1)).toEqual(1) }) }) describe('deprecate', () => { test('deprecate', () => { const test = jest.fn(() => { console.info('### deprecated function called ###') }) const deprecatedFn = jest.fn( deprecate(test, 'Some.Deprecated.Api', 'some deprecated error') ) // arguments - function deprecatedFn() expect(deprecatedFn).toHaveBeenCalledTimes(1) expect(test).toHaveBeenCalledTimes(1) // arguments - string const testDeprecatedFn = jest.fn(() => deprecate('Some.Deprecated.Api', 'some deprecated error') ) testDeprecatedFn() expect(testDeprecatedFn).toHaveBeenCalledTimes(1) // arguments - empty string const testDeprecatedFn2 = jest.fn(() => deprecate('Some.Deprecated.Api')) testDeprecatedFn2() expect(testDeprecatedFn2).toHaveBeenCalledTimes(1) }) }) describe('isEmpty', () => { test('isValid', () => { // val - undefined expect(isValid(undefined)).toBeFalsy() // val - any expect(isValid(!undefined)).toBeTruthy() }) test('isEmpty', () => { // val - null expect(isEmpty(null)).toBeTruthy() // val - boolean expect(isEmpty(true)).toBeFalsy() // val - number expect(isEmpty(2422)).toBeFalsy() // val - string expect(isEmpty('some text')).toBeFalsy() expect(isEmpty('')).toBeTruthy() // val - function const emptyFunc = function () {} const nonEmptyFunc = function (payload) { console.info(payload) } expect(isEmpty(emptyFunc)).toBeTruthy() expect(isEmpty(nonEmptyFunc)).toBeFalsy() // val - arrays expect(isEmpty([])).toBeTruthy() expect(isEmpty([0])).toBeTruthy() expect(isEmpty([''])).toBeTruthy() expect(isEmpty([''], true)).toBeFalsy() expect(isEmpty([0], true)).toBeFalsy() expect(isEmpty([1, 2, 3, 4, 5])).toBeFalsy() expect(isEmpty([0, undefined, null, ''])).toBeTruthy() // val - errors expect(isEmpty(new Error())).toBeTruthy() expect(isEmpty(new Error('some error'))).toBeFalsy() // val - objects // @ts-ignore const file = new File(['foo'], 'filename.txt', { type: 'text/plain' }) // The toString and Object.prototype.toString of the File in the Jest environment are inconsistent file.toString = Object.prototype.toString expect(isEmpty(file)).toBeFalsy() expect(isEmpty(new Map())).toBeTruthy() expect(isEmpty(new Map().set('key', 'val'))).toBeFalsy() expect(isEmpty(new Set())).toBeTruthy() expect(isEmpty(new Set([1, 2]))).toBeFalsy() expect(isEmpty({ key: 'val' })).toBeFalsy() expect(isEmpty({})).toBeTruthy() expect(isEmpty(Symbol())).toBeFalsy() }) }) describe('string', () => { test('stringLength', () => { expect(stringLength('🦄some text')).toEqual(10) }) }) describe('shared Subscribable', () => { test('Subscribable', () => { const cb = jest.fn((payload) => payload) // defualt subscribable const obj = new Subscribable() const handlerIdx = obj.subscribe(cb) expect(handlerIdx).toEqual(1) obj.notify({ key: 'val' }) expect(cb).toHaveBeenCalledTimes(1) expect(cb).toBeCalledWith({ key: 'val' }) obj.unsubscribe(handlerIdx) obj.notify({ key: 'val' }) expect(cb).toHaveBeenCalledTimes(1) // subscribable with custom filter const objWithCustomFilter = new Subscribable() const customFilter = (payload) => { payload.key2 = 'val2' return payload } objWithCustomFilter.subscription = { filter: customFilter, } objWithCustomFilter.subscribe(cb) const handlerIdx2 = objWithCustomFilter.subscribe(cb) expect(handlerIdx2).toEqual(2) objWithCustomFilter.notify({ key4: 'val4' }) expect(cb).toHaveBeenCalledTimes(3) expect(cb).toBeCalledWith({ key4: 'val4', key2: 'val2' }) // subscribable with custom notify const objWithCustomNotify = new Subscribable() const customNotify = jest.fn((payload) => { console.info(payload) return false }) objWithCustomNotify.subscription = { notify: customNotify, } objWithCustomNotify.subscribe(cb) objWithCustomNotify.notify({ key3: 'val3' }) expect(customNotify).toBeCalledTimes(1) objWithCustomNotify.unsubscribe() }) }) describe('types', () => { test('isFn', () => { const normalFunction = function normalFn() {} const asyncFunction = async function asyncFn() {} const generatorFunction = function* generatorFn() {} expect(isFn(() => {})).toBeTruthy() expect(isFn(normalFunction)).toBeTruthy() expect(isFn(asyncFunction)).toBeTruthy() expect(isFn(generatorFunction)).toBeTruthy() expect(isFn('')).toBeFalsy() expect(isFn(undefined)).toBeFalsy() expect(isFn(['🦄'])).toBeFalsy() }) test('isNumberLike', () => { expect(isNumberLike(123)).toBeTruthy() expect(isNumberLike('123')).toBeTruthy() expect(isNumberLike('aa')).toBeFalsy() }) test('isReactElement', () => { expect(isReactElement({ $$typeof: true, _owner: true })).toBeTruthy() }) test('isHTMLElement', () => { // @ts-ignore expect(isHTMLElement(document.createElement('div'))).toBeTruthy() }) test('isMap', () => { expect(isMap(new Map())).toBeTruthy() }) test('isSet', () => { expect(isSet(new Set())).toBeTruthy() }) test('isWeakMap', () => { expect(isWeakMap(new WeakMap())).toBeTruthy() expect(isWeakMap(new Map())).toBeFalsy() }) test('isWeakSet', () => { expect(isWeakSet(new WeakSet())).toBeTruthy() expect(isWeakSet(new Set())).toBeFalsy() }) }) describe('merge', () => { test('assign', () => { const target = { aa: { bb: { cc: { dd: 123, }, }, }, } const source = { aa: { bb: { cc: { ee: '1234', }, }, }, } expect( merge(target, source, { assign: true, }) ).toEqual({ aa: { bb: { cc: { dd: 123, ee: '1234', }, }, }, }) expect(target).toEqual({ aa: { bb: { cc: { dd: 123, ee: '1234', }, }, }, }) expect( merge( { react: { $$typeof: true, _owner: true, aa: 123, }, }, { react: { $$typeof: true, _owner: true, bb: 321, }, }, { assign: true, } ) ).toEqual({ react: { $$typeof: true, _owner: true, bb: 321, }, }) expect( merge( { react: { _isAMomentObject: true, aa: 123, }, }, { react: { _isAMomentObject: true, bb: 321, }, }, { assign: true, } ) ).toEqual({ react: { _isAMomentObject: true, bb: 321, }, }) expect( merge( { react: { _isJSONSchemaObject: true, aa: 123, }, }, { react: { _isJSONSchemaObject: true, bb: 321, }, }, { assign: true, } ) ).toEqual({ react: { _isJSONSchemaObject: true, bb: 321, }, }) expect( merge( { react: { _isBigNumber: true, c: [1, 234567890123], e: 0, s: 1, }, }, { react: { _isBigNumber: true, c: [2, 345678901234], e: 1, s: 0, }, }, { assign: true, } ) ).toEqual({ react: { _isBigNumber: true, c: [2, 345678901234], e: 1, s: 0, }, }) const toJSObj = { toJS: () => {}, bb: 321, } expect( merge( { toJSObj: { toJS: () => {}, aa: 123, }, }, { toJSObj, }, { assign: true, } ) ).toEqual({ toJSObj, }) const toJSONObj = { toJSON: () => {}, bb: 321, } expect( merge( { toJSONObj: { toJS: () => {}, aa: 123, }, }, { toJSONObj, }, { assign: true, } ) ).toEqual({ toJSONObj, }) }) test('empty', () => { expect( merge( { aa: undefined, }, { aa: {}, } ) ).toEqual({ aa: {} }) }) test('clone', () => { const target = { aa: { bb: { cc: { dd: 123, }, }, }, } const source = { aa: { bb: { cc: { ee: '1234', }, }, }, } expect(merge(target, source)).toEqual({ aa: { bb: { cc: { dd: 123, ee: '1234', }, }, }, }) expect(target).toEqual({ aa: { bb: { cc: { dd: 123, }, }, }, }) }) test('merge array', () => { expect(merge([11, 22], [333])).toEqual([11, 22, 333]) }) test('merge custom', () => { expect( merge( { aa: { cc: 123 } }, { aa: { bb: 321 } }, { customMerge() { return (a, b) => ({ ...a, ...b }) }, } ) ).toEqual({ aa: { cc: 123, bb: 321 } }) }) test('merge symbols', () => { const symbol = Symbol('xxx') expect(merge({ [symbol]: 123 }, { aa: 321 })).toEqual({ [symbol]: 123, aa: 321, }) const getOwnPropertySymbols = Object.getOwnPropertySymbols Object.getOwnPropertySymbols = null const mergedObject = merge({ [symbol]: 123 }, { aa: 321 }) Object.getOwnPropertySymbols = getOwnPropertySymbols expect(mergedObject).toEqual({ aa: 321, }) }) test('merge unmatch', () => { expect(merge({ aa: 123 }, [111])).toEqual([111]) }) test('lazy merge', () => { const merge1 = lazyMerge<any>(1, 2) expect(merge1).toBe(2) const merge2 = lazyMerge<any>('123', '321') expect(merge2).toBe('321') const merge3 = lazyMerge<any>(1, undefined) expect(merge3).toBe(1) const merge4 = lazyMerge<any>('123', undefined) expect(merge4).toBe('123') const merge5 = lazyMerge<any>(undefined, '123') expect(merge5).toBe('123') const merge6 = lazyMerge([1, 2, 3], [3, 4]) expect(merge6[0]).toBe(3) expect(merge6[1]).toBe(4) expect(merge6[2]).toBe(3) const merge7 = lazyMerge<any>( { get x() { return 'x' }, }, { get y() { return 'y' }, } ) expect(merge7.x).toBe('x') expect(merge7.y).toBe('y') const effects = { a: 1, b: 2, } const merge8 = lazyMerge<any>( { get x() { return effects.a }, }, { get y() { return effects.b }, } ) expect(merge8.x).toBe(1) expect(merge8.y).toBe(2) effects.a = 123 effects.b = 321 expect(merge8.x).toBe(123) expect(merge8.y).toBe(321) expect(Object.keys(merge8)).toEqual(['x', 'y']) expect('x' in merge8).toBe(true) expect('y' in merge8).toBe(true) expect('z' in merge8).toBe(false) const merge9Source = { a: 1 } const merge9Target = { b: 2 } const merge9 = lazyMerge<any>(merge9Target, merge9Source) merge9.a = 2 merge9.b = 3 merge9.c = 4 expect(merge9Source).toEqual({ a: 2, c: 4 }) expect(merge9Target).toEqual({ b: 3 }) }) }) describe('globalThis', () => { expect(globalThisPolyfill.requestAnimationFrame).not.toBeUndefined() }) describe('instanceof', () => { test('instOf', () => { expect(instOf(123, 123)).toBeFalsy() expect(instOf('123', '123')).toBeFalsy() }) }) test('defaults', () => { const toJSON = () => {} const toJS = () => {} expect( defaults( { aa: { _isAMomentObject: true, }, bb: { _isJSONSchemaObject: true, }, cc: { $$typeof: true, _owner: true, }, dd: { toJSON, }, ee: { toJS, }, ff: { _isBigNumber: true, toJSON, }, }, { aa: { value: 111 }, bb: { value: 222 }, cc: { value: 333 }, dd: { value: 444 }, ee: { value: 555 }, mm: { value: 123 }, ff: { value: { c: [1, 234567890123], e: 0, s: 1, }, }, } ) ).toEqual({ aa: { value: 111 }, bb: { value: 222 }, cc: { value: 333 }, dd: { value: 444 }, ee: { value: 555 }, mm: { value: 123 }, ff: { value: { c: [1, 234567890123], e: 0, s: 1, }, }, }) expect(defaults([1, 2, 3], [0, undefined])).toEqual([0, 2, 3]) const defaultDate = new RegExp('') // @ts-ignore defaultDate._name = 'name' const date2 = new RegExp('') expect(defaults(defaultDate, date2)._name).toEqual('name') }) test('applyMiddleware', async () => { expect(await applyMiddleware(0)).toEqual(0) expect( await applyMiddleware(0, [ (num: number, next) => next(num + 1), (num: number, next) => next(num + 1), (num: number, next) => next(num + 1), ]) ).toEqual(3) expect( await applyMiddleware(0, [ (num: number, next) => next(), (num: number, next) => next(num + 1), (num: number, next) => next(num + 1), ]) ).toEqual(2) const resolved = jest.fn() applyMiddleware(0, [ (num: number, next) => next(num + 1), () => '123', (num: number, next) => next(num + 1), ]).then(resolved) await sleep(16) expect(resolved).toBeCalledTimes(0) }) test('applyMiddleware with error', async () => { try { await applyMiddleware(0, [ () => { throw 'this is error' }, ]) } catch (e) { expect(e).toEqual('this is error') } }) test('move', () => { const array1 = [1] move(array1, 1, 0) expect(array1).toEqual([1]) move(array1, 0, 1) expect(array1).toEqual([1]) move(array1, -1, 1) expect(array1).toEqual([1]) move(array1, 0, 3) expect(array1).toEqual([1]) const array2 = [0, 1, 2] move(array2, 0, 2) expect(array2).toEqual([1, 2, 0]) move(array2, 1, 1) expect(array2).toEqual([1, 2, 0]) const array3 = [0, 1, 2, 3] move(array3, 3, 1) expect(array3).toEqual([0, 3, 1, 2]) }) ```