From 60c8bd57df67d6b5281c897273cbf6c92ffa770f Mon Sep 17 00:00:00 2001 From: Janry Date: Thu, 24 Oct 2019 12:41:02 +0800 Subject: [PATCH 01/99] [V1 UMBRELLA] (#346) --- .eslintrc | 2 +- docs/Examples/antd/List.md | 68 +- docs/Examples/antd/Relations.md | 3 +- docs/Examples/antd/Sample.md | 3 +- docs/Examples/next/List.md | 7 +- docs/Examples/next/Relations.md | 5 +- package.json | 11 +- packages/.eslintrc | 24 +- packages/antd/build.ts | 12 +- packages/antd/package.json | 12 +- packages/antd/src/compat/Form.tsx | 22 + packages/antd/src/compat/FormItem.tsx | 99 + packages/antd/src/compat/context.tsx | 35 + packages/antd/src/compat/index.ts | 10 + packages/antd/src/components/FormBlock.tsx | 22 + packages/antd/src/components/FormCard.tsx | 18 + packages/antd/src/components/FormItemGrid.tsx | 87 + packages/antd/src/components/FormLayout.tsx | 27 + packages/antd/src/components/FormStep.tsx | 98 + packages/antd/src/components/FormTextBox.tsx | 104 + packages/antd/src/components/Select.tsx | 39 + packages/antd/src/components/button.tsx | 81 +- .../antd/src/components/formButtonGroup.tsx | 164 +- packages/antd/src/components/grid.tsx | 175 - packages/antd/src/components/index.ts | 8 + packages/antd/src/components/layout.tsx | 326 -- packages/antd/src/fields/array.tsx | 177 - packages/antd/src/fields/boolean.tsx | 4 +- packages/antd/src/fields/cards.tsx | 267 +- packages/antd/src/fields/checkbox.tsx | 4 +- packages/antd/src/fields/date.tsx | 54 +- packages/antd/src/fields/index.ts | 15 + packages/antd/src/fields/number.tsx | 4 +- packages/antd/src/fields/password.tsx | 243 +- packages/antd/src/fields/radio.tsx | 4 +- packages/antd/src/fields/range.tsx | 4 +- packages/antd/src/fields/rating.tsx | 4 +- packages/antd/src/fields/string.tsx | 4 +- packages/antd/src/fields/table.tsx | 484 +- packages/antd/src/fields/textarea.tsx | 4 +- packages/antd/src/fields/time.tsx | 6 +- packages/antd/src/fields/transfer.tsx | 4 +- packages/antd/src/fields/upload.tsx | 4 +- packages/antd/src/form.tsx | 481 -- packages/antd/src/index.tsx | 57 +- packages/antd/src/locale.ts | 7 - packages/antd/src/shared.ts | 75 + packages/antd/src/type.tsx | 186 - packages/antd/src/types.ts | 90 + packages/antd/src/utils.tsx | 253 - packages/builder-next/README.md | 2 - packages/builder-next/src/index.js | 138 - packages/builder-next/tsconfig.json | 11 - packages/builder/.npmignore | 8 - packages/builder/LISENCE.md | 20 - packages/builder/README.md | 2 - packages/builder/package.json | 62 - packages/builder/src/App.js | 366 -- packages/builder/src/actions/index.js | 154 - .../builder/src/components/editor/index.js | 137 - .../builder/src/components/editor/style.js | 33 - .../builder/src/components/fields/field.js | 103 - .../builder/src/components/fields/index.js | 58 - .../builder/src/components/fields/layout.js | 63 - .../src/components/fields/layoutField.js | 85 - .../builder/src/components/fields/style.js | 87 - .../src/components/globalBtnList/index.js | 115 - packages/builder/src/components/index.js | 8 - .../builder/src/components/preview/card.js | 191 - .../src/components/preview/fieldMiddleware.js | 45 - .../builder/src/components/preview/index.js | 63 - .../builder/src/components/preview/mainBox.js | 133 - .../builder/src/components/preview/style.js | 161 - .../src/components/props/colsDetail.js | 53 - .../props/dataSourceEditor/index.js | 42 - .../props/defaultValueCascader/index.js | 36 - .../fieldAttrEditors/dataSourceEditor.js | 310 - .../fieldAttrEditors/dataSourceEnum.js | 126 - .../defaultValueEditor/arrayDefaultEditor.js | 41 - .../defaultValueEditor/boolDefaultEditor.js | 29 - .../defaultValueEditor/dateDefaultEditor.js | 79 - .../dateRangeDefaultEditor.js | 47 - .../dateTimeDefaultEditor.js | 52 - .../dateTimeRangeDefaultEditor.js | 35 - .../defaultValueGenerator.js | 110 - .../defaultValueEditor/index.js | 118 - .../defaultValueEditor/monthDefaultEditor.js | 80 - .../defaultValueEditor/stringDefaultEditor.js | 39 - .../src/components/props/fileSetting.js | 196 - .../src/components/props/propsSetting.js | 263 - .../builder/src/components/props/style.js | 60 - packages/builder/src/configs/index.js | 7 - .../builder/src/configs/supportConfigList.js | 284 - .../builder/src/configs/supportFieldList.js | 246 - .../src/configs/supportGlobalCfgList.js | 80 - .../builder/src/configs/supportLayoutList.js | 44 - packages/builder/src/configs/theme.js | 57 - packages/builder/src/constants/context.js | 5 - packages/builder/src/constants/itemType.js | 5 - packages/builder/src/demo/index-1-x.js | 147 - packages/builder/src/demo/index.js | 113 - packages/builder/src/index.js | 87 - packages/builder/src/reducers/codemode.js | 12 - packages/builder/src/reducers/componentId.js | 12 - .../builder/src/reducers/componentProps.js | 50 - packages/builder/src/reducers/gbConfig.js | 17 - packages/builder/src/reducers/index.js | 17 - .../builder/src/reducers/initSchemaData.js | 193 - packages/builder/src/reducers/preview.js | 12 - packages/builder/src/style.js | 174 - packages/builder/src/utils/arg.js | 313 - packages/builder/src/utils/baseForm.js | 4 - packages/builder/src/utils/comp.js | 58 - packages/builder/src/utils/lang.js | 143 - packages/builder/src/utils/util.js | 267 - packages/builder/tsconfig.json | 11 - packages/core/README.md | 30 +- packages/core/package.json | 13 +- packages/core/src/__test__/form.spec.js | 46 - .../__snapshots__/index.spec.ts.snap | 5106 +++++++++++++++++ .../core/src/__tests__/field.state.spec.ts | 32 + .../core/src/__tests__/form.state.spec.ts | 32 + packages/core/src/__tests__/graph.spec.ts | 49 + packages/core/src/__tests__/index.spec.ts | 1130 ++++ packages/core/src/__tests__/lifecycle.spec.ts | 13 + packages/core/src/__tests__/model.spec.ts | 71 + .../core/src/__tests__/vfield.state.spec.ts | 32 + packages/core/src/field.ts | 586 -- packages/core/src/form.ts | 847 --- packages/core/src/index.ts | 1050 +++- packages/core/src/path.ts | 81 - packages/core/src/shared/graph.ts | 270 + packages/core/src/shared/lifecycle.ts | 113 + packages/core/src/shared/model.ts | 180 + packages/core/src/shared/subscrible.ts | 29 + packages/core/src/state/field.ts | 187 + packages/core/src/state/form.ts | 68 + packages/core/src/state/virtual-field.ts | 56 + packages/core/src/types.ts | 319 + packages/core/src/utils.ts | 164 - packages/next/README.md | 77 +- packages/next/build.ts | 12 +- packages/next/package.json | 21 +- packages/next/src/compat/Form.tsx | 21 + packages/next/src/compat/FormItem.tsx | 109 + packages/next/src/compat/context.tsx | 35 + packages/next/src/compat/index.ts | 10 + packages/next/src/components/FormBlock.tsx | 26 + packages/next/src/components/FormCard.tsx | 22 + packages/next/src/components/FormItemGrid.tsx | 89 + packages/next/src/components/FormLayout.tsx | 27 + packages/next/src/components/FormStep.tsx | 98 + packages/next/src/components/FormTextBox.tsx | 104 + packages/next/src/components/button.tsx | 90 +- .../next/src/components/formButtonGroup.tsx | 123 +- packages/next/src/components/index.ts | 8 + packages/next/src/components/layout.tsx | 312 - packages/next/src/fields/array.tsx | 180 - packages/next/src/fields/boolean.ts | 4 +- packages/next/src/fields/cards.tsx | 244 +- packages/next/src/fields/checkbox.ts | 4 +- .../next/src/fields/{date.tsx => date.ts} | 4 +- packages/next/src/fields/index.ts | 15 + packages/next/src/fields/number.ts | 4 +- packages/next/src/fields/password.tsx | 188 +- packages/next/src/fields/radio.ts | 4 +- packages/next/src/fields/range.ts | 4 +- packages/next/src/fields/rating.ts | 4 +- packages/next/src/fields/string.ts | 4 +- packages/next/src/fields/table.tsx | 505 +- packages/next/src/fields/textarea.ts | 4 +- packages/next/src/fields/time.ts | 4 +- packages/next/src/fields/transfer.ts | 4 +- packages/next/src/fields/upload.tsx | 4 +- packages/next/src/form.tsx | 434 -- packages/next/src/index.tsx | 58 +- packages/next/src/locale.ts | 7 - packages/next/src/shared.ts | 66 + packages/next/src/type.tsx | 160 - packages/next/src/types.ts | 95 + packages/next/src/utils.tsx | 113 - packages/next/tsconfig.json | 2 +- packages/printer/package.json | 4 +- packages/printer/src/index.js | 2 +- .../.npmignore | 0 .../LESENCE.md | 0 packages/react-schema-renderer/README.md | 2 + .../jest.config.js | 0 .../package.json | 27 +- .../src/__old_tests__}/actions.spec.js | 0 .../src/__old_tests__}/context.spec.js | 0 .../src/__old_tests__}/destruct.spec.js | 2 +- .../src/__old_tests__}/display.spec.js | 0 .../src/__old_tests__}/dynamic.spec.js | 2 +- .../src/__old_tests__}/editable.spec.js | 52 +- .../src/__old_tests__}/effects.spec.js | 0 .../src/__old_tests__}/mutators.spec.js | 0 .../src/__old_tests__}/schema_form.spec.js | 0 .../src/__old_tests__}/traverse.spec.js | 0 .../src/__old_tests__}/utils.spec.js | 0 .../src/__old_tests__}/validate.spec.js | 23 - .../__old_tests__}/validate_relations.spec.js | 0 .../src/__old_tests__}/value.spec.js | 34 +- .../src/__old_tests__}/virtualbox.spec.js | 0 .../src/__old_tests__}/visible.spec.js | 0 .../src/__old_tests__}/x-component.spec.js | 0 .../__snapshots__/markup.spec.tsx.snap | 218 + .../__snapshots__/register.spec.tsx.snap | 259 + .../src/__tests__/field.spec.tsx | 12 + .../src/__tests__/form.spec.tsx | 12 + .../src/__tests__/json-schema.spec.tsx | 12 + .../src/__tests__/markup.spec.tsx | 105 + .../src/__tests__/register.spec.tsx | 166 + .../src/components/SchemaField.tsx | 144 + .../src/components/SchemaForm.tsx | 49 + .../src/components/SchemaMarkup.tsx | 131 + .../src/hooks/useSchemaForm.ts | 71 + packages/react-schema-renderer/src/index.tsx | 16 + .../src/shared/actions.ts | 15 + .../src/shared/connect.ts | 123 + .../src/shared/context.ts | 7 + .../src/shared/registry.ts | 134 + .../src/shared/schema.ts | 392 ++ .../src/shared/virtual-render.tsx | 16 + packages/react-schema-renderer/src/types.ts | 180 + packages/react-schema-renderer/tsconfig.json | 8 + .../.npmignore | 0 .../LICENSE.md | 0 packages/react-shared-components/README.md | 2 + .../jest.config.js | 0 .../package.json | 8 +- .../react-shared-components/src/ArrayList.tsx | 232 + .../src/PasswordStrength.tsx | 156 + .../src/PreviewText.tsx | 31 + packages/react-shared-components/src/index.ts | 4 + packages/react-shared-components/src/types.ts | 88 + .../react-shared-components/tsconfig.json | 8 + packages/{types => react}/LICENSE.md | 0 packages/react/README.md | 299 +- packages/react/package.json | 22 +- packages/react/src/__tests__/actions.spec.tsx | 12 + .../react/src/__tests__/consumer.spec.tsx | 12 + packages/react/src/__tests__/effects.spec.tsx | 12 + packages/react/src/__tests__/field.spec.tsx | 12 + packages/react/src/__tests__/form.spec.tsx | 71 + .../react/src/__tests__/provider.spec.tsx | 12 + packages/react/src/__tests__/spy.spec.tsx | 12 + .../react/src/__tests__/useDirty.spec.tsx | 12 + .../react/src/__tests__/useField.spec.tsx | 12 + .../src/__tests__/useForceUpdate.spec.tsx | 12 + packages/react/src/__tests__/useForm.spec.tsx | 12 + packages/react/src/__tests__/virtual.spec.tsx | 12 + packages/react/src/components/Field.tsx | 53 + packages/react/src/components/Form.tsx | 18 + .../react/src/components/FormConsumer.tsx | 46 + .../react/src/components/FormProvider.tsx | 12 + packages/react/src/components/FormSpy.tsx | 73 + .../react/src/components/VirtualField.tsx | 26 + packages/react/src/context.ts | 7 + packages/react/src/decorators/connect.ts | 203 - packages/react/src/decorators/markup.tsx | 95 - packages/react/src/hooks/useDirty.ts | 17 + packages/react/src/hooks/useField.ts | 76 + packages/react/src/hooks/useForceUpdate.ts | 18 + packages/react/src/hooks/useForm.ts | 128 + packages/react/src/hooks/useVirtualField.ts | 71 + packages/react/src/index.ts | 24 + packages/react/src/index.tsx | 92 - packages/react/src/initialize/index.ts | 13 - packages/react/src/initialize/object.tsx | 25 - packages/react/src/initialize/render.tsx | 17 - packages/react/src/initialize/virtualbox.tsx | 18 - packages/react/src/shared.ts | 270 + packages/react/src/shared/array.tsx | 309 - packages/react/src/shared/broadcast.tsx | 167 - packages/react/src/shared/context.tsx | 16 - packages/react/src/shared/core.tsx | 133 - packages/react/src/shared/mutators.ts | 69 - packages/react/src/shared/virtualbox.tsx | 92 - packages/react/src/state/field.tsx | 225 - packages/react/src/state/form.tsx | 317 - packages/react/src/state/index.tsx | 6 - packages/react/src/type.ts | 67 - packages/react/src/types.ts | 181 + packages/react/src/utils.tsx | 37 - packages/{utils => shared}/.npmignore | 0 packages/{utils => shared}/LICENSE.md | 0 packages/{utils => shared}/README.md | 2 +- packages/shared/jest.config.js | 1 + packages/{types => shared}/package.json | 22 +- packages/shared/src/__tests__/index.spec.ts | 116 + packages/{utils => shared}/src/array.ts | 99 +- packages/{utils => shared}/src/broadcast.ts | 37 +- packages/{utils => shared}/src/case.ts | 0 packages/{utils => shared}/src/clone.ts | 13 +- packages/{utils => shared}/src/compare.ts | 75 +- packages/shared/src/deprecate.ts | 22 + packages/shared/src/global.ts | 19 + packages/shared/src/index.ts | 12 + packages/{utils => shared}/src/isEmpty.ts | 2 + packages/shared/src/merge.ts | 2 + packages/shared/src/path.ts | 3 + .../stringLength.ts => shared/src/string.ts} | 0 packages/{types => shared}/src/types.ts | 0 packages/shared/tsconfig.json | 8 + packages/types/README.md | 2 - packages/types/src/effects.ts | 9 - packages/types/src/field.ts | 68 - packages/types/src/form.ts | 193 - packages/types/src/index.ts | 8 - packages/types/src/path.ts | 3 - packages/types/src/rule.ts | 36 - packages/types/src/schema.ts | 28 - packages/types/src/validator.ts | 11 - packages/types/tsconfig.json | 13 - packages/utils/src/__tests__/index.spec.js | 325 -- packages/utils/src/accessor.ts | 594 -- packages/utils/src/defer.ts | 13 - packages/utils/src/globalThis.ts | 13 - packages/utils/src/index.ts | 13 - packages/utils/src/lru.ts | 310 - packages/utils/src/schema.ts | 135 - packages/utils/tsconfig.json | 13 - packages/validator/package.json | 6 +- .../validator/src/__tests__/index.spec.js | 46 - .../validator/src/__tests__/index.spec.ts | 185 + .../src/{validators/regexp.ts => formats.ts} | 0 packages/validator/src/index.ts | 140 +- packages/validator/src/locale.ts | 54 + packages/validator/src/locale/index.ts | 38 - packages/validator/src/message.ts | 32 +- packages/validator/src/rules.ts | 87 + packages/validator/src/types.ts | 94 + packages/validator/src/utils.ts | 38 - packages/validator/src/validator.ts | 277 + packages/validator/src/validators/custom.ts | 12 - packages/validator/src/validators/format.ts | 33 - packages/validator/src/validators/index.ts | 50 - packages/validator/src/validators/pattern.ts | 37 - packages/validator/src/validators/required.ts | 29 - scripts/jest.base.js | 1 + 341 files changed, 16623 insertions(+), 16767 deletions(-) create mode 100644 packages/antd/src/compat/Form.tsx create mode 100644 packages/antd/src/compat/FormItem.tsx create mode 100644 packages/antd/src/compat/context.tsx create mode 100644 packages/antd/src/compat/index.ts create mode 100644 packages/antd/src/components/FormBlock.tsx create mode 100644 packages/antd/src/components/FormCard.tsx create mode 100644 packages/antd/src/components/FormItemGrid.tsx create mode 100644 packages/antd/src/components/FormLayout.tsx create mode 100644 packages/antd/src/components/FormStep.tsx create mode 100644 packages/antd/src/components/FormTextBox.tsx create mode 100644 packages/antd/src/components/Select.tsx delete mode 100644 packages/antd/src/components/grid.tsx create mode 100644 packages/antd/src/components/index.ts delete mode 100644 packages/antd/src/components/layout.tsx delete mode 100644 packages/antd/src/fields/array.tsx create mode 100644 packages/antd/src/fields/index.ts delete mode 100644 packages/antd/src/form.tsx delete mode 100644 packages/antd/src/locale.ts create mode 100644 packages/antd/src/shared.ts delete mode 100644 packages/antd/src/type.tsx create mode 100644 packages/antd/src/types.ts delete mode 100644 packages/antd/src/utils.tsx delete mode 100644 packages/builder-next/README.md delete mode 100644 packages/builder-next/src/index.js delete mode 100644 packages/builder-next/tsconfig.json delete mode 100644 packages/builder/.npmignore delete mode 100644 packages/builder/LISENCE.md delete mode 100644 packages/builder/README.md delete mode 100644 packages/builder/package.json delete mode 100644 packages/builder/src/App.js delete mode 100644 packages/builder/src/actions/index.js delete mode 100644 packages/builder/src/components/editor/index.js delete mode 100644 packages/builder/src/components/editor/style.js delete mode 100644 packages/builder/src/components/fields/field.js delete mode 100644 packages/builder/src/components/fields/index.js delete mode 100644 packages/builder/src/components/fields/layout.js delete mode 100644 packages/builder/src/components/fields/layoutField.js delete mode 100644 packages/builder/src/components/fields/style.js delete mode 100644 packages/builder/src/components/globalBtnList/index.js delete mode 100644 packages/builder/src/components/index.js delete mode 100644 packages/builder/src/components/preview/card.js delete mode 100644 packages/builder/src/components/preview/fieldMiddleware.js delete mode 100644 packages/builder/src/components/preview/index.js delete mode 100644 packages/builder/src/components/preview/mainBox.js delete mode 100644 packages/builder/src/components/preview/style.js delete mode 100644 packages/builder/src/components/props/colsDetail.js delete mode 100644 packages/builder/src/components/props/dataSourceEditor/index.js delete mode 100644 packages/builder/src/components/props/defaultValueCascader/index.js delete mode 100644 packages/builder/src/components/props/editors/fieldAttrEditors/dataSourceEditor.js delete mode 100644 packages/builder/src/components/props/editors/fieldAttrEditors/dataSourceEnum.js delete mode 100644 packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/arrayDefaultEditor.js delete mode 100644 packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/boolDefaultEditor.js delete mode 100644 packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/dateDefaultEditor.js delete mode 100644 packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/dateRangeDefaultEditor.js delete mode 100644 packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/dateTimeDefaultEditor.js delete mode 100644 packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/dateTimeRangeDefaultEditor.js delete mode 100644 packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/defaultValueGenerator.js delete mode 100644 packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/index.js delete mode 100644 packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/monthDefaultEditor.js delete mode 100644 packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/stringDefaultEditor.js delete mode 100644 packages/builder/src/components/props/fileSetting.js delete mode 100644 packages/builder/src/components/props/propsSetting.js delete mode 100644 packages/builder/src/components/props/style.js delete mode 100644 packages/builder/src/configs/index.js delete mode 100644 packages/builder/src/configs/supportConfigList.js delete mode 100644 packages/builder/src/configs/supportFieldList.js delete mode 100644 packages/builder/src/configs/supportGlobalCfgList.js delete mode 100644 packages/builder/src/configs/supportLayoutList.js delete mode 100644 packages/builder/src/configs/theme.js delete mode 100644 packages/builder/src/constants/context.js delete mode 100644 packages/builder/src/constants/itemType.js delete mode 100644 packages/builder/src/demo/index-1-x.js delete mode 100644 packages/builder/src/demo/index.js delete mode 100644 packages/builder/src/index.js delete mode 100644 packages/builder/src/reducers/codemode.js delete mode 100644 packages/builder/src/reducers/componentId.js delete mode 100644 packages/builder/src/reducers/componentProps.js delete mode 100644 packages/builder/src/reducers/gbConfig.js delete mode 100644 packages/builder/src/reducers/index.js delete mode 100644 packages/builder/src/reducers/initSchemaData.js delete mode 100644 packages/builder/src/reducers/preview.js delete mode 100644 packages/builder/src/style.js delete mode 100644 packages/builder/src/utils/arg.js delete mode 100644 packages/builder/src/utils/baseForm.js delete mode 100644 packages/builder/src/utils/comp.js delete mode 100644 packages/builder/src/utils/lang.js delete mode 100644 packages/builder/src/utils/util.js delete mode 100644 packages/builder/tsconfig.json delete mode 100644 packages/core/src/__test__/form.spec.js create mode 100644 packages/core/src/__tests__/__snapshots__/index.spec.ts.snap create mode 100644 packages/core/src/__tests__/field.state.spec.ts create mode 100644 packages/core/src/__tests__/form.state.spec.ts create mode 100644 packages/core/src/__tests__/graph.spec.ts create mode 100644 packages/core/src/__tests__/index.spec.ts create mode 100644 packages/core/src/__tests__/lifecycle.spec.ts create mode 100644 packages/core/src/__tests__/model.spec.ts create mode 100644 packages/core/src/__tests__/vfield.state.spec.ts delete mode 100644 packages/core/src/field.ts delete mode 100644 packages/core/src/form.ts delete mode 100644 packages/core/src/path.ts create mode 100644 packages/core/src/shared/graph.ts create mode 100644 packages/core/src/shared/lifecycle.ts create mode 100644 packages/core/src/shared/model.ts create mode 100644 packages/core/src/shared/subscrible.ts create mode 100644 packages/core/src/state/field.ts create mode 100644 packages/core/src/state/form.ts create mode 100644 packages/core/src/state/virtual-field.ts create mode 100644 packages/core/src/types.ts delete mode 100644 packages/core/src/utils.ts create mode 100644 packages/next/src/compat/Form.tsx create mode 100644 packages/next/src/compat/FormItem.tsx create mode 100644 packages/next/src/compat/context.tsx create mode 100644 packages/next/src/compat/index.ts create mode 100644 packages/next/src/components/FormBlock.tsx create mode 100644 packages/next/src/components/FormCard.tsx create mode 100644 packages/next/src/components/FormItemGrid.tsx create mode 100644 packages/next/src/components/FormLayout.tsx create mode 100644 packages/next/src/components/FormStep.tsx create mode 100644 packages/next/src/components/FormTextBox.tsx create mode 100644 packages/next/src/components/index.ts delete mode 100644 packages/next/src/components/layout.tsx delete mode 100644 packages/next/src/fields/array.tsx rename packages/next/src/fields/{date.tsx => date.ts} (90%) create mode 100644 packages/next/src/fields/index.ts delete mode 100644 packages/next/src/form.tsx delete mode 100644 packages/next/src/locale.ts create mode 100644 packages/next/src/shared.ts delete mode 100644 packages/next/src/type.tsx create mode 100644 packages/next/src/types.ts delete mode 100644 packages/next/src/utils.tsx rename packages/{builder-next => react-schema-renderer}/.npmignore (100%) rename packages/{react => react-schema-renderer}/LESENCE.md (100%) create mode 100644 packages/react-schema-renderer/README.md rename packages/{react => react-schema-renderer}/jest.config.js (100%) rename packages/{builder-next => react-schema-renderer}/package.json (59%) rename packages/{react/src/__tests__ => react-schema-renderer/src/__old_tests__}/actions.spec.js (100%) rename packages/{react/src/__tests__ => react-schema-renderer/src/__old_tests__}/context.spec.js (100%) rename packages/{react/src/__tests__ => react-schema-renderer/src/__old_tests__}/destruct.spec.js (97%) rename packages/{react/src/__tests__ => react-schema-renderer/src/__old_tests__}/display.spec.js (100%) rename packages/{react/src/__tests__ => react-schema-renderer/src/__old_tests__}/dynamic.spec.js (99%) rename packages/{react/src/__tests__ => react-schema-renderer/src/__old_tests__}/editable.spec.js (89%) rename packages/{react/src/__tests__ => react-schema-renderer/src/__old_tests__}/effects.spec.js (100%) rename packages/{react/src/__tests__ => react-schema-renderer/src/__old_tests__}/mutators.spec.js (100%) rename packages/{react/src/__tests__ => react-schema-renderer/src/__old_tests__}/schema_form.spec.js (100%) rename packages/{react/src/__tests__ => react-schema-renderer/src/__old_tests__}/traverse.spec.js (100%) rename packages/{react/src/__tests__ => react-schema-renderer/src/__old_tests__}/utils.spec.js (100%) rename packages/{react/src/__tests__ => react-schema-renderer/src/__old_tests__}/validate.spec.js (95%) rename packages/{react/src/__tests__ => react-schema-renderer/src/__old_tests__}/validate_relations.spec.js (100%) rename packages/{react/src/__tests__ => react-schema-renderer/src/__old_tests__}/value.spec.js (97%) rename packages/{react/src/__tests__ => react-schema-renderer/src/__old_tests__}/virtualbox.spec.js (100%) rename packages/{react/src/__tests__ => react-schema-renderer/src/__old_tests__}/visible.spec.js (100%) rename packages/{react/src/__tests__ => react-schema-renderer/src/__old_tests__}/x-component.spec.js (100%) create mode 100644 packages/react-schema-renderer/src/__tests__/__snapshots__/markup.spec.tsx.snap create mode 100644 packages/react-schema-renderer/src/__tests__/__snapshots__/register.spec.tsx.snap create mode 100644 packages/react-schema-renderer/src/__tests__/field.spec.tsx create mode 100644 packages/react-schema-renderer/src/__tests__/form.spec.tsx create mode 100644 packages/react-schema-renderer/src/__tests__/json-schema.spec.tsx create mode 100644 packages/react-schema-renderer/src/__tests__/markup.spec.tsx create mode 100644 packages/react-schema-renderer/src/__tests__/register.spec.tsx create mode 100644 packages/react-schema-renderer/src/components/SchemaField.tsx create mode 100644 packages/react-schema-renderer/src/components/SchemaForm.tsx create mode 100644 packages/react-schema-renderer/src/components/SchemaMarkup.tsx create mode 100644 packages/react-schema-renderer/src/hooks/useSchemaForm.ts create mode 100644 packages/react-schema-renderer/src/index.tsx create mode 100644 packages/react-schema-renderer/src/shared/actions.ts create mode 100644 packages/react-schema-renderer/src/shared/connect.ts create mode 100644 packages/react-schema-renderer/src/shared/context.ts create mode 100644 packages/react-schema-renderer/src/shared/registry.ts create mode 100644 packages/react-schema-renderer/src/shared/schema.ts create mode 100644 packages/react-schema-renderer/src/shared/virtual-render.tsx create mode 100644 packages/react-schema-renderer/src/types.ts create mode 100644 packages/react-schema-renderer/tsconfig.json rename packages/{types => react-shared-components}/.npmignore (100%) rename packages/{builder-next => react-shared-components}/LICENSE.md (100%) create mode 100644 packages/react-shared-components/README.md rename packages/{utils => react-shared-components}/jest.config.js (100%) rename packages/{utils => react-shared-components}/package.json (83%) create mode 100644 packages/react-shared-components/src/ArrayList.tsx create mode 100644 packages/react-shared-components/src/PasswordStrength.tsx create mode 100644 packages/react-shared-components/src/PreviewText.tsx create mode 100644 packages/react-shared-components/src/index.ts create mode 100644 packages/react-shared-components/src/types.ts create mode 100644 packages/react-shared-components/tsconfig.json rename packages/{types => react}/LICENSE.md (100%) create mode 100644 packages/react/src/__tests__/actions.spec.tsx create mode 100644 packages/react/src/__tests__/consumer.spec.tsx create mode 100644 packages/react/src/__tests__/effects.spec.tsx create mode 100644 packages/react/src/__tests__/field.spec.tsx create mode 100644 packages/react/src/__tests__/form.spec.tsx create mode 100644 packages/react/src/__tests__/provider.spec.tsx create mode 100644 packages/react/src/__tests__/spy.spec.tsx create mode 100644 packages/react/src/__tests__/useDirty.spec.tsx create mode 100644 packages/react/src/__tests__/useField.spec.tsx create mode 100644 packages/react/src/__tests__/useForceUpdate.spec.tsx create mode 100644 packages/react/src/__tests__/useForm.spec.tsx create mode 100644 packages/react/src/__tests__/virtual.spec.tsx create mode 100644 packages/react/src/components/Field.tsx create mode 100644 packages/react/src/components/Form.tsx create mode 100644 packages/react/src/components/FormConsumer.tsx create mode 100644 packages/react/src/components/FormProvider.tsx create mode 100644 packages/react/src/components/FormSpy.tsx create mode 100644 packages/react/src/components/VirtualField.tsx create mode 100644 packages/react/src/context.ts delete mode 100644 packages/react/src/decorators/connect.ts delete mode 100644 packages/react/src/decorators/markup.tsx create mode 100644 packages/react/src/hooks/useDirty.ts create mode 100644 packages/react/src/hooks/useField.ts create mode 100644 packages/react/src/hooks/useForceUpdate.ts create mode 100644 packages/react/src/hooks/useForm.ts create mode 100644 packages/react/src/hooks/useVirtualField.ts create mode 100644 packages/react/src/index.ts delete mode 100644 packages/react/src/index.tsx delete mode 100644 packages/react/src/initialize/index.ts delete mode 100644 packages/react/src/initialize/object.tsx delete mode 100644 packages/react/src/initialize/render.tsx delete mode 100644 packages/react/src/initialize/virtualbox.tsx create mode 100644 packages/react/src/shared.ts delete mode 100644 packages/react/src/shared/array.tsx delete mode 100644 packages/react/src/shared/broadcast.tsx delete mode 100644 packages/react/src/shared/context.tsx delete mode 100644 packages/react/src/shared/core.tsx delete mode 100644 packages/react/src/shared/mutators.ts delete mode 100644 packages/react/src/shared/virtualbox.tsx delete mode 100644 packages/react/src/state/field.tsx delete mode 100644 packages/react/src/state/form.tsx delete mode 100644 packages/react/src/state/index.tsx delete mode 100644 packages/react/src/type.ts create mode 100644 packages/react/src/types.ts delete mode 100644 packages/react/src/utils.tsx rename packages/{utils => shared}/.npmignore (100%) rename packages/{utils => shared}/LICENSE.md (100%) rename packages/{utils => shared}/README.md (57%) create mode 100644 packages/shared/jest.config.js rename packages/{types => shared}/package.json (68%) create mode 100644 packages/shared/src/__tests__/index.spec.ts rename packages/{utils => shared}/src/array.ts (71%) rename packages/{utils => shared}/src/broadcast.ts (58%) rename packages/{utils => shared}/src/case.ts (100%) rename packages/{utils => shared}/src/clone.ts (88%) rename packages/{utils => shared}/src/compare.ts (60%) create mode 100644 packages/shared/src/deprecate.ts create mode 100644 packages/shared/src/global.ts create mode 100644 packages/shared/src/index.ts rename packages/{utils => shared}/src/isEmpty.ts (96%) create mode 100644 packages/shared/src/merge.ts create mode 100644 packages/shared/src/path.ts rename packages/{utils/src/stringLength.ts => shared/src/string.ts} (100%) rename packages/{types => shared}/src/types.ts (100%) create mode 100644 packages/shared/tsconfig.json delete mode 100644 packages/types/README.md delete mode 100644 packages/types/src/effects.ts delete mode 100644 packages/types/src/field.ts delete mode 100644 packages/types/src/form.ts delete mode 100644 packages/types/src/index.ts delete mode 100644 packages/types/src/path.ts delete mode 100644 packages/types/src/rule.ts delete mode 100644 packages/types/src/schema.ts delete mode 100644 packages/types/src/validator.ts delete mode 100644 packages/types/tsconfig.json delete mode 100644 packages/utils/src/__tests__/index.spec.js delete mode 100644 packages/utils/src/accessor.ts delete mode 100644 packages/utils/src/defer.ts delete mode 100644 packages/utils/src/globalThis.ts delete mode 100644 packages/utils/src/index.ts delete mode 100644 packages/utils/src/lru.ts delete mode 100644 packages/utils/src/schema.ts delete mode 100644 packages/utils/tsconfig.json delete mode 100644 packages/validator/src/__tests__/index.spec.js create mode 100644 packages/validator/src/__tests__/index.spec.ts rename packages/validator/src/{validators/regexp.ts => formats.ts} (100%) create mode 100644 packages/validator/src/locale.ts delete mode 100644 packages/validator/src/locale/index.ts create mode 100644 packages/validator/src/rules.ts create mode 100644 packages/validator/src/types.ts delete mode 100644 packages/validator/src/utils.ts create mode 100644 packages/validator/src/validator.ts delete mode 100644 packages/validator/src/validators/custom.ts delete mode 100644 packages/validator/src/validators/format.ts delete mode 100644 packages/validator/src/validators/index.ts delete mode 100644 packages/validator/src/validators/pattern.ts delete mode 100644 packages/validator/src/validators/required.ts diff --git a/.eslintrc b/.eslintrc index 572c8635275..82a84fcae93 100644 --- a/.eslintrc +++ b/.eslintrc @@ -12,5 +12,5 @@ "ecmaFeatures": { "jsx": true } - } + }, } \ No newline at end of file diff --git a/docs/Examples/antd/List.md b/docs/Examples/antd/List.md index 10ca72e9fde..452896f3f97 100644 --- a/docs/Examples/antd/List.md +++ b/docs/Examples/antd/List.md @@ -3,10 +3,11 @@ > 数组场景,区块型数组,能解决大量字段的聚合输入,但是对于数据的对比化展示,区分 > 度不够明显 -下面对于List场景我们主要封装了 -- Array类型组件 -- Table类型组件 -- Card类型组件 +下面对于 List 场景我们主要封装了 + +- Array 类型组件 +- Table 类型组件 +- Card 类型组件 这些组件你都可以对其做简单的定制来适应你当前的业务需求,比如 @@ -66,8 +67,8 @@ const App = () => { maxItems={3} type="array" x-props={{ - renderAddition:'这是定制的添加文案', - renderRemove:'这是定制的删除文案' + renderAddition: '这是定制的添加文案', + renderRemove: '这是定制的删除文案' }} > @@ -82,11 +83,7 @@ const App = () => { - + @@ -149,11 +146,23 @@ const App = () => ( maxItems={3} type="array" x-component="table" - x-props={{ - renderExtraOperations(){ - return
Hello worldasdasdasdasd
+ x-props={{ + scroll: { x: '200%' }, + renderExtraOperations() { + return ( +
+ Hello worldasdasdasdasd +
+ ) }, - operationsWidth:400 + operationsWidth: 400 }} > @@ -169,7 +178,7 @@ const App = () => ( - + @@ -207,12 +216,17 @@ import 'antd/dist/antd.css' const App = () => ( - + ( - + - + { }) }) $('onFieldChange', 'aa').subscribe(fieldState => { + console.log(fieldState.value) setFieldState('bb', state => { state.visible = !fieldState.value }) @@ -135,7 +136,7 @@ const App = () => { }) }} labelCol={6} - wrapperCol={4} + wrapperCol={12} onSubmit={v => console.log(v)} > diff --git a/docs/Examples/antd/Sample.md b/docs/Examples/antd/Sample.md index eb0ddeca04c..8320628a3a1 100644 --- a/docs/Examples/antd/Sample.md +++ b/docs/Examples/antd/Sample.md @@ -34,7 +34,6 @@ ReactDOM.render( actions={actions} labelCol={7} initialValues={{ - date:'2019-08-01', upload3:[{ downloadURL: "//img.alicdn.com/tfs/TB1n8jfr1uSBuNjy1XcXXcYjFXa-200-200.png", @@ -82,7 +81,7 @@ ReactDOM.render( /> - + ( - + diff --git a/docs/Examples/next/Relations.md b/docs/Examples/next/Relations.md index 24d95d4ba22..8703d9adf56 100644 --- a/docs/Examples/next/Relations.md +++ b/docs/Examples/next/Relations.md @@ -51,6 +51,7 @@ const App = () => { return ( { $('onFormInit').subscribe(() => { setFieldState(FormPath.match('*(gg,hh)'), state => { @@ -131,7 +132,7 @@ const App = () => { wrapperCol={4} onSubmit={v => console.log(v)} > - + {
- + =16.8.0", - "react-dom": ">=16.8.0" + "react-dom": ">=16.8.0", + "@types/styled-components": "^4.1.19" }, "dependencies": { - "@uform/react": "^0.4.3", - "@uform/types": "^0.4.3", - "@uform/utils": "^0.4.3", + "@uform/react-schema-renderer": "^0.4.0", + "@uform/react-shared-components":"^0.4.0", + "@uform/types": "^0.4.0", + "@uform/shared": "^0.4.0", "classnames": "^2.2.6", "moveto": "^1.7.4", "react-stikky": "^0.1.15", diff --git a/packages/antd/src/compat/Form.tsx b/packages/antd/src/compat/Form.tsx new file mode 100644 index 00000000000..521739893c5 --- /dev/null +++ b/packages/antd/src/compat/Form.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import { Form } from 'antd' +import { FormProps } from 'antd/lib/form' +import { IFormItemTopProps } from '../types' +import { FormItemProvider } from './context' +import { normalizeCol } from '../shared' + +export const CompatNextForm: React.FC< + FormProps & IFormItemTopProps +> = props => { + return ( + +
+ + ) +} diff --git a/packages/antd/src/compat/FormItem.tsx b/packages/antd/src/compat/FormItem.tsx new file mode 100644 index 00000000000..67f19610e7f --- /dev/null +++ b/packages/antd/src/compat/FormItem.tsx @@ -0,0 +1,99 @@ +import React, { createContext, useContext } from 'react' +import { Form } from 'antd' +import { useFormItem } from './context' +import { IFormItemTopProps, ICompatItemProps } from '../types' +import { normalizeCol } from '../shared' + +const computeStatus = (props: ICompatItemProps) => { + if (props.loading) { + return 'validating' + } + if (props.invalid) { + return 'error' + } + if (props.warnings && props.warnings.length) { + return 'warning' + } + return '' +} + +const computeHelp = (props: ICompatItemProps) => { + if (props.help) return props.help + const messages = [].concat(props.errors || [], props.warnings || []) + return messages.length ? messages : props.schema && props.schema.description +} + +const computeLabel = (props: ICompatItemProps) => { + if (props.label) return props.label + if (props.schema && props.schema.title) { + return props.schema.title + } +} + +const computeExtra = (props: ICompatItemProps) => { + if (props.extra) return props.extra +} + +function pickProps(obj: T, ...keys: (keyof T)[]): Pick { + const result: Pick = {} as any + for (let i = 0; i < keys.length; i++) { + if (obj[keys[i]] !== undefined) { + result[keys[i]] = obj[keys[i]] + } + } + return result +} + +const computeSchemaExtendProps = ( + props: ICompatItemProps +): IFormItemTopProps => { + if (props.schema) { + return pickProps( + { + ...props.schema.getExtendsItemProps(), + ...props.schema.getExtendsProps() + }, + 'prefix', + 'labelAlign', + 'labelTextAlign', + 'size', + 'labelCol', + 'wrapperCol' + ) + } +} + +const FormItemPropsContext = createContext({}) + +export const FormItemProps = ({ children, ...props }) => ( + + {children} + +) + +export const CompatNextFormItem: React.FC = props => { + const { prefixCls, labelAlign, labelCol, wrapperCol } = useFormItem() + const help = computeHelp(props) + const label = computeLabel(props) + const status = computeStatus(props) + const extra = computeExtra(props) + const itemProps = computeSchemaExtendProps(props) + const outerFormItemProps = useContext(FormItemPropsContext) + return ( + {extra}

: undefined} + {...itemProps} + {...outerFormItemProps} + > + {props.children} +
+ ) +} diff --git a/packages/antd/src/compat/context.tsx b/packages/antd/src/compat/context.tsx new file mode 100644 index 00000000000..e3f38383c73 --- /dev/null +++ b/packages/antd/src/compat/context.tsx @@ -0,0 +1,35 @@ +import React, { createContext, useContext } from 'react' +import { IFormItemTopProps } from '../types' + +const FormItemContext = createContext({}) + +export const FormItemProvider: React.FC = ({ + children, + prefix, + size, + labelAlign, + labelCol, + inline, + labelTextAlign, + wrapperCol +}) => ( + + {children} + +) + +FormItemProvider.displayName = 'FormItemProvider' + +export const useFormItem = () => { + return useContext(FormItemContext) +} diff --git a/packages/antd/src/compat/index.ts b/packages/antd/src/compat/index.ts new file mode 100644 index 00000000000..1eca2fb65ed --- /dev/null +++ b/packages/antd/src/compat/index.ts @@ -0,0 +1,10 @@ +import { + registerFormComponent, + registerFormItemComponent +} from '@uform/react-schema-renderer' +import { CompatNextForm } from './Form' +import { CompatNextFormItem } from './FormItem' + +registerFormComponent(CompatNextForm) + +registerFormItemComponent(CompatNextFormItem) diff --git a/packages/antd/src/components/FormBlock.tsx b/packages/antd/src/components/FormBlock.tsx new file mode 100644 index 00000000000..ab870a2a4cd --- /dev/null +++ b/packages/antd/src/components/FormBlock.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import { createVirtualBox } from '@uform/react-schema-renderer' +import { Card } from 'antd' +import { CardProps } from 'antd/lib/card' +import styled from 'styled-components' + +export const FormBlock = createVirtualBox( + 'block', + styled(({ children, className, ...props }) => { + return ( + + {children} + + ) + })` + margin-bottom: 10px !important; + &.ant-card { + border: none; + box-shadow: none; + } + ` +) diff --git a/packages/antd/src/components/FormCard.tsx b/packages/antd/src/components/FormCard.tsx new file mode 100644 index 00000000000..69c6b071c16 --- /dev/null +++ b/packages/antd/src/components/FormCard.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import { createVirtualBox } from '@uform/react-schema-renderer' +import { Card } from 'antd' +import { CardProps } from 'antd/lib/card' +import styled from 'styled-components' + +export const FormCard = createVirtualBox( + 'card', + styled(({ children, className, ...props }) => { + return ( + + {children} + + ) + })` + margin-bottom: 10px !important; + ` +) diff --git a/packages/antd/src/components/FormItemGrid.tsx b/packages/antd/src/components/FormItemGrid.tsx new file mode 100644 index 00000000000..6f6e7fcd662 --- /dev/null +++ b/packages/antd/src/components/FormItemGrid.tsx @@ -0,0 +1,87 @@ +import React, { Fragment } from 'react' +import { CompatNextFormItem } from '../compat/FormItem' +import { createVirtualBox } from '@uform/react-schema-renderer' +import { toArr } from '@uform/shared' +import { Row, Col } from 'antd' +import { RowProps, ColProps } from 'antd/lib/grid' +import { FormItemProps as ItemProps } from 'antd/lib/form' +import { IFormItemGridProps, IItemProps } from '../types' +import { normalizeCol } from '../shared' + +export const FormItemGrid = createVirtualBox< + React.PropsWithChildren +>('grid', props => { + const { + cols: rawCols, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + title, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + description, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + help, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + extra, + ...selfProps + } = props + const children = toArr(props.children) + const cols = toArr(rawCols).map(col => normalizeCol(col)) + const childNum = children.length + + if (cols.length < childNum) { + let offset: number = childNum - cols.length + let lastSpan: number = + 24 - + cols.reduce((buf, col) => { + return ( + buf + + Number(col.span ? col.span : 0) + + Number(col.offset ? col.offset : 0) + ) + }, 0) + for (let i = 0; i < offset; i++) { + cols.push({ span: Math.floor(lastSpan / offset) }) + } + } + const grids = ( + + {children.reduce((buf, child, key) => { + return child + ? buf.concat( + + {child} + + ) + : buf + }, [])} + + ) + + if (title) { + return ( + + {grids} + + ) + } + return {grids} +}) + +export const FormGridRow = createVirtualBox( + 'grid-row', + props => { + const { title, description, extra } = props + const grids = {props.children} + if (title) { + return ( + + {grids} + + ) + } + return grids + } +) + +export const FormGridCol = createVirtualBox('grid-col', props => { + return {props.children} +}) diff --git a/packages/antd/src/components/FormLayout.tsx b/packages/antd/src/components/FormLayout.tsx new file mode 100644 index 00000000000..35f242caf7e --- /dev/null +++ b/packages/antd/src/components/FormLayout.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { FormItemProvider, useFormItem } from '../compat/context' +import { createVirtualBox } from '@uform/react-schema-renderer' +import cls from 'classnames' +import { IFormItemTopProps } from '../types' + +export const FormLayout = createVirtualBox( + 'layout', + props => { + const { inline } = useFormItem() + const isInline = props.inline || inline + const children = + isInline || props.className || props.style ? ( +
+ {props.children} +
+ ) : ( + props.children + ) + return {children} + } +) diff --git a/packages/antd/src/components/FormStep.tsx b/packages/antd/src/components/FormStep.tsx new file mode 100644 index 00000000000..3f5ee35121a --- /dev/null +++ b/packages/antd/src/components/FormStep.tsx @@ -0,0 +1,98 @@ +import React, { useState, useMemo, useRef } from 'react' +import { + createControllerBox, + ISchemaVirtualFieldComponentProps, + FormPathPattern, + createEffectHook, + createFormActions +} from '@uform/react-schema-renderer' +import { toArr } from '@uform/shared' +import { Observable } from 'rxjs/internal/Observable' +import { Steps } from 'antd' +import { IFormStep } from '../types' + +enum StateMap { + ON_FORM_STEP_NEXT = 'onFormStepNext', + ON_FORM_STEP_PREVIOUS = 'onFormStepPrevious', + ON_FORM_STEP_GO_TO = 'onFormStepGoto', + ON_FORM_STEP_CURRENT_CHANGE = 'onFormStepCurrentChange' +} +const EffectHooks = { + onStepNext$: createEffectHook(StateMap.ON_FORM_STEP_NEXT), + onStepPrevious$: createEffectHook(StateMap.ON_FORM_STEP_PREVIOUS), + onStepGoto$: createEffectHook(StateMap.ON_FORM_STEP_GO_TO), + onStepCurrentChange$: createEffectHook<{ + value: number + preValue: number + }>(StateMap.ON_FORM_STEP_CURRENT_CHANGE) +} + +const effects = (relations: FormPathPattern[]) => { + const actions = createFormActions() + return EffectHooks.onStepCurrentChange$().subscribe(({ value }) => { + relations.forEach((pattern, index) => { + actions.setFieldState(pattern, (state: any) => { + state.display = index === value + }) + }) + }) +} + +type StepComponentExtendsProps = StateMap & { + getEffects: ( + relations: FormPathPattern[] + ) => Observable<{ + value: number + preValue: number + }> +} + +export const FormStep: React.FC & + StepComponentExtendsProps = createControllerBox( + 'step', + ({ props, form }: ISchemaVirtualFieldComponentProps) => { + const [current, setCurrent] = useState(0) + const ref = useRef(current) + const { dataSource, ...stepProps } = props['x-component-props'] || {} + const items = toArr(dataSource) + const update = (cur: number) => { + form.notify(StateMap.ON_FORM_STEP_CURRENT_CHANGE, { + value: cur, + preValue: current + }) + setCurrent(cur) + } + useMemo(() => { + update(ref.current) + form.subscribe(({ type, payload }) => { + switch (type) { + case StateMap.ON_FORM_STEP_NEXT: + update( + ref.current + 1 > items.length - 1 ? ref.current : ref.current + 1 + ) + break + case StateMap.ON_FORM_STEP_PREVIOUS: + update(ref.current - 1 < 0 ? ref.current : ref.current - 1) + break + case StateMap.ON_FORM_STEP_GO_TO: + if (!(payload < 0 || payload > items.length)) { + update(payload) + } + break + } + }) + }, []) + ref.current = current + return ( + + {items.map((props, key) => { + return + })} + + ) + } +) as any + +Object.assign(FormStep, StateMap, EffectHooks, { + effects +}) diff --git a/packages/antd/src/components/FormTextBox.tsx b/packages/antd/src/components/FormTextBox.tsx new file mode 100644 index 00000000000..45cf18233ee --- /dev/null +++ b/packages/antd/src/components/FormTextBox.tsx @@ -0,0 +1,104 @@ +import React, { useRef, useEffect } from 'react' +import { createControllerBox } from '@uform/react-schema-renderer' +import { IFormTextBox } from '../types' +import { toArr } from '@uform/shared' +import { CompatNextFormItem } from '../compat/FormItem' +import styled from 'styled-components' + +export const FormTextBox = createControllerBox( + 'text-box', + styled(({ props, className, children }) => { + const { + title, + help, + text, + name, + extra, + gutter, + style, + ...componentProps + } = Object.assign( + { + gutter: 5 + }, + props['x-component-props'] + ) + const ref: React.RefObject = useRef() + const arrChildren = toArr(children) + const split = text.split('%s') + let index = 0 + useEffect(() => { + if (ref.current) { + const eles = ref.current.querySelectorAll('.text-box-field') + eles.forEach((el: HTMLElement) => { + const ctrl = el.querySelector('.next-form-item-control:first-child') + if (ctrl) { + el.style.width = getComputedStyle(ctrl).width + } + }) + } + }, []) + const newChildren = split.reduce((buf, item, key) => { + return buf.concat( + item ? ( +

+ {item} +

+ ) : null, + arrChildren[key] ? ( +
+ {arrChildren[key]} +
+ ) : null + ) + }, []) + + const textChildren = ( +
+ {newChildren} +
+ ) + + if (!title) return textChildren + + return ( + + {textChildren} + + ) + })` + display: flex; + .text-box-words:nth-child(1) { + margin-left: 0; + } + .text-box-field { + display: inline-block; + } + .next-form-item { + margin-bottom: 0 !important; + } + .preview-text { + text-align: center !important; + } + ` +) diff --git a/packages/antd/src/components/Select.tsx b/packages/antd/src/components/Select.tsx new file mode 100644 index 00000000000..969410d5ec8 --- /dev/null +++ b/packages/antd/src/components/Select.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import { Select as AntSelect } from 'antd' +import { SelectProps as AntSelectProps } from 'antd/lib/select' +import styled from 'styled-components' + +type SelectOption = { + label: React.ReactText + value: any + [key: string]: any +} + +type SelectProps = AntSelectProps & { + dataSource?: SelectOption[] +} + +export const Select: React.FC = styled((props: SelectProps) => { + const { dataSource = [], ...others } = props + const children = dataSource.map((item, index) => { + const { label, value, ...others } = item + return ( + + {label} + + ) + }) + return ( + + {children} + + ) +})` + min-width: 100px; + width: 100%; +` diff --git a/packages/antd/src/components/button.tsx b/packages/antd/src/components/button.tsx index 966e4d6c90e..5d07468e830 100644 --- a/packages/antd/src/components/button.tsx +++ b/packages/antd/src/components/button.tsx @@ -1,38 +1,87 @@ import React from 'react' -import { FormConsumer } from '@uform/react' +import { FormSpy, LifeCycleTypes } from '@uform/react-schema-renderer' import { Button } from 'antd' -import { ISubmitProps } from '../type' +import { ButtonProps } from 'antd/lib/button' +import { ISubmitProps, IResetProps } from '../types' -export const Submit = ({ showLoading, ...props }: ISubmitProps) => { +export const TextButton: React.FC = props => ( + ) }} - + ) } -export const Reset: React.FC> = props => { +Submit.defaultProps = { + showLoading: true +} + +export const Reset: React.FC = ({ + children, + forceClear, + validate, + ...props +}) => { return ( - - {({ reset }) => { + + {({ form }) => { return ( - ) }} - + ) } diff --git a/packages/antd/src/components/formButtonGroup.tsx b/packages/antd/src/components/formButtonGroup.tsx index 2ddcc01dda7..7899c572d23 100644 --- a/packages/antd/src/components/formButtonGroup.tsx +++ b/packages/antd/src/components/formButtonGroup.tsx @@ -1,24 +1,10 @@ -import React, { Component } from 'react' -import ReactDOM from 'react-dom' -import { Row, Col } from './grid' -import { FormLayoutConsumer } from '../form' -import { IFormButtonGroupProps } from '../type' +import React, { useRef } from 'react' +import { Row, Col } from 'antd' import Sticky from 'react-stikky' import cls from 'classnames' import styled from 'styled-components' - -const getAlign = align => { - if (align === 'start' || align === 'end') { - return align - } - if (align === 'left' || align === 'top') { - return 'flex-start' - } - if (align === 'right' || align === 'bottom') { - return 'flex-end' - } - return align -} +import { useFormItem } from '../compat/context' +import { IFormButtonGroupProps } from '../types' export interface IOffset { top: number | string @@ -27,6 +13,13 @@ export interface IOffset { left: number | string } +const getAlign = align => { + if (align === 'start' || align === 'end') return align + if (align === 'left' || align === 'top') return 'flex-start' + if (align === 'right' || align === 'bottom') return 'flex-end' + return align +} + const isElementInViewport = ( rect: ClientRect, { @@ -36,7 +29,7 @@ const isElementInViewport = ( offset?: IOffset | number threshold?: number } = {} -) => { +): boolean => { const { top, right, bottom, left, width, height } = rect const intersection = { t: bottom, @@ -62,74 +55,28 @@ const isElementInViewport = ( ) } -export const FormButtonGroup: React.FC = styled( - class FormButtonGroup extends Component { - public static defaultProps = { - span: 24 - } - - private formNode: HTMLElement - - public render() { - const { sticky, style, className } = this.props - - const content = ( - - {({ inline } = {}) => ( -
- {this.renderChildren()} -
- )} -
- ) - - if (sticky) { - return ( -
- - {({ FormRef } = {}) => { - if (!FormRef) { - return - } - return ( - -
- {content} -
-
- ) - }} -
-
- ) - } - - return content - } - - private renderChildren() { - const { children, itemStyle, offset, span } = this.props +export const FormButtonGroup = styled( + (props: React.PropsWithChildren) => { + const { + span, + zIndex, + sticky, + style, + offset, + className, + children, + triggerDistance, + itemStyle + } = props + const { inline } = useFormItem() + const selfRef = useRef() + const renderChildren = () => { return ( -
+
- -
+ +
{children}
@@ -138,20 +85,53 @@ export const FormButtonGroup: React.FC = styled(
) } - - private getStickyBoundaryHandler(ref) { + const getStickyBoundaryHandler = () => { return () => { - // eslint-disable-next-line react/no-find-dom-node - this.formNode = this.formNode || ReactDOM.findDOMNode(ref.current) - if (this.formNode) { - return isElementInViewport(this.formNode.getBoundingClientRect()) + if (selfRef.current && selfRef.current.parentElement) { + const container = selfRef.current.parentElement + return isElementInViewport(container.getBoundingClientRect()) } return true } } + + const content = ( +
+ {renderChildren()} +
+ ) + + if (sticky) { + return ( +
+ +
+ {content} +
+
+
+ ) + } + + return content } -)` - ${props => +)` + ${(props: IFormButtonGroupProps) => props.align ? `display:flex;justify-content: ${getAlign(props.align)}` : ''} &.is-inline { display: inline-block; diff --git a/packages/antd/src/components/grid.tsx b/packages/antd/src/components/grid.tsx deleted file mode 100644 index e9773c591a1..00000000000 --- a/packages/antd/src/components/grid.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import React, { Component, Children, cloneElement } from 'react' -import cx from 'classnames' -import { toArr } from '@uform/utils' -import { IColProps, IRowProps } from '../type' - -export class Row extends Component { - public static defaultProps = { - prefix: 'ant-', - pure: false, - fixed: false, - gutter: 0, - wrap: false, - component: 'div' - } - - public render() { - /* eslint-disable @typescript-eslint/no-unused-vars */ - const { - prefix, - pure, - wrap, - fixed, - gutter, - fixedWidth, - align, - justify, - hidden, - className, - component: Tag, - children, - ...others - } = this.props - /* eslint-enable @typescript-eslint/no-unused-vars */ - - let hiddenClassObj - if (hidden === true) { - hiddenClassObj = { [`${prefix}row-hidden`]: true } - } else if (typeof hidden === 'string') { - hiddenClassObj = { [`${prefix}row-${hidden}-hidden`]: !!hidden } - } else if (Array.isArray(hidden)) { - hiddenClassObj = hidden.reduce((ret, point) => { - ret[`${prefix}row-${point}-hidden`] = !!point - return ret - }, {}) - } - - const newClassName = cx({ - [`${prefix}row`]: true, - [`${prefix}row-wrap`]: wrap, - [`${prefix}row-fixed`]: fixed, - [`${prefix}row-fixed-${fixedWidth}`]: !!fixedWidth, - [`${prefix}row-justify-${justify}`]: !!justify, - [`${prefix}row-align-${align}`]: !!align, - ...hiddenClassObj, - [className]: !!className - }) - - let newChildren = toArr(children) - const gutterNumber = parseInt(gutter, 10) - - if (gutterNumber !== 0) { - const halfGutterString = `${gutterNumber / 2}px` - others.style = { - marginLeft: `-${halfGutterString}`, - marginRight: `-${halfGutterString}`, - ...(others.style || {}) - } - newChildren = Children.map(children, (child: React.ReactElement) => { - if ( - child && - child.type && - typeof child.type === 'function' && - (child.type as any).isNextCol - ) { - const newChild = cloneElement(child, { - style: { - paddingLeft: halfGutterString, - paddingRight: halfGutterString, - ...(child.props.style || {}) - } - }) - return newChild - } - - return child - }) - } - - return ( - - {newChildren} - - ) - } -} - -const breakPoints = ['xxs', 'xs', 's', 'm', 'l', 'xl'] - -export class Col extends Component { - public static isNextCol = true - - public static defaultProps = { - prefix: 'ant-', - pure: false, - component: 'div' - } - - public render() { - const { - prefix, - pure, - span, - offset, - fixedSpan, - fixedOffset, - hidden, - align, - xxs, - xs, - s, - m, - l, - xl, - component: Tag, - className, - children, - ...others - } = this.props - - const pointClassObj = breakPoints.reduce((ret, point) => { - let pointProps: { span?: string; offset?: string } = {} - if (typeof this.props[point] === 'object') { - pointProps = this.props[point] - } else { - pointProps.span = this.props[point] - } - - ret[`${prefix}col-${point}-${pointProps.span}`] = !!pointProps.span - ret[ - `${prefix}col-${point}-offset-${pointProps.offset}` - ] = !!pointProps.offset - return ret - }, {}) - - let hiddenClassObj - if (hidden === true) { - hiddenClassObj = { [`${prefix}col-hidden`]: true } - } else if (typeof hidden === 'string') { - hiddenClassObj = { [`${prefix}col-${hidden}-hidden`]: !!hidden } - } else if (Array.isArray(hidden)) { - hiddenClassObj = hidden.reduce((ret, point) => { - ret[`${prefix}col-${point}-hidden`] = !!point - return ret - }, {}) - } - - const classes = cx({ - [`${prefix}col`]: true, - [`${prefix}col-${span}`]: !!span, - [`${prefix}col-fixed-${fixedSpan}`]: !!fixedSpan, - [`${prefix}col-offset-${offset}`]: !!offset, - [`${prefix}col-offset-fixed-${fixedOffset}`]: !!fixedOffset, - [`${prefix}col-${align}`]: !!align, - ...pointClassObj, - ...hiddenClassObj, - [className]: className - }) - - return ( - - {children} - - ) - } -} diff --git a/packages/antd/src/components/index.ts b/packages/antd/src/components/index.ts new file mode 100644 index 00000000000..1015143d9c7 --- /dev/null +++ b/packages/antd/src/components/index.ts @@ -0,0 +1,8 @@ +export * from './Button' +export * from './FormButtonGroup' +export * from './FormLayout' +export * from './FormItemGrid' +export * from './FormCard' +export * from './FormBlock' +export * from './FormTextBox' +export * from './FormStep' diff --git a/packages/antd/src/components/layout.tsx b/packages/antd/src/components/layout.tsx deleted file mode 100644 index f87cf4bd801..00000000000 --- a/packages/antd/src/components/layout.tsx +++ /dev/null @@ -1,326 +0,0 @@ -import React, { Component, useEffect, useRef } from 'react' -import { createVirtualBox, createControllerBox } from '@uform/react' -import { toArr } from '@uform/utils' -import { IFormItemGridProps, IFormItemProps } from '@uform/types' -import { Card, Row, Col } from 'antd' -import styled from 'styled-components' -import cls from 'classnames' - -import { FormLayoutConsumer, FormItem, FormLayoutProvider } from '../form' -import { - IFormTextBox, - IFormCardProps, - IFormBlockProps, - IFormLayoutProps, - TFormCardOrFormBlockProps, - IFormItemGridProps as IFormItemGridPropsAlias -} from '../type' - -const normalizeCol = ( - col: { span: number; offset?: number } | number, - defaultValue: { span: number } = { span: 0 } -): { span: number; offset?: number } => { - if (!col) { - return defaultValue - } else { - return typeof col === 'object' ? col : { span: col } - } -} - -export const FormLayoutItem: React.FC = function(props) { - return React.createElement( - FormLayoutConsumer, - {}, - ({ - labelAlign, - labelTextAlign, - labelCol, - wrapperCol, - size, - autoAddColon - }) => { - return React.createElement( - FormItem, - { - labelAlign, - labelTextAlign, - labelCol, - wrapperCol, - autoAddColon, - size, - ...props - }, - props.children - ) - } - ) -} - -export const FormLayout = createVirtualBox( - 'layout', - ({ children, ...props }) => { - return ( - - {value => { - const newValue = { ...value, ...props } - const child = - newValue.inline || newValue.className || newValue.style ? ( -
- {children} -
- ) : ( - children - ) - return ( - {child} - ) - }} -
- ) - } -) - -export const FormItemGrid = createVirtualBox( - 'grid', - class extends Component { - public render() { - const { title } = this.props - if (title) { - return this.renderFormItem(this.renderGrid()) - } else { - return this.renderGrid() - } - } - - private renderFormItem(children) { - const { title, help, name, extra, ...props } = this.props - return React.createElement( - FormLayoutItem, - { - label: title, - noMinHeight: true, - id: name, - extra, - help, - ...props - } as IFormItemGridProps, - children - ) - } - - private renderGrid() { - const { - children: rawChildren, - cols: rawCols, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - title, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - description, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - help, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - extra, - ...props - } = this.props - - const children = toArr(rawChildren) - const childNum = children.length - const cols = toArr(rawCols).map(col => normalizeCol(col)) - - if (cols.length < childNum) { - const offset: number = childNum - cols.length - const lastSpan: number = - 24 - - cols.reduce((buf, col) => { - return ( - buf + - Number(col.span ? col.span : 0) + - Number(col.offset ? col.offset : 0) - ) - }, 0) - - for (let i = 0; i < offset; i++) { - cols.push({ span: Math.floor(lastSpan / offset) }) - } - } - - // cols = toArr(cols).map(col => normalizeCol(col)) - - return ( - - {children.reduce((buf, child, key) => { - return child - ? buf.concat( - - {child} - - ) - : buf - }, [])} - - ) - } - } -) - -export const FormCard = createVirtualBox( - 'card', - styled( - class extends Component { - public static defaultProps = { - // bodyHeight: 'auto' - } - public render() { - const { children, className, ...props } = this.props - return ( - - {children} - - ) - } - } - )` - margin-bottom: 30px; - .ant-card-body { - padding-top: 30px; - padding-bottom: 0 !important; - } - &.ant-card { - display: block; - margin-bottom: 30px; - } - ` -) - -export const FormBlock = createVirtualBox( - 'block', - styled( - class extends Component { - public static defaultProps = { - // bodyHeight: 'auto' - } - - public render() { - const { children, className, ...props } = this.props - return ( - - {children} - - ) - } - } - )` - margin-bottom: 0px; - .ant-card-body { - padding-top: 20px; - padding-bottom: 0 !important; - } - &.ant-card { - border: none; - padding: 0 15px; - padding-bottom: 15px; - display: block; - box-shadow: none; - } - .ant-card-head { - padding: 0 !important; - min-height: 24px; - font-weight: normal; - } - .ant-card-head-title { - padding: 0; - } - ` -) - -export const FormTextBox = createControllerBox( - 'text-box', - styled(({ children, schema, className }) => { - const { title, help, text, name, extra, ...props } = schema['x-props'] - const ref: React.RefObject = useRef() - const arrChildren = toArr(children) - const split = String(text).split('%s') - useEffect(() => { - if (ref.current) { - const eles = ref.current.querySelectorAll('.text-box-field') - eles.forEach((el: HTMLElement) => { - const ctrl = el.querySelector( - '.ant-form-item-control>*:not(.ant-form-item-space)' - ) - if (ctrl) { - el.style.width = getComputedStyle(ctrl).width - } - }) - } - }, []) - - let index = 0 - const newChildren = split.reduce((buf, item, key) => { - return buf.concat( - item ? ( - - {item} - - ) : null, - arrChildren[key] ? ( -
- {arrChildren[key]} -
- ) : null - ) - }, []) - - if (!title) { - return ( -
- {newChildren} -
- ) - } - - return React.createElement( - FormLayoutItem, - { - label: title, - noMinHeight: true, - id: name, - extra, - help, - ...props - }, -
- {newChildren} -
- ) - })` - display: flex; - .text-box-words { - font-size: 14px; - line-height: 34px; - color: #333; - ${props => { - const { editable, schema } = props - const { gutter } = schema['x-props'] - if (!editable) { - return { - margin: 0 - } - } - return { - margin: `0 ${gutter === 0 || gutter ? gutter : 10}px` - } - }} - } - .text-box-words:nth-child(1) { - margin-left: 0; - } - .text-box-field { - display: inline-block; - } - ` -) diff --git a/packages/antd/src/fields/array.tsx b/packages/antd/src/fields/array.tsx deleted file mode 100644 index 52feca6fd41..00000000000 --- a/packages/antd/src/fields/array.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import React from 'react' -import { registerFormField, createArrayField } from '@uform/react' -import { Icon } from 'antd' -import styled, { css } from 'styled-components' - -export const CircleButton = styled['div'].attrs({ className: 'cricle-btn' })` - ${props => (!props.hasText ? 'width:30px; height:30px;' : '')} - margin-right:10px; - border-radius: ${props => (!props.hasText ? '100px' : 'none')}; - border: ${props => (!props.hasText ? '1px solid #eee' : 'none')}; - margin-bottom: 20px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - ${props => - !props.hasText - ? `&:hover{ - background:#f7f4f4; - }` - : ''} - .op-name { - margin-left: 3px; - } -` - -export const TextButton = styled['div'].attrs({ - className: 'ant-btn-text' -})` - display: inline-block; - height: 20px; - line-height: 20px; - cursor: pointer; - .op-name { - margin-left: 4px; - } - ${props => - props.inline && - css` - display: inline-block; - width: auto; - `} -` - -export const ArrayField = createArrayField({ - CircleButton, - TextButton, - AddIcon: () => , - RemoveIcon: () => , - MoveDownIcon: () => , - MoveUpIcon: () => -}) - -registerFormField( - 'array', - styled( - class extends ArrayField { - public render() { - const { className, name, value, renderField } = this.props - const cls = this.getProps('className') - const style = this.getProps('style') - return ( -
- {value.map((item, index) => { - return ( -
-
- {index + 1} -
-
{renderField(index)}
-
- {this.renderRemove(index, item)} - {this.renderMoveDown(index, item)} - {this.renderMoveUp(index)} - {this.renderExtraOperations(index)} -
-
- ) - })} - {this.renderEmpty()} - {value.length > 0 && this.renderAddition()} -
- ) - } - } - )` - border: 1px solid #eee; - min-width: 400px; - .array-item { - padding: 20px; - padding-bottom: 0; - padding-top: 30px; - border-bottom: 1px solid #eee; - position: relative; - &:nth-child(even) { - background: #fafafa; - } - .array-index { - position: absolute; - top: 0; - left: 0; - display: block; - span { - position: absolute; - color: rgb(255, 255, 255); - z-index: 1; - font-size: 12px; - top: 3px; - left: 3px; - line-height: initial; - } - &::after { - content: ''; - display: block; - border-top: 20px solid transparent; - border-left: 20px solid transparent; - border-bottom: 20px solid transparent; - border-right: 20px solid #888; - transform: rotate(45deg); - position: absolute; - z-index: 0; - top: -20px; - left: -20px; - } - } - .array-item-operator { - display: flex; - border-top: 1px solid #eee; - padding-top: 20px; - } - } - .array-empty-wrapper { - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - &.disabled { - cursor: default; - } - .array-empty { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - margin: 20px; - img { - display: block; - height: 80px; - } - .ant-btn-text { - color: #999; - i { - margin-right: 3px; - } - } - } - } - .array-item-wrapper { - margin: 0 -20px; - } - .array-item-addition { - padding: 10px 20px; - line-height: normal !important; - background: #fbfbfb; - .ant-btn-text { - color: #888; - i { - margin-right: 3px; - } - } - } - ` -) diff --git a/packages/antd/src/fields/boolean.tsx b/packages/antd/src/fields/boolean.tsx index eb28ca25b26..cfea566fe57 100644 --- a/packages/antd/src/fields/boolean.tsx +++ b/packages/antd/src/fields/boolean.tsx @@ -1,6 +1,6 @@ import { Switch } from 'antd' -import { connect, registerFormField } from '@uform/react' -import { acceptEnum, mapStyledProps } from '../utils' +import { connect, registerFormField } from '@uform/react-schema-renderer' +import { acceptEnum, mapStyledProps } from '../shared' registerFormField( 'boolean', diff --git a/packages/antd/src/fields/cards.tsx b/packages/antd/src/fields/cards.tsx index a48422b0f22..1b13bb6ba83 100644 --- a/packages/antd/src/fields/cards.tsx +++ b/packages/antd/src/fields/cards.tsx @@ -1,132 +1,132 @@ -import React, { Fragment, ReactElement } from 'react' +import React, { Fragment } from 'react' +import { + registerFormField, + ISchemaFieldComponentProps, + SchemaField +} from '@uform/react-schema-renderer' +import { toArr, isFn } from '@uform/shared' +import { ArrayList } from '@uform/react-shared-components' +import { CircleButton, TextButton } from '../components/Button' +import { Card, Icon } from 'antd' import styled from 'styled-components' -import { registerFormField } from '@uform/react' -import { Card } from 'antd' -import { toArr } from '@uform/utils' -import { ArrayField } from './array' -const FormCardsField = styled( - class extends ArrayField { - public renderOperations(item, index) { - return ( - - {this.renderRemove(index, item)} - {this.renderMoveDown(index, item)} - {this.renderMoveUp(index)} - {this.renderExtraOperations(index)} - - ) - } +const ArrayComponents = { + CircleButton, + TextButton, + AdditionIcon: () => , + RemoveIcon: () => , + MoveDownIcon: () => , + MoveUpIcon: () => +} - public renderCardEmpty = (title: string | ReactElement) => { - return ( - - {this.renderEmpty()} - - ) +const FormCardsField = styled( + (props: ISchemaFieldComponentProps & { className: string }) => { + const { value, schema, className, editable, path, mutators } = props + const { + renderAddition, + renderRemove, + renderMoveDown, + renderMoveUp, + renderEmpty, + renderExtraOperations, + ...componentProps + } = schema.getExtendsComponentProps() || {} + const onAdd = () => { + const items = Array.isArray(schema.items) + ? schema.items[schema.items.length - 1] + : schema.items + mutators.push(items.getEmptyValue()) } - - public render() { - const { value, className, schema, renderField } = this.props - const { - title, - style, - className: cls, - renderAddition, - renderRemove, - renderEmpty, - renderMoveDown, - renderMoveUp, - renderOperations, - ...others - } = this.getProps() || ({} as any) - - return ( -
+ {toArr(value).map((item, index) => { return ( - {index + 1}. {title || schema.title} + {index + 1}. {componentProps.title || schema.title} } - className="card-list" - key={index} - extra={this.renderOperations(item, index)} + extra={ + + mutators.remove(index)} + /> + mutators.moveDown(index)} + /> + mutators.moveUp(index)} + /> + {isFn(renderExtraOperations) + ? renderExtraOperations(index) + : renderExtraOperations} + + } > - {renderField(index)} + ) })} - {value.length === 0 && this.renderCardEmpty(title)} -
- {value.length > 0 && this.renderAddition()} -
-
- ) - } - } -)` - .ant-card-body { - padding-top: 30px; - padding-bottom: 0 !important; - } - .ant-card-head-main { - display: flex; - justify-content: space-between; - align-items: center; + + {({ children }) => { + return ( + +
{children}
+
+ ) + }} +
+ + {({ children, isEmpty }) => { + if (!isEmpty) { + return ( +
+ {children} +
+ ) + } + }} +
+ +
+ ) } +)` .ant-card { - display: block; - margin-bottom: 0px; - background: #fff; - .array-empty-wrapper { - display: flex; - justify-content: center; - cursor: pointer; - margin-bottom: 0px; - &.disabled { - cursor: default; - } - .array-empty { - display: flex; - flex-direction: column; - margin-bottom: 20px; - align-items: center; - img { - margin-bottom: 16px; - height: 85px; - } - .next-btn-text { - color: #888; - } - .next-icon:before { - width: 16px !important; - font-size: 16px !important; - margin-right: 5px; - } - } - } - - .next-card { + .ant-card { box-shadow: none; } - .card-list { - box-shadow: none; - border: 1px solid #eee; + .ant-card-body{ + padding:20px 10px 0 10px; } - - .array-item-addition { + .array-cards-addition { box-shadow: none; border: 1px solid #eee; transition: all 0.35s ease-in-out; @@ -134,40 +134,47 @@ const FormCardsField = styled( border: 1px solid #ccc; } } + .empty-wrapper { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom:10px; + img { + height: 85px; + } + .ant-btn { + color: #888; + } + } } - .ant-card.card-list { - margin-top: 20px; + .card-list-empty.card-list-item { + cursor: pointer; } - .addition-wrapper .array-item-addition { - margin-top: 20px; + .array-cards-addition { + margin-top: 10px; margin-bottom: 3px; - } - .cricle-btn { - margin-bottom: 0; - } - .ant-card-extra { - display: flex; - } - .array-item-addition { background: #fff; display: flex; cursor: pointer; - padding: 10px 0; + padding: 5px 0; justify-content: center; box-shadow: 1px 1px 4px 0 rgba(0, 0, 0, 0.1); - .next-btn-text { - color: #888; - } - .next-icon:before { - width: 16px !important; - font-size: 16px !important; - margin-right: 5px; - } } - .card-list:first-child { + .card-list-item { + margin-top: 10px; + border: 1px solid #eee; + } + .card-list-item:first-child { margin-top: 0 !important; } + .ant-card-extra { + display: flex; + button { + margin-right: 8px; + } + } ` registerFormField('cards', FormCardsField) +registerFormField('array', FormCardsField) diff --git a/packages/antd/src/fields/checkbox.tsx b/packages/antd/src/fields/checkbox.tsx index de04757933e..e24d5874411 100644 --- a/packages/antd/src/fields/checkbox.tsx +++ b/packages/antd/src/fields/checkbox.tsx @@ -1,10 +1,10 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Checkbox } from 'antd' import { transformDataSourceKey, mapStyledProps, mapTextComponent -} from '../utils' +} from '../shared' const { Group: CheckboxGroup } = Checkbox diff --git a/packages/antd/src/fields/date.tsx b/packages/antd/src/fields/date.tsx index 8d109485368..d258487c464 100644 --- a/packages/antd/src/fields/date.tsx +++ b/packages/antd/src/fields/date.tsx @@ -1,54 +1,44 @@ import React from 'react' -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import moment from 'moment' -import { DatePicker as AntDatePicker } from 'antd' +import { DatePicker } from 'antd' import { mapStyledProps, mapTextComponent, - StateLoading, compose, isStr, isArr -} from '../utils' +} from '../shared' -const { - RangePicker: AntRangePicker, - WeekPicker: AntWeekPicker, - MonthPicker: AntMonthPicker -} = AntDatePicker +const { RangePicker, WeekPicker, MonthPicker } = DatePicker -class AntYearPicker extends React.Component { +class YearPicker extends React.Component { public render() { - return + return } } -const DatePicker = StateLoading(AntDatePicker) -const RangePicker = StateLoading(AntRangePicker) -const MonthPicker = StateLoading(AntMonthPicker) -const WeekPicker = StateLoading(AntWeekPicker) -const YearPicker = StateLoading(AntYearPicker) - const transformMoment = (value, format = 'YYYY-MM-DD HH:mm:ss') => { return value && value.format ? value.format(format) : value } -const mapMomentValue = (props, fieldProps) => { - const { value, showTime = false } = props +const mapMomentValue = props => { + const { value, showTime = false, disabled = false } = props try { - if (!fieldProps.editable) return props - if (isStr(value) && value) { - props.value = moment( - value, - showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD' - ) - } else if (isArr(value) && value.length) { - props.value = value.map( - item => - (item && - moment(item, showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD')) || - '' - ) + if (!disabled) { + if (isStr(value) && value) { + props.value = moment( + value, + showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD' + ) + } else if (isArr(value) && value.length) { + props.value = value.map( + item => + (item && + moment(item, showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD')) || + '' + ) + } } } catch (e) { throw new Error(e) diff --git a/packages/antd/src/fields/index.ts b/packages/antd/src/fields/index.ts new file mode 100644 index 00000000000..59c23266c82 --- /dev/null +++ b/packages/antd/src/fields/index.ts @@ -0,0 +1,15 @@ +import './string' +import './number' +import './boolean' +import './date' +import './time' +import './range' +import './upload' +import './checkbox' +import './radio' +import './rating' +import './transfer' +import './cards' +import './table' +import './textarea' +import './password' \ No newline at end of file diff --git a/packages/antd/src/fields/number.tsx b/packages/antd/src/fields/number.tsx index 2a36d250716..e85d868452a 100644 --- a/packages/antd/src/fields/number.tsx +++ b/packages/antd/src/fields/number.tsx @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { InputNumber } from 'antd' -import { acceptEnum, mapStyledProps, mapTextComponent } from '../utils' +import { acceptEnum, mapStyledProps, mapTextComponent } from '../shared' registerFormField( 'number', diff --git a/packages/antd/src/fields/password.tsx b/packages/antd/src/fields/password.tsx index 926ae0e1ffe..02e057f15da 100644 --- a/packages/antd/src/fields/password.tsx +++ b/packages/antd/src/fields/password.tsx @@ -1,179 +1,53 @@ import React, { useState } from 'react' -import styled from 'styled-components' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Input } from 'antd' -import { InputProps } from 'antd/es/input' -import { connect, registerFormField } from '@uform/react' -import { mapStyledProps } from '../utils' +import { PasswordProps } from 'antd/lib/input' +import { PasswordStrength } from '@uform/react-shared-components' +import styled from 'styled-components' +import { mapStyledProps } from '../shared' -var isNum = function(c) { - return c >= 48 && c <= 57 -} -var isLower = function(c) { - return c >= 97 && c <= 122 -} -var isUpper = function(c) { - return c >= 65 && c <= 90 -} -var isSymbol = function(c) { - return !(isLower(c) || isUpper(c) || isNum(c)) -} -var isLetter = function(c) { - return isLower(c) || isUpper(c) +export interface IPasswordProps extends PasswordProps { + checkStrength: boolean } -const getStrength = val => { - if (!val) { - return 0 - } - let num = 0 - let lower = 0 - let upper = 0 - let symbol = 0 - let MNS = 0 - let rep = 0 - let repC = 0 - let consecutive = 0 - let sequential = 0 - const len = () => num + lower + upper + symbol - const require = () => { - var re = num > 0 ? 1 : 0 - re += lower > 0 ? 1 : 0 - re += upper > 0 ? 1 : 0 - re += symbol > 0 ? 1 : 0 - if (re > 2 && len() >= 8) { - return re + 1 - } else { - return 0 - } - } - for (var i = 0; i < val.length; i++) { - var c = val.charCodeAt(i) - if (isNum(c)) { - num++ - if (i !== 0 && i !== val.length - 1) { - MNS++ - } - if (i > 0 && isNum(val.charCodeAt(i - 1))) { - consecutive++ - } - } else if (isLower(c)) { - lower++ - if (i > 0 && isLower(val.charCodeAt(i - 1))) { - consecutive++ - } - } else if (isUpper(c)) { - upper++ - if (i > 0 && isUpper(val.charCodeAt(i - 1))) { - consecutive++ - } - } else { - symbol++ - if (i !== 0 && i !== val.length - 1) { - MNS++ - } - } - var exists = false - for (var j = 0; j < val.length; j++) { - if (val[i] === val[j] && i !== j) { - exists = true - repC += Math.abs(val.length / (j - i)) - } - } - if (exists) { - rep++ - var unique = val.length - rep - repC = unique ? Math.ceil(repC / unique) : Math.ceil(repC) - } - if (i > 1) { - var last1 = val.charCodeAt(i - 1) - var last2 = val.charCodeAt(i - 2) - if (isLetter(c)) { - if (isLetter(last1) && isLetter(last2)) { - var v = val.toLowerCase() - var vi = v.charCodeAt(i) - var vi1 = v.charCodeAt(i - 1) - var vi2 = v.charCodeAt(i - 2) - if (vi - vi1 === vi1 - vi2 && Math.abs(vi - vi1) === 1) { - sequential++ - } - } - } else if (isNum(c)) { - if (isNum(last1) && isNum(last2)) { - if (c - last1 === last1 - last2 && Math.abs(c - last1) === 1) { - sequential++ - } - } - } else { - if (isSymbol(last1) && isSymbol(last2)) { - if (c - last1 === last1 - last2 && Math.abs(c - last1) === 1) { - sequential++ +const Password: React.FC = styled((props: IPasswordProps) => { + const { className, checkStrength, onChange, ...others } = props + const [value, setValue] = useState(props.value || props.defaultValue) + return ( +
+ { + setValue(event.target.value) + if (onChange) { + onChange(event) } - } - } - } - } - let sum = 0 - let length = len() - sum += 4 * length - if (lower > 0) { - sum += 2 * (length - lower) - } - if (upper > 0) { - sum += 2 * (length - upper) - } - if (num !== length) { - sum += 4 * num - } - sum += 6 * symbol - sum += 2 * MNS - sum += 2 * require() - if (length === lower + upper) { - sum -= length - } - if (length === num) { - sum -= num - } - sum -= repC - sum -= 2 * consecutive - sum -= 3 * sequential - sum = sum < 0 ? 0 : sum - sum = sum > 100 ? 100 : sum - - if (sum >= 80) { - return 100 - } else if (sum >= 60) { - return 80 - } else if (sum >= 40) { - return 60 - } else if (sum >= 20) { - return 40 - } else { - return 20 - } -} - -interface IStrengthProps { - strength: number - className?: string -} - -// 校验强度 UI -const StrengthFC = styled(({ strength, className }: IStrengthProps) => ( -
-
-
-
-
-
-
+ {checkStrength && ( + + {score => { + return ( +
+
+
+
+
+
+
+ ) + }} + + )}
-
-))` + ) +})` .password-strength-wrapper { background: #e0e0e0; margin-bottom: 3px; @@ -210,42 +84,9 @@ const StrengthFC = styled(({ strength, className }: IStrengthProps) => ( } ` -export interface IPasswordProps extends Omit { - checkStrength: boolean // 是否启用密码强度校验 - onChange: (value: InputProps['value']) => void -} - -const PasswordFC = (props: Partial) => { - const [strength, setStrength] = useState(0) - - const { checkStrength, ...others } = props - - const onChangeHandler = (e: React.ChangeEvent) => { - const value = e.target.value - - // 开启才计算 - checkStrength && setStrength(getStrength(value)) - - // 回调 - props.onChange && props.onChange(value) - } - - return ( - - - - {checkStrength && } - - ) -} - registerFormField( 'password', connect({ getProps: mapStyledProps - })(PasswordFC) + })(Password) ) diff --git a/packages/antd/src/fields/radio.tsx b/packages/antd/src/fields/radio.tsx index f21dc2c8dee..3c510b49489 100644 --- a/packages/antd/src/fields/radio.tsx +++ b/packages/antd/src/fields/radio.tsx @@ -1,10 +1,10 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Radio } from 'antd' import { transformDataSourceKey, mapStyledProps, mapTextComponent -} from '../utils' +} from '../shared' const { Group: RadioGroup } = Radio diff --git a/packages/antd/src/fields/range.tsx b/packages/antd/src/fields/range.tsx index edab597790a..03c5fa1f730 100644 --- a/packages/antd/src/fields/range.tsx +++ b/packages/antd/src/fields/range.tsx @@ -1,7 +1,7 @@ import React from 'react' import { Slider } from 'antd' -import { connect, registerFormField } from '@uform/react' -import { mapStyledProps } from '../utils' +import { connect, registerFormField } from '@uform/react-schema-renderer' +import { mapStyledProps } from '../shared' export interface ISliderMarks { [key: number]: diff --git a/packages/antd/src/fields/rating.tsx b/packages/antd/src/fields/rating.tsx index 5b3c8cac7df..044cd5fbc9e 100644 --- a/packages/antd/src/fields/rating.tsx +++ b/packages/antd/src/fields/rating.tsx @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Rate } from 'antd' -import { mapStyledProps } from '../utils' +import { mapStyledProps } from '../shared' registerFormField( 'rating', diff --git a/packages/antd/src/fields/string.tsx b/packages/antd/src/fields/string.tsx index 6e6ba694e33..ffd041c0d9d 100644 --- a/packages/antd/src/fields/string.tsx +++ b/packages/antd/src/fields/string.tsx @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Input } from 'antd' -import { acceptEnum, mapStyledProps, mapTextComponent } from '../utils' +import { acceptEnum, mapStyledProps, mapTextComponent } from '../shared' registerFormField( 'string', diff --git a/packages/antd/src/fields/table.tsx b/packages/antd/src/fields/table.tsx index 0f5e7950ed2..01be7668509 100644 --- a/packages/antd/src/fields/table.tsx +++ b/packages/antd/src/fields/table.tsx @@ -1,355 +1,165 @@ -import React, { Component } from 'react' +import React from 'react' +import { + registerFormField, + ISchemaFieldComponentProps, + SchemaField, + Schema +} from '@uform/react-schema-renderer' +import { toArr, isFn, isArr } from '@uform/shared' +import { ArrayList } from '@uform/react-shared-components' +import { CircleButton, TextButton } from '../components/Button' +import { Table, Form, Icon } from 'antd' +import { FormItemProps } from '../compat/FormItem' import styled from 'styled-components' -import { registerFormField } from '@uform/react' -import { isFn, toArr } from '@uform/utils' -import { ArrayField } from './array' -export interface IColumnProps { - title?: string - dataIndex?: string - width?: string | number - cell: (item?: any, index?: number) => React.ReactElement +const ArrayComponents = { + CircleButton, + TextButton, + AdditionIcon: () => , + RemoveIcon: () => , + MoveDownIcon: () => , + MoveUpIcon: () => } -class Column extends Component { - public static displayName = '@schema-table-column' - public render() { - return this.props.children - } -} - -registerFormField( - 'table', - styled( - class extends ArrayField { - public createFilter(key, payload) { - const { schema } = this.props - const columnFilter: (key: string, payload: any) => boolean = - schema['x-props'] && schema['x-props'].columnFilter - - return (render, otherwise) => { - if (isFn(columnFilter)) { - return columnFilter(key, payload) - ? isFn(render) - ? render() - : render - : isFn(otherwise) - ? otherwise() - : otherwise - } else { - return render() - } - } - } - - public render() { - const { - value, - schema, - locale, - className, - renderField, - getOrderProperties - } = this.props - const cls = this.getProps('className') - const style = this.getProps('style') - const operationsWidth = this.getProps('operationsWidth') - return ( -
-
- - {getOrderProperties(schema.items).reduce( - (buf, { key, schema }) => { - const filter = this.createFilter(key, schema) - const res = filter( - () => { - return buf.concat( - { - return renderField([index, key]) - }} - /> - ) - }, - () => { - return buf - } - ) - return res - }, - [] - )} - - { - return ( -
- {this.renderRemove(index, item)} - {this.renderMoveDown(index, item)} - {this.renderMoveUp(index)} - {this.renderExtraOperations(index)} -
- ) - }} - /> -
- {this.renderAddition()} -
-
- ) - } +const FormTableField = styled( + (props: ISchemaFieldComponentProps & { className: string }) => { + const { value, schema, className, editable, path, mutators } = props + const { + renderAddition, + renderRemove, + renderMoveDown, + renderMoveUp, + renderEmpty, + renderExtraOperations, + operations, + ...componentProps + } = schema.getExtendsComponentProps() || {} + const onAdd = () => { + const items = Array.isArray(schema.items) + ? schema.items[schema.items.length - 1] + : schema.items + mutators.push(items.getEmptyValue()) } - )` - display: inline-block; - .array-item-addition { - line-height: normal !important; - padding: 10px; - background: #fbfbfb; - border-left: 1px solid #dcdee3; - border-right: 1px solid #dcdee3; - border-bottom: 1px solid #dcdee3; - .ant-btn-text { - color: #888; - i { - margin-right: 3px; + const renderColumns = (items: Schema) => { + return items.mapProperties((props, key) => { + const itemProps = { + ...props.getExtendsItemProps(), + ...props.getExtendsProps() } - } - } - .ant-table-cell-wrapper > .ant-form-item { - margin-bottom: 0; - } - .array-item-operator { - display: flex; - } - ` -) - -export interface ITableProps { - className?: string - dataSource: any -} - -/** - * 轻量级Table - */ -const Table = styled( - class Table extends Component { - public renderCell({ record, col, rowIndex }) { - return ( -
- {isFn(col.cell) - ? col.cell( - record ? record[col.dataIndex] : undefined, - rowIndex, - record - ) - : record - ? record[col.dataIndex] - : undefined} -
- ) - } - - public renderTable(columns, dataSource) { - return ( -
- - - - {columns.map((col, index) => { - return ( - - ) - })} - - - - {dataSource.map((record, rowIndex) => { - return ( - - {columns.map((col, colIndex) => { - return ( - - ) - })} - - ) - })} - {this.renderPlacehodler(dataSource, columns)} - -
-
- {col.title} -
-
- {this.renderCell({ - record, - col, - rowIndex - })} -
-
- ) + return { + title: props.title, + ...itemProps, + key, + dataIndex: key, + render: (value: any, record: any, index: number) => { + return ( + + + + ) + } + } + }) } - - public renderPlacehodler(dataSource, columns) { - if (dataSource.length === 0) { - return ( - - -
- { + return buf.concat(renderColumns(items)) + }, []) + : renderColumns(schema.items) + if (editable) { + columns.push({ + ...operations, + key: 'operations', + dataIndex: 'operations', + render: (value: any, record: any, index: number) => { + return ( + +
+ mutators.remove(index)} + /> + mutators.moveDown(index)} + /> + mutators.moveUp(index)} /> + {isFn(renderExtraOperations) + ? renderExtraOperations(index) + : renderExtraOperations}
- - - ) - } - } - - public getColumns(children) { - const columns: IColumnProps[] = [] - React.Children.forEach>( - children, - child => { - if (React.isValidElement(child)) { - if ( - child.type === Column || - child.type.displayName === '@schema-table-column' - ) { - columns.push(child.props) - } - } +
+ ) } - ) - - return columns - } - - public render() { - const columns = this.getColumns(this.props.children) - const dataSource = toArr(this.props.dataSource) - return ( -
-
-
- {this.renderTable(columns, dataSource)} -
-
-
- ) + }) } + return ( +
+ +
+ + {({ children }) => { + return ( +
+ {children} +
+ ) + }} +
+
+
+ ) } )` - .ant-table { - position: relative; - } - - .ant-table, - .ant-table *, - .ant-table :after, - .ant-table :before { - -webkit-box-sizing: border-box; - box-sizing: border-box; - } - - .ant-table table { - border-collapse: collapse; - border-spacing: 0; - width: 100%; - background: #fff; - display: table !important; - margin: 0 !important; - } - - .ant-table table tr:first-child td { - border-top-width: 0; - } - - .ant-table th { - padding: 0; - background: #ebecf0; - color: #333; - text-align: left; - font-weight: 400; - min-width: 200px; - border: 1px solid #dcdee3; + min-width: 600px; + margin-bottom: 10px; + table { + margin-bottom: 0 !important; + } + .array-table-addition { + background: #fbfbfb; + cursor: pointer; + margin-top: 3px; + border-radius: 3px; + .next-btn-text { + color: #888; + } + .next-icon:before { + width: 16px !important; + font-size: 16px !important; + margin-right: 5px; + } } - - .ant-table th .ant-table-cell-wrapper { - padding: 12px 16px; - overflow: hidden; - text-overflow: ellipsis; - word-break: break-all; + .ant-btn { + color: #888; } - - .ant-table td { - padding: 0; - border: 1px solid #dcdee3; - } - - .ant-table td .ant-table-cell-wrapper { - padding: 12px 16px; - overflow: hidden; - text-overflow: ellipsis; - word-break: break-all; + .array-item-operator { display: flex; - } - - .ant-table.zebra tr:nth-child(odd) td { - background: #fff; - } - - .ant-table.zebra tr:nth-child(2n) td { - background: #f7f8fa; - } - - .ant-table-empty { - color: #a0a2ad; - padding: 32px 0; - text-align: center; - } - - .ant-table-row { - -webkit-transition: all 0.3s ease; - transition: all 0.3s ease; - background: #fff; - color: #333; - border: none !important; - } - - .ant-table-row.hidden { - display: none; - } - - .ant-table-row.hovered, - .ant-table-row.selected { - background: #f2f3f7; - color: #333; - } - - .ant-table-body, - .ant-table-header { - overflow: auto; + button { + margin-right: 8px; + } } ` + +registerFormField('table', FormTableField) diff --git a/packages/antd/src/fields/textarea.tsx b/packages/antd/src/fields/textarea.tsx index b8b8d1cbe8a..97eb7bbc996 100644 --- a/packages/antd/src/fields/textarea.tsx +++ b/packages/antd/src/fields/textarea.tsx @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Input } from 'antd' -import { acceptEnum, mapStyledProps, mapTextComponent } from '../utils' +import { acceptEnum, mapStyledProps, mapTextComponent } from '../shared' const { TextArea } = Input diff --git a/packages/antd/src/fields/time.tsx b/packages/antd/src/fields/time.tsx index cd83a2c3ba2..fa314bf049f 100644 --- a/packages/antd/src/fields/time.tsx +++ b/packages/antd/src/fields/time.tsx @@ -1,7 +1,7 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import moment from 'moment' import { TimePicker } from 'antd' -import { mapStyledProps, mapTextComponent } from '../utils' +import { mapStyledProps, mapTextComponent } from '../shared' registerFormField( 'time', @@ -9,7 +9,7 @@ registerFormField( getValueFromEvent(_, value) { return value }, - getProps: (props, fieldProps) => { + getProps: (props: any, fieldProps) => { const { value, disabled = false } = props try { if (!disabled && value) { diff --git a/packages/antd/src/fields/transfer.tsx b/packages/antd/src/fields/transfer.tsx index 2c30691b498..b7761ccb845 100644 --- a/packages/antd/src/fields/transfer.tsx +++ b/packages/antd/src/fields/transfer.tsx @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Transfer } from 'antd' -import { mapStyledProps } from '../utils' +import { mapStyledProps } from '../shared' registerFormField( 'transfer', diff --git a/packages/antd/src/fields/upload.tsx b/packages/antd/src/fields/upload.tsx index d3ec524d0b7..896e59ed4e8 100644 --- a/packages/antd/src/fields/upload.tsx +++ b/packages/antd/src/fields/upload.tsx @@ -1,8 +1,8 @@ import React from 'react' -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Button, Upload, Icon } from 'antd' import styled from 'styled-components' -import { toArr, isArr, isEqual, mapStyledProps } from '../utils' +import { toArr, isArr, isEqual, mapStyledProps } from '../shared' const { Dragger: UploadDragger } = Upload diff --git a/packages/antd/src/form.tsx b/packages/antd/src/form.tsx deleted file mode 100644 index 13857383c1f..00000000000 --- a/packages/antd/src/form.tsx +++ /dev/null @@ -1,481 +0,0 @@ -import React from 'react' -import classNames from 'classnames' -import { Row, Col, Popover, Icon } from 'antd' -import styled from 'styled-components' -import { registerFormWrapper, registerFieldMiddleware } from '@uform/react' -import { IFormItemProps, IFormProps } from '@uform/types' - -import LOCALE from './locale' -import { isFn, moveTo, isStr, stringLength } from './utils' - -/** - * 轻量级 Form,不包含任何数据管理能力 - */ - -export const { - Provider: FormLayoutProvider, - Consumer: FormLayoutConsumer -} = React.createContext(undefined) - -const normalizeCol = col => { - return typeof col === 'object' ? col : { span: col } -} - -const getParentNode = (node, selector) => { - if (!node || (node && !node.matches)) { - return - } - if (node.matches(selector)) { - return node - } else { - return getParentNode(node.parentNode || node.parentElement, selector) - } -} - -const isPopDescription = (description, maxTipsNum = 30) => { - if (isStr(description)) { - return stringLength(description) > maxTipsNum - } else { - return React.isValidElement(description) - } -} - -export const FormItem = styled( - class FormItem extends React.Component { - public static defaultProps = { - prefix: 'ant-' - } - - public render() { - /* eslint-disable @typescript-eslint/no-unused-vars */ - const { - className, - labelAlign, - labelTextAlign, - style, - prefix, - wrapperCol, - labelCol, - size, - help, - extra, - noMinHeight, - isTableColItem, - validateState, - autoAddColon, - maxTipsNum, - required, - type, - schema, - ...others - } = this.props - /* eslint-enable @typescript-eslint/no-unused-vars */ - const itemClassName = classNames({ - [`${prefix}form-item`]: true, - [`${prefix}${labelAlign}`]: labelAlign, - [`has-${validateState}`]: !!validateState, - [`${prefix}${size}`]: !!size, - [`${className}`]: !!className, - [`field-${type}`]: !!type - }) - - // 垂直模式并且左对齐才用到 - const Tag = (wrapperCol || labelCol) && labelAlign !== 'top' ? Row : 'div' - const label = labelAlign === 'inset' ? null : this.getItemLabel() - return ( - - {label} - {this.getItemWrapper()} - - ) - } - - private getItemLabel() { - const { - id, - required, - label, - labelCol, - wrapperCol, - prefix, - extra, - labelAlign, - labelTextAlign, - autoAddColon, - isTableColItem, - maxTipsNum - } = this.props - - if (!label || isTableColItem) { - return null - } - - const ele = ( - // @ts-ignore - - ) - - const cls = classNames({ - [`${prefix}form-item-label`]: true, - [`${prefix}${labelTextAlign}`]: !!labelTextAlign - }) - - if ((wrapperCol || labelCol) && labelAlign !== 'top') { - return ( - - {ele} - {isPopDescription(extra, maxTipsNum) && this.renderHelper()} - - ) - } - - return ( -
- {ele} - {isPopDescription(extra, maxTipsNum) && this.renderHelper()} -
- ) - } - - private getItemWrapper() { - const { - labelCol, - wrapperCol, - children, - extra, - label, - labelAlign, - help, - prefix, - noMinHeight, - size, - isTableColItem, - maxTipsNum - } = this.props - - const message = ( -
- {help &&
{help}
} - {!help && !isPopDescription(extra, maxTipsNum) && ( -
{extra}
- )} -
- ) - const ele = ( -
- {React.cloneElement(children, { size })} - {message} -
- ) - if ( - (wrapperCol || labelCol) && - labelAlign !== 'top' && - !isTableColItem && - label - ) { - return ( - - {ele} - - ) - } - - return {ele} - } - - private renderHelper() { - return ( - - {/* TODO antd 没有 size 属性 */} - - - ) - } - } -)` - margin-bottom: 0 !important; - .ant-form-item-control-wrapper { - line-height: 32px; - } - .ant-form-item-control { - line-height: 32px; - } - &.field-table { - .ant-form-item-control { - overflow: auto; - } - } - .antd-uploader { - display: block; - } - .ant-form-item-msg { - &.ant-form-item-space { - min-height: 18px; - margin-bottom: 2px; - .ant-form-item-help, - .ant-form-item-extra { - margin-top: 0; - line-height: 1.5; - } - } - } - .ant-form-tips { - margin-left: -5px; - margin-right: 10px; - transform: translateY(1px); - } - .ant-form-item-extra { - color: #888; - font-size: 12px; - line-height: 1.7; - } - .ant-col { - padding-right: 0; - } - .ant-card-head { - background: none; - } - .ant-form-item-label label { - color: #666; - font-size: 14px; - &.no-colon:after { - content: ''; - } - } - ul { - padding: 0; - li { - margin: 0; - & + li { - margin: 0; - } - } - } -` - -const toArr = val => (Array.isArray(val) ? val : val ? [val] : []) - -registerFormWrapper(OriginForm => { - OriginForm = styled(OriginForm)` - &.ant-form-inline, - .ant-form-inline { - display: flex; - .rs-uform-content { - margin-right: 15px; - } - .ant-form-item { - display: inline-block; - vertical-align: top; - } - .ant-form-item:not(:last-child) { - margin-right: 20px; - } - - .ant-form-item.ant-left .ant-form-item-control { - display: inline-block; - display: table-cell\0; - vertical-align: top; - line-height: 0; - } - } - .ant-form-item-label { - line-height: 32px; - } - .ant-form-item-label label[required]:before { - margin-right: 4px; - content: '*'; - color: #ff3000; - } - .ant-form-item-help { - margin-top: 4px; - font-size: 12px; - line-height: 1.5; - color: #999; - } - .ant-form-item.has-error .ant-form-item-help { - color: #ff3000; - } - - .ant-table { - table { - table-layout: auto; - } - } - ` - - class Form extends React.Component { - public static defaultProps = { - component: 'form', - prefix: 'ant-', - size: 'default', - labelAlign: 'left', - layout: 'horizontal', - locale: LOCALE, - autoAddColon: true - } - - public static displayName = 'SchemaForm' - public static LOCALE = LOCALE - private FormRef = React.createRef() - - public render() { - const { - className, - inline, - size, - labelAlign, - labelTextAlign, - autoAddColon, - children, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - labelCol, - layout, - wrapperCol, - maxTipsNum, - style, - prefix, - ...others - } = this.props - - const isInline = inline || layout === 'line' - const formClassName = classNames({ - [`${prefix}form`]: true, - [`${prefix}form-${isInline ? 'inline' : layout}`]: true, - [`${prefix}${size}`]: size, - [`${prefix}form-${labelAlign}`]: !!labelAlign, - [className]: !!className - }) - return ( - - - {children} - - - ) - } - - private validateFailedHandler(onValidateFailed) { - return (...args) => { - if (isFn(onValidateFailed)) { - onValidateFailed(...args) - } - const container = this.FormRef.current as HTMLElement - if (container) { - const errors = container.querySelectorAll('.ant-form-item-help') - if (errors && errors.length) { - const node = getParentNode(errors[0], '.ant-form-item') - if (node) { - moveTo(node) - } - } - } - } - } - } - - Form.LOCALE = LOCALE - - return Form -}) - -const isTableColItem = (path, getSchema) => { - const schema = getSchema(path) - return schema && schema.type === 'array' && schema['x-component'] === 'table' -} - -registerFieldMiddleware(Field => { - return props => { - const { - name, - errors, - editable, - path, - required, - schema, - schemaPath, - getSchema - } = props - if (path.length === 0) { - // 根节点是不需要包FormItem的 - return React.createElement(Field, props) - } - return React.createElement( - FormLayoutConsumer, - {}, - ({ - labelAlign, - labelTextAlign, - labelCol, - maxTipsNum, - wrapperCol, - size, - autoAddColon - }) => { - return React.createElement( - FormItem, - { - labelAlign, - labelTextAlign, - labelCol, - maxTipsNum, - wrapperCol, - autoAddColon, - size, - required: editable === false ? false : required, - ...schema['x-item-props'], - label: schema.title, - noMinHeight: schema.type === 'object' && !schema['x-component'], - isTableColItem: isTableColItem( - schemaPath.slice(0, schemaPath.length - 2), - getSchema - ), - type: schema['x-component'] || schema.type, - id: name, - validateState: toArr(errors).length ? 'error' : undefined, - extra: schema.description, - help: - toArr(errors).join(' , ') || - (schema['x-item-props'] && schema['x-item-props'].help) - }, - React.createElement(Field, props) - ) - } - ) - } -}) diff --git a/packages/antd/src/index.tsx b/packages/antd/src/index.tsx index 8534b7ef68b..85d9569b902 100644 --- a/packages/antd/src/index.tsx +++ b/packages/antd/src/index.tsx @@ -1,44 +1,17 @@ -import './form' -import './fields/string' -import './fields/number' -import './fields/boolean' -import './fields/date' -import './fields/time' -import './fields/range' -import './fields/upload' -import './fields/checkbox' -import './fields/radio' -import './fields/rating' -import './fields/transfer' -import './fields/array' -import './fields/table' -import './fields/textarea' -import './fields/password' -import './fields/cards' - -export * from '@uform/react' -export * from './components/formButtonGroup' -export * from './components/button' -export * from './components/layout' import React from 'react' import { - SchemaForm as InternalSchemaForm, - Field as InternalField -} from '@uform/react' -import { SchemaFormProps, FieldProps } from './type' - -export { mapStyledProps, mapTextComponent } from './utils' - -export default class SchemaForm extends React.Component> { - render() { - return - } -} - -export class Field extends React.Component< - FieldProps -> { - render() { - return - } -} + SchemaMarkupForm, + SchemaMarkupField +} from '@uform/react-schema-renderer' +import { INextSchemaFormProps, INextSchemaFieldProps } from './types' +import './fields' +import './compat' +export * from '@uform/react-schema-renderer' +export * from './components' +export * from './types' +export { mapStyledProps, mapTextComponent } from './shared' +export const SchemaForm: React.FC< + INextSchemaFormProps +> = SchemaMarkupForm as any +export const Field: React.FC = SchemaMarkupField +export default SchemaForm diff --git a/packages/antd/src/locale.ts b/packages/antd/src/locale.ts deleted file mode 100644 index aa28ff56c6e..00000000000 --- a/packages/antd/src/locale.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable @typescript-eslint/camelcase */ -export default { - addItem: '添加', - array_invalid_minItems: '条目数不允许小于%s条', - array_invalid_maxItems: '条目数不允许大于%s条', - operations: '操作' -} diff --git a/packages/antd/src/shared.ts b/packages/antd/src/shared.ts new file mode 100644 index 00000000000..64e0363713a --- /dev/null +++ b/packages/antd/src/shared.ts @@ -0,0 +1,75 @@ +import React from 'react' +import { PreviewText } from '@uform/react-shared-components' +import { + IConnectProps, + MergedFieldComponentProps +} from '@uform/react-schema-renderer' +import { Select } from './components/Select' +export * from '@uform/shared' + +export const mapTextComponent = ( + Target: React.JSXElementConstructor, + props: any = {}, + fieldProps: any = {} +): React.JSXElementConstructor => { + const { editable } = fieldProps + if (editable !== undefined) { + if (editable === false) { + return PreviewText + } + } + if (Array.isArray(props.dataSource)) { + return Select + } + return Target +} + +export const acceptEnum = (component: React.JSXElementConstructor) => { + return ({ dataSource, ...others }) => { + if (dataSource) { + return React.createElement(Select, { dataSource, ...others }) + } else { + return React.createElement(component, others) + } + } +} + +export const transformDataSourceKey = (component, dataSourceKey) => { + return ({ dataSource, ...others }) => { + return React.createElement(component, { + [dataSourceKey]: dataSource, + ...others + }) + } +} + +export const normalizeCol = ( + col: { span: number; offset?: number } | number, + defaultValue: { span: number } +): { span: number; offset?: number } => { + if (!col) { + return defaultValue + } else { + return typeof col === 'object' ? col : { span: Number(col) } + } +} + +export const mapStyledProps = ( + props: IConnectProps, + fieldProps: MergedFieldComponentProps +) => { + const { loading, errors } = fieldProps + if (loading) { + props.state = props.state || 'loading' + } else if (errors && errors.length) { + props.state = 'error' + } +} + +export const compose = (...args: any[]) => { + return (payload: any, ...extra: any[]) => { + return args.reduce((buf, fn) => { + return buf !== undefined ? fn(buf, ...extra) : fn(payload, ...extra) + }, payload) + } +} diff --git a/packages/antd/src/type.tsx b/packages/antd/src/type.tsx deleted file mode 100644 index 18b6114d416..00000000000 --- a/packages/antd/src/type.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import { ColProps } from 'antd/es/col' -import { CardProps } from 'antd/es/card' -import { BaseButtonProps } from 'antd/es/button/button' -import { - IFormActions, - ISchema, - IEffects, - IFieldError, - TextAlign, - Size, - Layout, - TextEl, - LabelAlign, - IAsyncFormActions -} from '@uform/types' -import { SwitchProps } from 'antd/lib/switch' -import { CheckboxGroupProps } from 'antd/lib/checkbox' -import { - DatePickerProps, - RangePickerProps, - MonthPickerProps, - WeekPickerProps -} from 'antd/lib/date-picker/interface' -import { InputNumberProps } from 'antd/lib/input-number' -import { IPasswordProps } from './fields/password' -import { RadioGroupProps } from 'antd/lib/radio' -import { ISliderProps } from './fields/range' -import { RateProps } from 'antd/lib/rate' -import { InputProps } from 'antd/lib/input' -import { TextAreaProps } from 'antd/es/input' -import { TimePickerProps } from 'antd/lib/time-picker' -import { TransferProps } from 'antd/lib/transfer' -import { IUploaderProps } from './fields/upload' -import { SelectProps } from 'antd/lib/select' - -type ColSpanType = number | string - -export interface ColSize { - span?: ColSpanType - offset?: ColSpanType -} - -export interface ILocaleMessages { - [key: string]: string | ILocaleMessages -} - -export interface IFormLayoutProps { - className?: string - inline?: boolean - labelAlign?: LabelAlign - wrapperCol?: ColProps | number - labelCol?: ColProps | number - labelTextAlign?: TextAlign - size?: Size - style?: React.CSSProperties -} - -export interface IFormItemGridProps { - cols?: Array - description?: TextEl - gutter?: number - title?: TextEl -} - -export interface IRowProps { - prefix?: string - pure?: boolean - wrap?: boolean - fixed?: boolean - hidden?: boolean - className?: string - fixedWidth?: string | number - style?: React.CSSProperties - component?: keyof JSX.IntrinsicElements | React.ComponentType - gutter?: string - align?: string | number - justify?: string | number - children: React.ReactNode -} - -export interface IColProps extends ColProps { - prefix?: string - pure?: boolean - className?: string - fixedSpan?: string | number - fixedOffset?: string | number - hidden?: boolean - align?: any - component?: keyof JSX.IntrinsicElements | React.ComponentType - children?: React.ReactNode - xxs?: ColSpanType | ColSize - xs?: ColSpanType | ColSize - s?: ColSpanType | ColSize - m?: ColSpanType | ColSize - l?: ColSpanType | ColSize - xl?: ColSpanType | ColSize -} - -export interface IFormCardProps extends CardProps { - className?: string -} - -export interface IFormBlockProps extends CardProps { - className?: string -} - -export type TFormCardOrFormBlockProps = Omit - -export interface IFormTextBox { - text?: string - name?: string - title?: TextEl - description?: TextEl - gutter?: number - required?: boolean -} - -export interface IFormButtonGroupProps { - sticky?: boolean - style?: React.CSSProperties - itemStyle?: React.CSSProperties - className?: string - align?: 'left' | 'right' | 'start' | 'end' | 'top' | 'bottom' | 'center' - triggerDistance?: number - zIndex?: number - span?: ColSpanType - offset?: ColSpanType -} - -export interface ISubmitProps extends Omit { - showLoading?: boolean -} - -export interface SchemaFormProps { - actions?: IFormActions | IAsyncFormActions - initialValues?: V - defaultValue?: V - value?: V - editable?: boolean | ((name: string) => boolean) - effects?: IEffects - locale?: ILocaleMessages - schema?: ISchema - onChange?: (values: V) => void - onReset?: (values: V) => void - onSubmit?: (values: V) => void - onValidateFailed?: (fieldErrors: IFieldError[]) => void - autoAddColon?: boolean - className?: string - inline?: boolean - layout?: Layout - maxTipsNum?: number - labelAlign?: LabelAlign - labelTextAlign?: TextAlign - labelCol?: ColSize | number - wrapperCol?: ColSize | number - size?: Size - style?: React.CSSProperties - prefix?: string -} - -interface InternalFieldTypes { - boolean: SwitchProps | SelectProps - checkbox: CheckboxGroupProps - date: DatePickerProps - daterange: RangePickerProps - month: MonthPickerProps - week: WeekPickerProps - year: DatePickerProps - number: InputNumberProps | SelectProps - password: IPasswordProps - radio: RadioGroupProps - range: ISliderProps - rating: RateProps - string: InputProps | SelectProps - textarea: TextAreaProps | SelectProps - time: TimePickerProps - transfer: TransferProps - upload: IUploaderProps -} - -export interface FieldProps extends ISchema { - type?: T - name?: string - editable?: boolean - ['x-props']?: T extends keyof InternalFieldTypes ? InternalFieldTypes[T] : any -} diff --git a/packages/antd/src/types.ts b/packages/antd/src/types.ts new file mode 100644 index 00000000000..879cfba78ae --- /dev/null +++ b/packages/antd/src/types.ts @@ -0,0 +1,90 @@ +import { ButtonProps } from 'antd/lib/button' +import { FormProps, FormItemProps as ItemProps } from 'antd/lib/form' +import { + StepsProps as StepProps, + StepProps as StepItemProps +} from 'antd/lib/steps' +import { + ISchemaFormProps, + IMarkupSchemaFieldProps, + ISchemaFieldComponentProps +} from '@uform/react-schema-renderer' +import { StyledComponent } from 'styled-components' + +type ColSpanType = number | string + +export type INextSchemaFormProps = ISchemaFormProps & + FormProps & + IFormItemTopProps + +export type INextSchemaFieldProps = IMarkupSchemaFieldProps + +export interface ISubmitProps extends ButtonProps { + onSubmit?: ISchemaFormProps['onSubmit'] + showLoading?: boolean +} + +export interface IResetProps extends ButtonProps { + forceClear?: boolean + validate?: boolean +} + +export type IFormItemTopProps = React.PropsWithChildren< + Exclude< + Pick, + 'labelCol' | 'wrapperCol' + > & { + inline?: boolean + className?: string + style?: React.CSSProperties + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } + } +> + +export interface ICompatItemProps + extends Exclude, + Partial { + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} + +export type StyledCP

= StyledComponent< + (props: React.PropsWithChildren

) => React.ReactElement, + any, + {}, + never +> + +export type StyledCC = StyledCP & Statics + +export interface IFormButtonGroupProps { + sticky?: boolean + style?: React.CSSProperties + itemStyle?: React.CSSProperties + className?: string + align?: 'left' | 'right' | 'start' | 'end' | 'top' | 'bottom' | 'center' + triggerDistance?: number + zIndex?: number + span?: ColSpanType + offset?: ColSpanType +} + +export interface IItemProps { + title?: React.ReactText + description?: React.ReactText +} + +export interface IFormItemGridProps extends IItemProps { + cols?: Array + gutter?: number +} + +export interface IFormTextBox extends IItemProps { + text?: string + gutter?: number +} + +export interface IFormStep extends StepProps { + dataSource: StepItemProps[] +} diff --git a/packages/antd/src/utils.tsx b/packages/antd/src/utils.tsx deleted file mode 100644 index d449d403cd1..00000000000 --- a/packages/antd/src/utils.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import React from 'react' -import { Select as AntSelect, Icon } from 'antd' -import ReactDOM from 'react-dom' -import styled from 'styled-components' -import { isFn } from '@uform/utils' -import { IConnectProps, IFieldProps } from '@uform/react' -export * from '@uform/utils' - -export interface ISelectProps { - dataSource: any[] - className: string -} - -export interface IElement extends Element { - oldHTML?: string -} -const MoveTo = typeof window !== 'undefined' ? require('moveto') : null -const WrapSelect = styled( - class extends React.Component { - public render() { - const { dataSource = [], ...others } = this.props - const children = dataSource.map(item => { - const { label, value, ...others } = item - return ( - - {label} - - ) - }) - return ( - - {children} - - ) - } - } -)` - min-width: 100px; - width: 100%; -` - -const Text = styled(props => { - let value - if (props.dataSource && props.dataSource.length) { - const find = props.dataSource.filter(({ value }) => - Array.isArray(props.value) - ? props.value.some(val => val == value) - : props.value == value - ) - value = find.map(item => item.label).join(' , ') - } else { - value = Array.isArray(props.value) - ? props.value.join(' ~ ') - : String( - props.value === undefined || props.value === null ? '' : props.value - ) - } - return ( -

- {value || 'N/A'} - {props.innerAfter ? ' ' + props.innerAfter : ''} - {props.addonAfter ? ' ' + props.addonAfter : ''} -
- ) -})` - height: 32px; - line-height: 32px; - vertical-align: middle; - font-size: 13px; - color: #333; - &.small { - height: 24px; - line-height: 24px; - } - &.large { - height: 40px; - line-height: 40px; - } -` - -export interface IStateLoadingProps { - state?: string - dataSource: any[] -} - -const loadingSvg = - '' - -export const StateLoading = (Target: React.ComponentClass) => { - return class Select extends React.Component { - public wrapper: React.ReactInstance - public wrapperDOM: HTMLElement - public classList: string[] - - public componentDidMount() { - if (this.wrapper) { - this.wrapperDOM = ReactDOM.findDOMNode(this.wrapper) - this.mapState() - } - } - - public componentDidUpdate() { - this.mapState() - } - - public render() { - return ( - { - if (inst) { - this.wrapper = inst - } - }} - {...this.props} - /> - ) - } - - public mapState() { - const { state } = this.props - const loadingName = 'anticon-spin' - const iconSizeClassNames = [ - 'xxs', - 'xs', - 'small', - 'medium', - 'large', - 'xl', - 'xxl', - 'xxxl' - ] - this.classList = this.classList || [] - - if (this.wrapperDOM) { - const icon: IElement = this.wrapperDOM.querySelector('.anticon') - if (!icon || !icon.classList) { - return - } - - if (state === 'loading') { - icon.classList.forEach(className => { - if (className.indexOf('anticon-') > -1) { - if ( - className !== loadingName && - iconSizeClassNames.every(val => `anticon-${val}` !== className) - ) { - icon.classList.remove(className) - this.classList.push(className) - } - } - }) - if (icon.innerHTML) { - icon.oldHTML = icon.innerHTML - icon.innerHTML = loadingSvg - } - if (!icon.classList.contains(loadingName)) { - icon.classList.add(loadingName) - } - } else { - icon.classList.remove(loadingName) - this.classList.forEach(className => { - icon.classList.add(className) - }) - if (icon.oldHTML) { - icon.innerHTML = icon.oldHTML - } - this.classList = [] - } - } - } - } -} - -const Select = StateLoading(WrapSelect) - -export const acceptEnum = component => { - return ({ dataSource, ...others }) => { - if (dataSource || others.showSearch) { - return React.createElement(Select, { dataSource, ...others }) - } else { - return React.createElement(component, others) - } - } -} - -export const mapStyledProps = ( - props: IConnectProps, - { loading, size }: IFieldProps -) => { - if (loading) { - props.state = props.state || 'loading' - props.suffix = props.suffix || ( - - ) - } else { - props.suffix = props.suffix || - } - if (size) { - props.size = size - } -} - -export const mapTextComponent = ( - Target: React.ComponentClass, - props, - { - editable, - name - }: { editable: boolean | ((name: string) => boolean); name: string } -): React.ComponentClass => { - if (editable !== undefined) { - if (isFn(editable)) { - if (!editable(name)) { - return Text - } - } else if (editable === false) { - return Text - } - } - return Target -} - -export const compose = (...args) => { - return (payload, ...extra) => { - return args.reduce((buf, fn) => { - return buf !== undefined ? fn(buf, ...extra) : fn(payload, ...extra) - }, payload) - } -} - -export const transformDataSourceKey = (component, dataSourceKey) => { - return ({ dataSource, ...others }) => { - return React.createElement(component, { - [dataSourceKey]: dataSource, - ...others - }) - } -} - -export const moveTo = element => { - if (!element || !MoveTo) { - return - } - if (element.scrollIntoView) { - element.scrollIntoView({ - behavior: 'smooth', - inline: 'start', - block: 'start' - }) - } else { - new MoveTo().move(element.getBoundingClientRect().top) - } -} diff --git a/packages/builder-next/README.md b/packages/builder-next/README.md deleted file mode 100644 index 9d2412db39e..00000000000 --- a/packages/builder-next/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# @uform/builder-next -> UForm 可视化配置 next实现 \ No newline at end of file diff --git a/packages/builder-next/src/index.js b/packages/builder-next/src/index.js deleted file mode 100644 index 2957247e88f..00000000000 --- a/packages/builder-next/src/index.js +++ /dev/null @@ -1,138 +0,0 @@ -import React from 'react' -import SchemaForm, { FormButtonGroup, Submit, Reset } from '@uform/next' -import Builder from '@uform/builder' - -import { - Button, - Collapse, - Message, - Upload, - Input, - Select, - DatePicker, - Icon, - Checkbox, - NumberPicker, - TimePicker, - Radio, - Form, - Tab -} from '@alifd/next' - -// style -import '@alifd/next/dist/next.css' - -SchemaForm.FormButtonGroup = FormButtonGroup -SchemaForm.Submit = Submit -SchemaForm.Reset = Reset - -const renderSchema = {} - -const props = { - UI: { - version: '1.x', - Button, - Accordion: Collapse, - Toast: Message, - Upload, - Input, - Select, - Icon, - DatePicker, - TimePicker, - Checkbox, - NumberPicker, - Radio, - RadioGroup: Radio.Group, - TabPane: Tab.Item, - Form, - Tab - }, - // 主题: dark/light,默认dark - themeStyle: 'dark', - // 是否展示布局组件,默认为false - showLayoutField: true, - // 是否展示预览按钮,默认为true - showPreviewBtn: true, - // 是否展示源码按钮 - showSourceCodeBtn: true, - // 控制返回按钮点击事件 - onBackBtnClick: () => { - alert('点击了返回') - }, - // 额外全局按钮 - globalButtonList: [ - // { - // key: 'submit', - // title: '自定义保存', - // render: (props) => { - // return {props.children} - // }, - // props: { - // // loading: true, - // }, - // }, { - // key: 'cancel', - // title: '取消', - // props: { - // onClick: () => { - // alert('点击取消'); - // } - // }, - // } - ], - // 是否展示全局配置 - showGlobalCfg: true, - // 全局配置额外项 - extraGlobalCfgList: [ - { - name: 'labelCol', - title: 'label宽度占比', - type: 'string' - }, - { - name: 'wrapperCol', - title: 'wrapper宽度占比', - type: 'string' - } - ], - globalCfg: {}, - supportFieldList: [], - includeFieldListKeyList: [ - 'input', - 'multipleInput', - 'number', - 'radio', - 'checkbox', - 'date', - 'month', - 'daterange', - 'time' - ], - - // 渲染引擎 - renderEngine: SchemaForm, - - schema: renderSchema, - // onChange: (data) => { - // console.info('index onChange data', data); - // }, - onSubmit: data => { - console.info('index onSubmit data', data) - } -} - -class Component extends React.Component { - constructor(props) { - super(props) - this.state = { - schema: renderSchema - } - } - - render() { - return - } -} - -export default Component diff --git a/packages/builder-next/tsconfig.json b/packages/builder-next/tsconfig.json deleted file mode 100644 index 3f2ad609650..00000000000 --- a/packages/builder-next/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./lib", - "declaration": false, - "allowJs": true, - "skipLibCheck": true - }, - "include": ["./src/**/*.js", "./src/**/*.ts", "./src/**/*.tsx"], - "exclude": ["./src/__tests__/*"] -} diff --git a/packages/builder/.npmignore b/packages/builder/.npmignore deleted file mode 100644 index 2d6bc728edb..00000000000 --- a/packages/builder/.npmignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules -config -public -scripts -*.log -src/demo -build -__tests__ \ No newline at end of file diff --git a/packages/builder/LISENCE.md b/packages/builder/LISENCE.md deleted file mode 100644 index 2bd9316cd51..00000000000 --- a/packages/builder/LISENCE.md +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015-present, Alibaba Group Holding Limited. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/packages/builder/README.md b/packages/builder/README.md deleted file mode 100644 index c4aaedcacd1..00000000000 --- a/packages/builder/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# @uform/builder -> UForm 可视化搭建实现 \ No newline at end of file diff --git a/packages/builder/package.json b/packages/builder/package.json deleted file mode 100644 index 04021b7ad6a..00000000000 --- a/packages/builder/package.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "name": "@uform/builder", - "version": "0.4.3", - "license": "MIT", - "main": "lib/index.js", - "repository": { - "type": "git", - "url": "git+https://github.com/alibaba/uform.git" - }, - "bugs": { - "url": "https://github.com/alibaba/uform/issues" - }, - "homepage": "https://github.com/alibaba/uform#readme", - "engines": { - "npm": ">=3.0.0" - }, - "scripts": { - "build": "tsc" - }, - "peerDependencies": { - "@alifd/next": "^1.13.1", - "@babel/runtime": "^7.4.4", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - }, - "resolutions": { - "@types/react": "16.8.23" - }, - "dependencies": { - "@uform/react": "^0.4.3", - "@uform/utils": "^0.4.3", - "@uform/validator": "^0.4.3", - "classnames": "^2.2.5", - "immutability-helper": "^3.0.0", - "lodash.flow": "^3.5.0", - "lodash.isequal": "^4.5.0", - "lodash.merge": "^4.6.1", - "lodash.pick": "^4.4.0", - "lodash.pickby": "^4.6.0", - "lodash.remove": "^4.7.0", - "lodash.uniqby": "^4.7.0", - "moment": "^2.24.0", - "prop-types": "^15.6.1", - "react-dnd": "^7.4.1", - "react-dnd-html5-backend": "^7.4.0", - "react-powerplug": "^1.0.0", - "react-redux": "^5.0.7", - "redux": "^4.0.0", - "redux-logger": "^3.0.6", - "redux-thunk": "^2.2.0", - "styled-components": "^4.1.2", - "uuid": "^3.2.1" - }, - "publishConfig": { - "access": "public" - }, - "gitHead": "4d068dad6183e8da294a4c899a158326c0b0b050", - "devDependencies": { - "react-test-renderer": "^16.8.6", - "typescript": "^3.5.2" - } -} diff --git a/packages/builder/src/App.js b/packages/builder/src/App.js deleted file mode 100644 index 7a4c3f81b7a..00000000000 --- a/packages/builder/src/App.js +++ /dev/null @@ -1,366 +0,0 @@ -import React, { Component, createRef } from 'react' -import cls from 'classnames' -import PropTypes from 'prop-types' -import { connect } from 'react-redux' -import { - changePreview, - changeGbConfig, - initSchema, - changeCodeMode, - changeComponent, - editComponent -} from './actions' -import { Divider } from './utils/util' - -import isEqual from 'lodash.isequal' -import AppStyle from './style' - -// components -import { - FieldList, - Preview, - GlobalBtnList, - PropsSetting -} from './components/index' - -import { SchemaForm, Field } from './utils/baseForm' -import defaultGlobalCfgList from './configs/supportGlobalCfgList' - -const noop = () => {} - -class App extends Component { - constructor(props) { - super(props) - this.state = { - systemError: false, - accordionList: [] - } - this.appRef = createRef(null) - this.appHeaderRef = createRef(null) - } - - generateGlobalCfgList = () => { - // Merge custom form global property configuration - const globalCfgList = [ - ...defaultGlobalCfgList, - ...this.props.extraGlobalCfgList - ] - const _globalCfgList = [] - for (let i = globalCfgList.length - 1; i >= 0; i--) { - if ( - !_globalCfgList.find(cfgItem => cfgItem.name === globalCfgList[i].name) - ) { - _globalCfgList.unshift(globalCfgList[i]) - } - } - return _globalCfgList - } - - // global config - renderGlobalConfig = () => { - const globalCfgList = this.generateGlobalCfgList() - - const content = ( - { - this.props.changeGbConfig(value) - }} - defaultValue={this.props.gbConfig} - labelAlign="left" - labelCol={10} - labelTextAlign="right" - > - {globalCfgList.map(props => ( - - ))} - - ) - - return content - } - - getAccordionList = () => { - const list = [ - { - title: '组件配置', - content: ( - - ), - expanded: true - } - ] - - if (this.props.showGlobalCfg) { - list.unshift({ - title: '全局配置', - content: this.renderGlobalConfig(), - expanded: false - }) - } - return list - } - - componentWillMount() { - const { - schema, - globalCfg, - changeGbConfig: _changeGbConfig, - initSchema: _initSchema - } = this.props - - _changeGbConfig(globalCfg) - _initSchema(schema) - } - - componentDidMount() { - this.setState({ - accordionList: this.getAccordionList() - }) - - const appDom = this.appRef.current - const appHeaderDom = this.appHeaderRef.current - - if (appDom.offsetTop !== 0) { - document.querySelector( - '.schamaform-content' - ).style.height = `${window.innerHeight - - appDom.offsetTop - - appHeaderDom.offsetHeight}px` - } - } - - componentWillUnmount() { - // Clear the selected componentId with the selected component - this.props.changeComponent() - this.props.editComponent(null, { - active: false - }) - } - - componentDidUpdate(prevProps) { - const oldProperties = - prevProps.schema && prevProps.schema.properties - ? prevProps.schema.properties - : {} - const { schema = {}, globalCfg = {}, initSchema: _initSchema } = this.props - - const { properties = {} } = schema - - if (!isEqual(properties, oldProperties)) { - _initSchema(schema) - } - - if (!isEqual(globalCfg, this.props.globalCfg)) { - changeGbConfig(globalCfg) - } - } - - componentDidCatch(error, info) { - if (window.location.href.indexOf('av_debug') > -1) { - console.error('Form configurator system error', error, info) - } - this.setState({ - systemError: true - }) - } - - // dynamic import layout component - getLayoutTpl() { - if (!this.props.showLayoutField) return null - - try { - const LayoutList = React.lazy( - () => - new Promise((resolve, reject) => { - import('./components/fields/layout') - .then(result => - resolve(result.default ? result : { default: result }) - ) - .catch(reject) - }) - ) - return ( - Loading...
}> - - - - ) - } catch (e) { - console.error('getEditor function error', e) - return null - } - } - - // dynamic import editor component - getEditorTpl() { - if (!this.props.showSourceCodeBtn) return null - - try { - const Editor = React.lazy( - () => - new Promise((resolve, reject) => { - import('./components/editor/index') - .then(result => - resolve(result.default ? result : { default: result }) - ) - .catch(reject) - }) - ) - return ( - Loading...
}> - - - ) - } catch (e) { - console.error('getEditor function error', e) - return null - } - } - - render() { - const { initSchemaData, renderEngine } = this.props - const { Accordion, version: UIVersion } = this.props.UI - - const contentHeight = window.innerHeight - - return this.state.systemError ? ( -

系统发生异常

- ) : ( - - -
-
- {this.getLayoutTpl()} - -
-
- -
-
- {UIVersion === '1.x' ? ( - - ) : ( - { - this.setState({ - accordionList: list - }) - }} - /> - )} -
-
- {this.getEditorTpl()} -
- ) - } -} - -App.propTypes = { - // 左上角返回按钮事件绑定 - onBackBtnClick: PropTypes.func, - // 是否展示源码编辑按钮 - showSourceCodeBtn: PropTypes.bool, - changeGbConfig: PropTypes.func, - // 当前是否是预览状态 - preview: PropTypes.bool, - changePreview: PropTypes.func, - componentProps: PropTypes.object, - schema: PropTypes.object, - showGlobalCfg: PropTypes.bool, - // 额外注入的全局配置 - extraGlobalCfgList: PropTypes.arrayOf( - PropTypes.objectOf(PropTypes.any, PropTypes.any) - ), - // 全局配置 - globalCfg: PropTypes.object, - // 全局的组件列表 - supportFieldList: PropTypes.array, - // 过滤全局组件列表 - includeFieldListKeyList: PropTypes.arrayOf(PropTypes.string), - supportConfigList: PropTypes.array, - beforeSubmit: PropTypes.func, - onSubmit: PropTypes.func, - onChange: PropTypes.func, - renderEngine: PropTypes.any.isRequired, - formSubmitUrl: PropTypes.string, - showPreviewBtn: PropTypes.bool, - globalButtonList: PropTypes.arrayOf( - PropTypes.objectOf(PropTypes.any, PropTypes.any) - ) -} - -App.defaultProps = { - onBackBtnClick: () => { - window.history.back() - }, - showGlobalCfg: false, - showSourceCodeBtn: false, - extraGlobalCfgList: [], - supportFieldList: [], - includeFieldListKeyList: [], - supportConfigList: [], - globalCfg: {}, - schema: {}, - renderEngine: SchemaForm, - beforeSubmit: noop, - onSubmit: data => {}, - onChange: noop, - formSubmitUrl: '', - showPreviewBtn: true, - globalButtonList: [] -} - -const mapStateToProps = state => state - -const mapDispatchToProps = dispatch => ({ - changePreview: preview => dispatch(changePreview(preview)), - changeGbConfig: data => dispatch(changeGbConfig(data)), - initSchema: data => dispatch(initSchema(data)), - changeCodeMode: codemode => dispatch(changeCodeMode(codemode)), - changeComponent: componentId => dispatch(changeComponent(componentId)), - editComponent: (...args) => dispatch(editComponent(...args)) -}) - -class StyledAppComp extends React.Component { - render() { - return - } -} - -export default connect( - mapStateToProps, - mapDispatchToProps -)(StyledAppComp) diff --git a/packages/builder/src/actions/index.js b/packages/builder/src/actions/index.js deleted file mode 100644 index 67377209b00..00000000000 --- a/packages/builder/src/actions/index.js +++ /dev/null @@ -1,154 +0,0 @@ -import uuid from 'uuid' - -/** - * 添加组件 - * @param {Object} component - */ -export const addComponent = (component, existId, id, type, containerId) => ({ - type: 'ADD_COMPONENT', - data: { - id: id || uuid(), - component, - existId, - addType: type, - containerId - } -}) - -/** - * 添加组件并置于编辑状态 - * @param {Object} component - */ -export const addComponentAndEdit = ( - component, - existId, - type, - containerId = [] -) => dispatch => { - const id = component.id || uuid() - - dispatch(addComponent(component, existId, id, type, containerId)) - dispatch(changeComponent(Array.isArray(id) ? id : [...containerId, id])) - dispatch( - showComponentProps(Array.isArray(id) ? id : [...containerId, id], component) - ) - dispatch( - editComponent(Array.isArray(id) ? id : [...containerId, id], { - active: true - }) - ) -} - -/** - * 移动组件 - * @param {Array} sourceId 源ID - * @param {Array} targetId 目标ID - */ -export const moveComponent = (sourceId, targetId) => ({ - type: 'MOVE_COMOPNENT', - data: { - id: sourceId, - targetId - } -}) - -/** - * 改变组件顺序 - */ -export const changeComponentOrder = (sourceId, targetId, containerId) => ({ - type: 'CHANGE_COMPONENT_ORDER', - data: { - id: sourceId, - targetId, - containerId - } -}) - -/** - * 修改组件数据 - */ -export const editComponent = (id, propsData) => ({ - type: 'EDIT_COMPONENT', - data: { - id, - propsData - } -}) - -/** - * 删除组件 - */ -export const deleteComponent = id => ({ - type: 'DELETE_COMPONENT', - data: { - id - } -}) - -/** - * 展示组件属性 - */ -export const showComponentProps = (id, comp) => ({ - type: 'SHOW_COMPONENT_PROPS', - data: { - id, - comp - } -}) - -/** - * 编辑组件属性 - */ -export const editComponentProps = (id, propsData) => ({ - type: 'EDIT_COMPONENT_PROPS', - data: { - id, - propsData - } -}) - -/** - * 改变预览状态 - */ -export const changePreview = preview => ({ - type: 'CHANGE_PREVIEW', - data: { - preview - } -}) - -/** - * 改变源码编辑状态 - */ -export const changeCodeMode = codemode => ({ - type: 'CHANGE_CODEMODE', - data: { - codemode - } -}) - -/** - * 改变当前编辑的组件 - */ -export const changeComponent = componentId => ({ - type: 'CHANGE_COMPONENT', - data: { - componentId - } -}) - -/** - * 修改全局配置 - */ -export const changeGbConfig = data => ({ - type: 'CHANGE_GB_CONFIG', - data -}) - -/** - * 初始化schema - */ -export const initSchema = data => ({ - type: 'INIT_SCHEMA', - data -}) diff --git a/packages/builder/src/components/editor/index.js b/packages/builder/src/components/editor/index.js deleted file mode 100644 index 060f6c7fcb0..00000000000 --- a/packages/builder/src/components/editor/index.js +++ /dev/null @@ -1,137 +0,0 @@ -import React from 'react' -import cls from 'classnames' - -import { connect } from 'react-redux' -import { initSchema, changeGbConfig, changeCodeMode } from '../../actions/index' -import EditorStyle from './style' - -class Component extends React.Component { - componentDidMount() { - /* eslint-disable */ - const defaultValue = this.getValueFromProps() - const theme = this.props.themeStyle === 'dark' ? 'vs-dark' : 'vs' - - if (window.loadedMonaco === true) { - self.monacoInstance = monaco.editor.create( - document.getElementById('J_uformEditor'), - { - language: 'javascript', - theme: theme, - value: defaultValue - } - ) - return false - } - - const script = document.createElement('script') - script.src = - '//g.alicdn.com/ascp-comp/common-monaco-editor/5.0.1/min/vs/loader.js' - document.head.appendChild(script) - - const tpl1 = `data:text/javascript;charset=utf-8,${encodeURIComponent(` - self.MonacoEnvironment = { - baseUrl: "https://g.alicdn.com/ascp-comp/common-monaco-editor/5.0.1/min/" - }; - importScripts( - "https://g.alicdn.com/ascp-comp/common-monaco-editor/5.0.1/min/vs/base/worker/workerMain.js" - )`)}` - - script.onload = () => { - const script2 = document.createElement('script') - const tpl = ` - require.config({ - paths: { - vs: "https://g.alicdn.com/ascp-comp/common-monaco-editor/5.0.1/min/vs" - } - }); - window.MonacoEnvironment = { - getWorkerUrl: function(workerId, label) { - return "${tpl1}" - } - }; - require(["vs/editor/editor.main"], function() { - window.loadedMonaco = true; - self.monacoInstance = self.monacoInstance || monaco.editor.create(document.getElementById("J_uformEditor"), { - language: "javascript", - theme: "${theme}", - value: ${JSON.stringify(defaultValue)} - }) - }) - ` - script2.innerHTML = tpl - document.head.appendChild(script2) - } - /* eslint-enable */ - } - - getValueFromProps() { - const { initSchemaData, gbConfig } = this.props - const val = JSON.stringify( - { - schema: initSchemaData, - gbConfig - }, - null, - '\t' - ) - - return val - } - - componentDidUpdate() { - window.monacoInstance && - window.monacoInstance.setValue(this.getValueFromProps()) - } - - render() { - const { className, codemode } = this.props - - return ( - -
- { - try { - // eslint-disable-next-line - const newValue = new Function( - `return ${window.monacoInstance.getValue()}` - )() - const { schema = {}, gbConfig = {} } = newValue - const { - initSchema: _initSchema, - changeGbConfig: _changeGbConfig - } = this.props - _initSchema(schema) - _changeGbConfig(gbConfig) - } catch (e) { - throw new Error(`格式转换失败,请检查代码: + ${e.message}`) - } - }} - > - 保存源码 - - - ) - } -} - -class StyledEditorComp extends React.Component { - render() { - return - } -} - -const mapStateToProps = state => state - -const mapDispatchToProps = dispatch => ({ - initSchema: data => dispatch(initSchema(data)), - changeGbConfig: data => dispatch(changeGbConfig(data)), - changeCodeMode: codemode => dispatch(changeCodeMode(codemode)) -}) - -export default connect( - mapStateToProps, - mapDispatchToProps -)(StyledEditorComp) diff --git a/packages/builder/src/components/editor/style.js b/packages/builder/src/components/editor/style.js deleted file mode 100644 index 532d4d66120..00000000000 --- a/packages/builder/src/components/editor/style.js +++ /dev/null @@ -1,33 +0,0 @@ -import styled from 'styled-components' - -export default styled.div` - position: absolute; - min-width: 500px; - top: 64px; - right: 0; - bottom: 0; - overflow-y: scroll; - box-shadow: 0 -1px 3px 0 rgba(0, 0, 0, 0.2); - transition: transform 0.2s ease-in; - transform: translate3d(500px, 0, 0); - &.active { - transform: translate3d(0, 0, 0); - } - z-index: 2000; - .editor { - position: absolute; - top: 0; - right: 0; - width: 100%; - height: 100%; - } - .editor-btn { - position: absolute; - top: 10px; - right: 10px; - padding: 4px; - background: rgba(90, 96, 255, 0.95); - color: #fff; - border-radius: 3px; - } -` diff --git a/packages/builder/src/components/fields/field.js b/packages/builder/src/components/fields/field.js deleted file mode 100644 index 62032a84c72..00000000000 --- a/packages/builder/src/components/fields/field.js +++ /dev/null @@ -1,103 +0,0 @@ -import React from 'react' -import { DragSource } from 'react-dnd' -import ItemTypes from '../../constants/itemType' -import uuid from 'uuid' - -const DEFAULT_ICON_URL = - '//gw.alicdn.com/tfs/TB10xa4DbrpK1RjSZTEXXcWAVXa-116-60.png' - -const wrapFieldItem = fieldItem => - !!fieldItem && typeof fieldItem === 'object' - ? { - iconUrl: DEFAULT_ICON_URL, - ...fieldItem - } - : { - type: fieldItem, - icon: '', - iconUrl: DEFAULT_ICON_URL, - width: '58', - height: '30', - title: '自定义组件' - } - -const Box = ({ - addComponentAndEdit, - fieldItem, - isDragging, - connectDragSource -}) => { - const style = { - opacity: isDragging ? 0.4 : 1 - } - - if (isDragging) { - style.filter = 'blur(2px) brightness(.6)' - } - - const newFieldItem = wrapFieldItem(fieldItem) - const { key, iconUrl, width, height, title } = newFieldItem - - return connectDragSource( -
  • { - addComponentAndEdit(newFieldItem) - }} - style={style} - > - - {title} -
  • - ) -} - -export default DragSource( - ItemTypes.FIELD, - { - beginDrag: props => { - const { fieldItem } = props - const id = uuid() - return { fieldItem, id } - }, - endDrag(props, monitor) { - console.info('endDrag') - if (!monitor.didDrop()) { - return - } - console.info('endDrag success') - const item = monitor.getItem() - const dropResult = monitor.getDropResult() - - // @note: 不要直接拿fieldItem,避免下面删掉属性直接影响到原有的fieldItem - const fieldItem = { ...item.fieldItem } - const { addComponentAndEdit } = props - - // 删除多余的跟渲染无关的属性 - try { - ;['height', 'icon', 'iconUrl', 'width'].forEach(key => { - delete fieldItem[key] - }) - } catch (e) {} - - if (dropResult) { - if (dropResult.targetType === 'layout') { - addComponentAndEdit(fieldItem, '', 'layout', dropResult.targetId) - } else { - addComponentAndEdit(fieldItem) - } - } - } - }, - (connect, monitor) => ({ - connectDragSource: connect.dragSource(), - isDragging: monitor.isDragging() - }) -)(Box) diff --git a/packages/builder/src/components/fields/index.js b/packages/builder/src/components/fields/index.js deleted file mode 100644 index d0f8a0ccaee..00000000000 --- a/packages/builder/src/components/fields/index.js +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react' -import cls from 'classnames' -import defaultSupportFieldList from '../../configs/supportFieldList' -import { Header, wrapComp2Class } from '../../utils/util' -import { connect } from 'react-redux' -import { addComponentAndEdit } from '../../actions' -import uniqBy from 'lodash.uniqby' -import { indexStyle as IndexStyle } from './style' -import Field from './field' - -function FieldList(props) { - const { - addComponentAndEdit, - supportFieldList = [], - includeFieldListKeyList = [] - } = props - - let fieldList = defaultSupportFieldList - if (supportFieldList.length) { - fieldList = uniqBy([...supportFieldList, ...defaultSupportFieldList], 'key') - } - if (includeFieldListKeyList.length) { - fieldList = fieldList.filter( - fieldItem => includeFieldListKeyList.indexOf(fieldItem.key) > -1 - ) - } - - return ( - -
    -

    组件

    -

    可将选项拖动到主面板进行编辑

    -
    -
      - {fieldList.map((fieldItem, i) => { - return ( - - ) - })} -
    -
    - ) -} - -const mapStateToProps = state => state - -const mapDispatchToProps = dispatch => ({ - addComponentAndEdit: (...args) => dispatch(addComponentAndEdit(...args)) -}) - -export default connect( - mapStateToProps, - mapDispatchToProps -)(wrapComp2Class(FieldList)) diff --git a/packages/builder/src/components/fields/layout.js b/packages/builder/src/components/fields/layout.js deleted file mode 100644 index a7b6733c067..00000000000 --- a/packages/builder/src/components/fields/layout.js +++ /dev/null @@ -1,63 +0,0 @@ -// 布局 -import React from 'react' -import cls from 'classnames' -import PropTypes from 'prop-types' -import supportLayoutList from '../../configs/supportLayoutList' -import { Header, wrapComp2Class } from '../../utils/util' -import { connect } from 'react-redux' -import { addComponentAndEdit } from '../../actions' -import Field from './layoutField' -import { layoutStyle as LayoutStyle } from './style' - -class Component extends React.Component { - constructor(props) { - super(props) - - this.layoutList = supportLayoutList - } - - renderList() { - return ( -
      - {this.layoutList.map((item, i) => { - return ( - - ) - })} -
    - ) - } - - render() { - return ( - -
    -

    布局

    -

    单击将布局添加入主面板

    -
    - {this.renderList()} -
    - ) - } -} - -Component.propTypes = { - addComponentAndEdit: PropTypes.func -} - -Component.defaultProps = {} - -const mapStateToProps = state => state - -const mapDispatchToProps = dispatch => ({ - addComponentAndEdit: (...args) => dispatch(addComponentAndEdit(...args)) -}) - -export default connect( - mapStateToProps, - mapDispatchToProps -)(wrapComp2Class(Component)) diff --git a/packages/builder/src/components/fields/layoutField.js b/packages/builder/src/components/fields/layoutField.js deleted file mode 100644 index 9a3da8f71ca..00000000000 --- a/packages/builder/src/components/fields/layoutField.js +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react' -import { DragSource } from 'react-dnd' -import ItemTypes from '../../constants/itemType' -import uuid from 'uuid' - -const Box = ({ - addComponentAndEdit, - fieldItem, - isDragging, - connectDragSource -}) => { - const opacity = isDragging ? 0.4 : 1 - const { key, title } = fieldItem - return ( -
  • { - const id = uuid() - const newFieldItem = { - type: 'object', - key: fieldItem.key, - id, - ...fieldItem.__key__data__, - properties: {}, - 'x-props': { - ...fieldItem.__key__data__['x-props'], - _extra: fieldItem - } - } - addComponentAndEdit(newFieldItem) - }} - style={Object.assign({}, { opacity })} - > - {title} -
  • - ) -} - -export default DragSource( - ItemTypes.LAYOUT, - { - beginDrag: props => { - const { fieldItem } = props - const id = uuid() - return { fieldItem, id } - }, - endDrag(props, monitor) { - if (!monitor.didDrop()) { - return - } - - const item = monitor.getItem() - const dropResult = monitor.getDropResult() - const { id } = item - const fieldItem = { ...item.fieldItem } - const { addComponentAndEdit } = props - try { - ;['height', 'icon', 'iconUrl', 'width'].forEach(key => { - delete fieldItem[key] - }) - } catch (e) {} - - if (dropResult) { - const newFieldItem = { - type: 'object', - key: fieldItem.key, - id, - ...fieldItem.__key__data__, - properties: {}, - 'x-props': { - ...fieldItem.__key__data__['x-props'], - _extra: fieldItem - } - } - - addComponentAndEdit(newFieldItem) - } - } - }, - (connect, monitor) => ({ - connectDragSource: connect.dragSource(), - isDragging: monitor.isDragging() - }) -)(Box) diff --git a/packages/builder/src/components/fields/style.js b/packages/builder/src/components/fields/style.js deleted file mode 100644 index 986077e5a42..00000000000 --- a/packages/builder/src/components/fields/style.js +++ /dev/null @@ -1,87 +0,0 @@ -import styled from 'styled-components' - -export const indexStyle = styled.div` - .field-list { - font-size: 0; - li { - overflow: hidden; - margin-bottom: 12px; - padding: 0 8px; - display: inline-block; - width: 33.33%; - height: 75px; - font-size: 12px; - text-align: center; - color: ${props => props.theme.whiteColor}; - box-sizing: border-box; - transition: all 0.1s ease; - cursor: pointer; - &:hover { - background: ${props => props.theme.compHoverBgColor}; - } - span { - display: block; - margin: auto; - max-width: 50px; - height: 32px; - word-break: break-all; - } - } - } - .field-icon { - display: block; - margin: 0 auto 12px; - height: 30px; - background-repeat: no-repeat; - background-position: 50% 50%; - } - @media screen and (max-width: 834px) { - .field-list { - li { - width: 100%; - } - } - } -` - -export const layoutStyle = styled.div` - .layout-list { - margin-bottom: 15px; - padding: 0 8px; - font-size: 0; - li { - overflow: hidden; - margin-right: 7px; - width: 70px; - height: 90px; - line-height: 90px; - border-radius: 4px; - display: inline-block; - font-size: 12px; - text-align: center; - background: ${props => props.theme.compHoverBgColor}; - color: ${props => props.theme.whiteColor}; - border: 1px solid ${props => props.theme.compHoverBgColor}; - box-sizing: border-box; - transition: all 0.1s ease; - cursor: pointer; - &:hover { - background: ${props => props.theme.compHoverBgColor}; - border-color: ${props => props.theme.whiteColor}; - } - &:nth-child(3n) { - margin-right: 0; - } - span { - display: block; - margin: auto; - word-break: break-all; - } - } - } -` - -export default { - indexStyle, - layoutStyle -} diff --git a/packages/builder/src/components/globalBtnList/index.js b/packages/builder/src/components/globalBtnList/index.js deleted file mode 100644 index d21eb40fa48..00000000000 --- a/packages/builder/src/components/globalBtnList/index.js +++ /dev/null @@ -1,115 +0,0 @@ -import React from 'react' -import { GLOBAL_BTN_ICON_URL } from '../../configs/theme' -import { wrapSubmitSchema, CustomIcon } from '../../utils/util' -import merge from 'lodash.merge' - -export default props => { - const { - preview, - codemode, - onSubmit, - themeStyle, - changeCodeMode: _changeCodeMode, - globalButtonList, - showPreviewBtn, - showSourceCodeBtn, - UI - } = props - - // 获取主题下的默认icon图片地址 - const globalBtnIconUrlWithTheme = GLOBAL_BTN_ICON_URL[themeStyle] - - // 默认按钮 - const defaultBtnList = [ - { - key: 'preview', - show: showPreviewBtn, - title: preview ? '返回编辑' : '预览', - props: { - onClick: () => { - props.changePreview(!props.preview) - } - }, - iconType: 'eye', - iconUrl: globalBtnIconUrlWithTheme.preview, - iconWidth: 16, - iconHeight: 16 - }, - { - key: 'submit', - title: '保存', - props: { - type: 'primary', - onClick: () => { - onSubmit && - typeof onSubmit === 'function' && - onSubmit({ - schema: wrapSubmitSchema(props.initSchemaData), - globalCfg: props.gbConfig - }) - } - }, - iconType: 'save', - iconUrl: globalBtnIconUrlWithTheme.submit, - iconWidth: 15, - iconHeight: 15 - }, - { - key: 'code', - show: showSourceCodeBtn, - title: codemode ? '关闭源码' : '源码', - props: { - onClick: () => { - _changeCodeMode(!props.codemode) - } - }, - iconUrl: globalBtnIconUrlWithTheme.code, - iconWidth: 21, - iconHeight: 16 - } - ] - - // 合并相同key的数据 - const _globalButtonList = defaultBtnList.map(btnItem => { - const { key } = btnItem - const customBtnItem = globalButtonList.find(btn => btn.key === key) - return customBtnItem ? merge({}, btnItem, customBtnItem) : btnItem - }) - - // 注入额外的数据 - globalButtonList.forEach(btnItem => { - if (['preview', 'submit', 'preview'].indexOf(btnItem.key) === -1) { - _globalButtonList.push(btnItem) - } - }) - - return _globalButtonList.map(btnItem => { - const { - props = {}, - key, - show = true, - title, - iconUrl, - iconWidth, - iconHeight, - render - } = btnItem - - if (!title || !show || !key) return null - - const customIconTpl = iconUrl ? ( - - ) : null - - const originalBtn = ( - - {customIconTpl} - {title} - - ) - - return render - ? React.createElement(render, btnItem, originalBtn) - : originalBtn - }) -} diff --git a/packages/builder/src/components/index.js b/packages/builder/src/components/index.js deleted file mode 100644 index f2a34ea7117..00000000000 --- a/packages/builder/src/components/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import PropsSetting from './props/propsSetting' -import FieldList from './fields/index' -import Preview from './preview/index' -import GlobalBtnList from './globalBtnList/index' - -export { PropsSetting, FieldList, Preview, GlobalBtnList } - -export default {} diff --git a/packages/builder/src/components/preview/card.js b/packages/builder/src/components/preview/card.js deleted file mode 100644 index 051b0d6d575..00000000000 --- a/packages/builder/src/components/preview/card.js +++ /dev/null @@ -1,191 +0,0 @@ -import React, { useImperativeHandle, useRef, forwardRef } from 'react' -import { DragSource, DropTarget } from 'react-dnd' -import cls from 'classnames' -import ItemTypes from '../../constants/itemType' -import { isLayoutWrapper } from '../../utils/util' -import flow from 'lodash.flow' -const style = { - cursor: 'move' -} -const Card = forwardRef( - ( - { - Field, - props, - canDrop, - isOver, - that, - isDragging, - connectDragSource, - connectDropTarget, - id - }, - ref - ) => { - const elementRef = useRef(null) - connectDragSource(elementRef) - connectDropTarget(elementRef) - useImperativeHandle(ref, () => ({ - getNode: () => elementRef.current - })) - - const opacity = isDragging ? 0.2 : 1 - const isActive = canDrop && isOver - let backgroundColor = '#fff' - if (isActive) { - backgroundColor = '#f5f5f5' - } - - const { active = false } = props.schema - const comp = { - ...props.schema - } - - return isLayoutWrapper(comp) ? ( - connectDropTarget( -
    - {!Object.keys(props.schema.properties).length ? ( -

    - 请从左边字段拖拽组件进来这里 -

    - ) : ( - React.createElement(Field, props) - )} -
    { - ev.preventDefault() - that.onMouseClick(id, comp) - }} - /> - -
    - ) - ) : ( -
    - {React.createElement(Field, { ...props })} -
    { - ev.preventDefault() - that.onMouseClick(id, comp) - }} - /> - -
    -
    - ) - } -) - -export default flow( - DragSource( - ItemTypes.CARD, - { - beginDrag: props => { - const { id } = props - return { - source: 'card', - id - } - }, - endDrag(props, monitor) { - if (!monitor.didDrop()) { - return - } - - const dropResult = monitor.getDropResult() - const { id: droppedId } = props - if (dropResult) { - const { targetId, targetType } = dropResult - - if (targetType === 'layout') { - // props.move(droppedId, targetId) - } else { - props.moveCard(droppedId, targetId, targetType) - } - } - } - }, - (connect, monitor) => ({ - connectDragSource: connect.dragSource(), - isDragging: monitor.isDragging() - }) - ), - DropTarget( - [ItemTypes.CARD, ItemTypes.FIELD], - { - drop(props, monitor) { - const comp = props.props.schema - - return { - name: 'card', - targetId: props.id, - targetType: isLayoutWrapper(comp) ? 'layout' : '' - } - }, - hover(props, monitor, component) { - const node = component.getNode() - if (!node) { - return null - } - const hoverBoundingRect = node.getBoundingClientRect() - // Get vertical middle - const hoverMiddleY = - (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2 - // Determine mouse position - const clientOffset = monitor.getClientOffset() - // Get pixels to the top - const hoverClientY = clientOffset.y - hoverBoundingRect.top - - const isOverHalf = hoverClientY > hoverMiddleY - monitor.getItem().isOverHalf = isOverHalf - } - }, - (connect, monitor) => ({ - connectDropTarget: connect.dropTarget(), - isOver: monitor.isOver(), - isOverCurrent: monitor.isOver({ shallow: true }), - canDrop: monitor.canDrop() - }) - ) -)(Card) diff --git a/packages/builder/src/components/preview/fieldMiddleware.js b/packages/builder/src/components/preview/fieldMiddleware.js deleted file mode 100644 index 279105adcd0..00000000000 --- a/packages/builder/src/components/preview/fieldMiddleware.js +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react' -import { registerFieldMiddleware } from '../../utils/baseForm' -import Card from './card' -import { FormConsumer } from '../../constants/context' - -export default that => { - // 判断注册过则不再注册 - const hasRegisted = window.__hasRegisted__ || false - if (hasRegisted) { - return false - } - window.__hasRegisted__ = true - - const moveCard = (dragIndex, hoverIndex) => { - that.props.changeComponentOrder(dragIndex, hoverIndex) - } - - const move = (sourceId, targetId) => { - that.props.moveComponent(sourceId, targetId) - } - - registerFieldMiddleware(Field => props => - React.createElement(FormConsumer, {}, (obj = {}) => { - const { type } = obj - - // 根节点或者非预览直接返回 - if (props.path.length === 0 || type !== 'preview') { - return React.createElement(Field, props) - } - - return ( - - ) - }) - ) -} diff --git a/packages/builder/src/components/preview/index.js b/packages/builder/src/components/preview/index.js deleted file mode 100644 index bce0adaf8b0..00000000000 --- a/packages/builder/src/components/preview/index.js +++ /dev/null @@ -1,63 +0,0 @@ -import React, { Component } from 'react' -import cls from 'classnames' - -import { connect } from 'react-redux' -import { - changeComponentOrder, - moveComponent, - deleteComponent, - showComponentProps, - changeComponent -} from '../../actions' - -import { Header, wrapComp2Class } from '../../utils/util' -import registerPreviewFieldMiddleware from './fieldMiddleware' -import MainBox from './mainBox' - -import PreviewStyle from './style' - -class Preview extends Component { - componentDidMount() { - registerPreviewFieldMiddleware(this) - } - - onMouseClick = (id, comp) => { - this.props.changeComponent(id) - this.props.showComponentProps(id, comp) - } - - deleteComponent = id => { - this.props.deleteComponent && this.props.deleteComponent(id) - } - - render() { - return ( - -
    -

    预览区域

    -

    组件过多时可下拉查看更多

    -
    - -
    - ) - } -} - -const mapStateToProps = state => state - -const mapDispatchToProps = dispatch => ({ - changeComponentOrder: (sourceId, targetId, containerId) => - dispatch(changeComponentOrder(sourceId, targetId, containerId)), - moveComponent: (sourceId, targetId) => - dispatch(moveComponent(sourceId, targetId)), - deleteComponent: id => dispatch(deleteComponent(id)), - showComponentProps: (id, comp) => dispatch(showComponentProps(id, comp)), - changeComponent: componentId => dispatch(changeComponent(componentId)) -}) - -export default connect( - mapStateToProps, - mapDispatchToProps -)(wrapComp2Class(Preview)) diff --git a/packages/builder/src/components/preview/mainBox.js b/packages/builder/src/components/preview/mainBox.js deleted file mode 100644 index 10ae6940cb3..00000000000 --- a/packages/builder/src/components/preview/mainBox.js +++ /dev/null @@ -1,133 +0,0 @@ -import React, { useRef, forwardRef } from 'react' -import { DropTarget } from 'react-dnd' -import ItemTypes from '../../constants/itemType' -import { SchemaForm } from '../../utils/baseForm' -import { isEmptyObj } from '../../utils/util' -import pick from 'lodash.pick' -import { normalizeSchema } from '../../utils/lang' -import { FormProvider } from '../../constants/context' - -const RenderPreviewList = ({ props }) => { - const { preview, gbConfig = {}, schema = {}, renderEngine, onChange } = props - const { properties = {} } = schema - const { FormButtonGroup, Submit, Reset } = renderEngine - const { needFormButtonGroup } = gbConfig - - if (isEmptyObj(properties)) { - return

    请从左边字段添加组件进来吧

    - } - - let children = ' ' - try { - children = - needFormButtonGroup === true || needFormButtonGroup === 'true' ? ( - - 提交 - 重置 - - ) : ( - ' ' - ) - } catch (e) { - if (window.location.href.indexOf('av_debug=true') > -1) { - console.error(`RenderPreviewList function error: ${e.message}`) - } - } - - // @see: https://alibaba.github.io/uform/#/aAUeUD/qAI7IVFnsJ - const globalCfg = pick(gbConfig, [ - 'labelCol', - 'wrapperCol', - 'action', - 'labelAlign', - 'labelTextAlign', - 'autoAddColon', - 'inline', - 'size', - 'editable', - 'defaultValue', - 'value', - 'locale', - 'schema', - 'effects', - 'actions', - 'editable', - 'onValidateFailed', - 'onReset', - 'onSubmit', - 'onChange' - ]) - - return ( - - - {children} - - - ) -} - -const MainBox = forwardRef( - ({ props, canDrop, isOver, connectDropTarget }, ref) => { - const elementRef = useRef(null) - connectDropTarget(elementRef) - - const isActive = canDrop && isOver - let backgroundColor = '#fff' - if (isActive) { - backgroundColor = '#f1f1f1' - } - - return ( -
    - -
    -
    - ) - } -) - -export default DropTarget( - [ItemTypes.FIELD, ItemTypes.LAYOUT], - { - drop: (props, monitor, component) => { - // console.info('drop', component) - if (!component) { - return - } - const hasDroppedOnChild = monitor.didDrop() - if (hasDroppedOnChild) { - return - } - // console.info('drop child') - component.onDrop(hasDroppedOnChild) - } - }, - (connect, monitor) => ({ - connectDropTarget: connect.dropTarget(), - isOver: monitor.isOver(), - isOverCurrent: monitor.isOver({ shallow: true }), - canDrop: monitor.canDrop() - }) -)(MainBox) diff --git a/packages/builder/src/components/preview/style.js b/packages/builder/src/components/preview/style.js deleted file mode 100644 index ba5ea73264c..00000000000 --- a/packages/builder/src/components/preview/style.js +++ /dev/null @@ -1,161 +0,0 @@ -import styled from 'styled-components' - -export default styled.div` - position: relative; - height: 100%; - .preview-main { - position: absolute; - top: 75px; - left: 20px; - right: 20px; - bottom: 20px; - padding: 20px; - background: #fff; - overflow-y: scroll; - border-radius: 4px; - } - .next-card-head { - margin-bottom: 10px; - } - .next-card-body { - padding-top: 0 !important; - } - .preview-line { - position: relative; - overflow: hidden; - margin-bottom: 0 !important; - padding: 10px !important; - border: 1px solid #e9e9e9; - outline: 1px solid #outline; - border-radius: 2px; - transition: all 0.1s ease; - user-select: none; - .preview-line-del { - cursor: pointer; - z-index: 101; - opacity: 0; - width: 30px; - height: 16px; - font-size: 12px; - color: #333; - text-align: center; - &::before { - content: ''; - display: block; - margin: 0 auto 5px; - background: url('https://gw.alicdn.com/tfs/TB1j5fABkvoK1RjSZFDXXXY3pXa-30-32.png') - no-repeat center center; - background-size: 15px 16px; - height: 16px; - } - } - &:hover { - > .comp-item-layout-tool .preview-line-del { - opacity: 1; - } - } - .preview-line-layer { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - z-index: 100; - cursor: move; - } - .next-form-item-label { - text-align: right; - } - } - .preview-line-bar { - height: 10px; - &.active { - overflow: hidden; - height: 60px; - &::after { - content: ''; - display: block; - border: 1px dashed #ddd; - border-radius: 4px; - margin: 6px 0; - height: 40px; - } - } - &:last-child { - padding-bottom: 999px; - } - } - .preview-line-enter { - border-color: #419bf9; - } - .preview-line-active { - cursor: pointer; - border-color: #5a60ff; - outline-color: #e6e7ff; - } - .preview-tips { - position: absolute; - left: 0; - top: 72px; - width: 100%; - text-align: center; - color: #999; - } - - .comp-item, - .comp-item-layout { - position: relative; - width: 100%; - .next-row { - width: 100%; - } - } - .comp-item { - z-index: 110; - &.is-over-half { - &::after { - content: ''; - display: block; - width: 100%; - height: 10px; - background: #222; - } - } - &.is-not-over-half { - &::before { - content: ''; - display: block; - width: 100%; - height: 10px; - background: #222; - } - } - } - .comp-item-layout { - margin: 10px 0; - padding: 20px 10px; - min-height: 200px; - border: 1px dashed #ccc; - border-radius: 5px; - &.active { - border-color: #3f486b; - } - } - .comp-item-layout-tool { - position: absolute; - top: 5px; - right: 5px; - z-index: 101; - > * { - float: right; - margin-left: 8px; - } - } - .comp-item-layout-empty { - margin-top: 0; - padding-top: 20px; - width: 100%; - text-align: center; - color: #999; - } -` diff --git a/packages/builder/src/components/props/colsDetail.js b/packages/builder/src/components/props/colsDetail.js deleted file mode 100644 index c969bd83f0d..00000000000 --- a/packages/builder/src/components/props/colsDetail.js +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -class ColsDetail extends React.Component { - static propTypes = { - value: PropTypes.arrayOf(PropTypes.any), - onChange: PropTypes.func - } - - handleChange = (idx, val) => { - const { UI } = this.props - if (!val) { - UI.Toast.error('请确保列宽是有效整数') - return false - } - const { onChange, value } = this.props - let newValue = [...value] - const diff = val - newValue[idx] - - if (diff >= newValue[newValue.length - 1]) { - UI.Toast.error('请确保4列宽度加起来等于24') - return false - } - - newValue = newValue.map((_val, i) => { - if (i === idx) { - return val - } - if (i === newValue.length - 1) { - return _val - diff - } - if (i < idx) { - return _val - } - return _val - }) - - onChange(newValue) - } - - render() { - const { value = [], UI } = this.props - return value.map((item, idx) => ( - this.handleChange(idx, val)} - /> - )) - } -} - -export default ColsDetail diff --git a/packages/builder/src/components/props/dataSourceEditor/index.js b/packages/builder/src/components/props/dataSourceEditor/index.js deleted file mode 100644 index e45afda5d99..00000000000 --- a/packages/builder/src/components/props/dataSourceEditor/index.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react' -import { connect, registerFormFields } from '../../../utils/baseForm' -import DataSourceEditor from '../editors/fieldAttrEditors/dataSourceEditor' - -class Component extends React.Component { - constructor(props) { - super(props) - this.handleFieldAttrChange = this.handleFieldAttrChange.bind(this) - } - - handleFieldAttrChange() { - return v => { - const { onChange } = this.props - onChange(v) - } - } - - render() { - const { fieldStore = {}, value, UI } = this.props - - return ( -
    - -
    - ) - } -} - -registerFormFields({ - dataSourceEditor: connect()(Component) -}) - -export default () => {} diff --git a/packages/builder/src/components/props/defaultValueCascader/index.js b/packages/builder/src/components/props/defaultValueCascader/index.js deleted file mode 100644 index d2044b9438c..00000000000 --- a/packages/builder/src/components/props/defaultValueCascader/index.js +++ /dev/null @@ -1,36 +0,0 @@ -import React, { Component } from 'react' -import { connect, registerFormFields } from '../../../utils/baseForm' -import DefaultValueEditor from '../editors/fieldAttrEditors/defaultValueEditor/index' - -class defaultValueCascader extends Component { - constructor(props) { - super(props) - this.handleFieldAttrChange = this.handleFieldAttrChange.bind(this) - } - - handleFieldAttrChange() { - return v => { - const { onChange } = this.props - onChange(v) - } - } - - render() { - const { fieldStore = {}, value, UI } = this.props - - return ( - - ) - } -} - -registerFormFields({ - defaultValueCascader: connect()(defaultValueCascader) -}) - -export default () => {} diff --git a/packages/builder/src/components/props/editors/fieldAttrEditors/dataSourceEditor.js b/packages/builder/src/components/props/editors/fieldAttrEditors/dataSourceEditor.js deleted file mode 100644 index b60efbf509d..00000000000 --- a/packages/builder/src/components/props/editors/fieldAttrEditors/dataSourceEditor.js +++ /dev/null @@ -1,310 +0,0 @@ -import React, { Component } from 'react' -import DataSourceEnum from './dataSourceEnum' -import styled from 'styled-components' - -const LABEL_COL = { fixedSpan: 4 } -const STYLE_W = { width: 220 } -const JSON_EXAMPLE = ` -[{ - "label": "foo", - "value": "bar" -}] -` -const REQ_OPT_EXAMPLE = ` - { - key1: aa, - key2: bb - } -` - -class DataSourceEditor extends Component { - constructor(props) { - super(props) - this.state = { - dataSourceType: this.getDefaultDataSourceType(props), - error: '' - } - } - - componentDidUpdate(prevProps, prevState) { - const _dataSourceType = this.getDefaultDataSourceType( - this.props, - prevState.dataSourceType - ) - if (_dataSourceType !== prevState.dataSourceType) { - this.setState({ - dataSourceType: _dataSourceType - }) - } - } - - getDefaultDataSourceType = (props = this.props, defaultType) => { - const { fieldStore, value } = props - const xProps = fieldStore['x-props'] || {} - const enums = value.enum - - const url = xProps.url || '' - if (enums) return 'local' - if (url) return 'remote' - - return defaultType || 'local' - } - - handleTypeChange = v => { - const { onChange } = this.props - if (v === 'local') { - this.setState({ dataSourceType: 'local' }, () => { - onChange({ - url: '' - }) - }) - } else { - this.setState({ dataSourceType: 'remote' }, () => { - onChange({ - enum: '' - }) - }) - } - } - - handleReqOptValueChange = v => { - const { onChange } = this.props - - if (this.timer1) clearTimeout(this.timer1) - this.timer1 = setTimeout(() => { - if (!v) { - onChange({ - requestOptions: {} - }) - } else { - onChange({ - requestOptions: { - data: this.transformData(v, false) - } - }) - } - }, 10) - } - - handleReqOptChange = ev => { - const { onChange } = this.props - const v = ev.target.value - - try { - JSON.parse(v) - } catch (e) { - this.setState({ error: e.message || '不是合法的json格式!' }) - return - } - - this.setState({ error: '' }) - - ev.preventDefault() - ev.stopPropagation() - - if (this.timer1) clearTimeout(this.timer1) - this.timer1 = setTimeout(() => { - if (!v) { - onChange({ - requestOptions: {} - }) - } else { - onChange({ - requestOptions: { - data: JSON.parse(v) - } - }) - } - }, 10) - } - - handleEnumChange = ev => { - const v = ev.target.value - if (!v) { - this.setState({ error: '' }) - return - } - - try { - JSON.parse(v) - } catch (e) { - this.setState({ error: e.message || '不是合法的json格式!' }) - return - } - - this.setState({ error: '' }) - - ev.preventDefault() - ev.stopPropagation() - - const { onChange } = this.props - if (this.timer) clearTimeout(this.timer) - this.timer = setTimeout(() => { - onChange({ - enum: JSON.parse(v) - }) - }, 10) - } - - handleEnumValueChange = value => { - if (this.timer) clearTimeout(this.timer) - this.timer = setTimeout(() => { - this.props.onChange({ - enum: value - }) - }, 10) - } - - handleUrlChange = v => { - const { onChange } = this.props - onChange({ - url: v - }) - } - - /** - * 对象跟数组互转 - * { a: 1, b: 2 } => [{value: 1, label: a}, {value: 2, label: b}] - */ - transformData = (data, toArr = true) => { - if (toArr) { - return ( - !!data && - typeof data === 'object' && - Object.keys(data).map(key => ({ - value: data[key], - label: key - })) - ) - } else { - const obj = {} - data && - Array.isArray(data) && - data.forEach(item => { - item.label !== undefined && (obj[item.label] = item.value) - }) - return obj - } - } - - render() { - const { fieldStore = {}, UI } = this.props - const { dataSourceType, error } = this.state - const xProps = fieldStore['x-props'] || {} - const { requestOptions = {} } = xProps - const { data = {} } = requestOptions - - const reqDataArr = this.transformData(data, true) - const { Form, Input, Tab, RadioGroup, TabPane, version: UIVersion } = UI - - const dataSource = [ - { - label: '手动填写', - value: 'local' - } - ] - - const defaultSource = xProps.enum || fieldStore.enum || '' - - if (Object.hasOwnProperty.call(xProps, 'url')) { - // xprops带有url的则支持远程拉取数据 - dataSource.push({ - label: '远程接口获取', - value: 'remote' - }) - } - - const tabProps = {} - tabProps[UIVersion === '1.x' ? 'shape' : 'type'] = 'text' - - const formItemProps = {} - const _key = UIVersion === '1.x' ? 'validateState' : 'validateStatus' - formItemProps[_key] = error ? 'error' : 'success' - - return ( - - - {dataSourceType === 'local' ? ( - {error}} - > - - - - - - - - - - ) : null} - {dataSourceType === 'remote' ? ( - {error}} - > - - - -
    - -
    -
    - - - -
    -
    - ) : null} -
    - ) - } -} - -export default styled(DataSourceEditor)` - .next-form-item-control { - width: 100%; - } - .next-tabs-medium .next-tabs-content { - padding: 0; - } -` diff --git a/packages/builder/src/components/props/editors/fieldAttrEditors/dataSourceEnum.js b/packages/builder/src/components/props/editors/fieldAttrEditors/dataSourceEnum.js deleted file mode 100644 index 9869d7abaed..00000000000 --- a/packages/builder/src/components/props/editors/fieldAttrEditors/dataSourceEnum.js +++ /dev/null @@ -1,126 +0,0 @@ -import React from 'react' -import styled from 'styled-components' - -class Component extends React.Component { - state = {} - - // 新增一行记录 - handleAddNewItem = () => { - const newDataSource = [...this.props.dataSource] - newDataSource.push({ - value: '', - label: '' - }) - this.props.onChange && this.props.onChange(newDataSource) - } - - // 删除某一行 - handleDeleteItem = idx => { - const newDataSource = [...this.props.dataSource] - newDataSource.splice(idx, 1) - this.props.onChange && this.props.onChange(newDataSource) - } - - // 修改某行记录 - handleChangeItemRow = (e, idx, key) => { - const { value } = e.target - const newDataSource = [...this.props.dataSource] - newDataSource[idx][key] = value - this.props.onChange && this.props.onChange(newDataSource) - } - - renderItemRow = (item = {}, idx) => { - const { value = '', label = '' } = item - const { UI } = this.props - const btnProps = {} - if (UI.version === '1.x') { - btnProps.text = true - } else { - btnProps.shape = 'text' - } - return ( -
    -
      -
    • - - this.handleChangeItemRow(e, idx, 'label')} - /> -
    • -
    • - - this.handleChangeItemRow(e, idx, 'value')} - /> -
    • -
    - this.handleDeleteItem(idx)} - > - - -
    - ) - } - - renderNewBtn() { - const { UI } = this.props - return ( -
    - - - 添加 - -
    - ) - } - - render() { - const { dataSource = [], className } = this.props - - return ( -
    - {dataSource && dataSource.map(this.renderItemRow)} - {this.renderNewBtn()} -
    - ) - } -} - -export default styled(Component)` - .source-row { - position: relative; - margin-bottom: 4px; - li { - display: inline-block; - width: 50%; - > label, - .next-input { - display: inline-block; - } - > label { - color: ${props => props.theme.whiteColor}; - text-align: right; - } - .next-input { - width: 50px; - } - } - .ashbin-btn { - position: absolute; - top: 50%; - right: 5px; - transform: translate3d(0, -50%, 0); - opacity: 0.5; - } - &:hover { - .ashbin-btn { - opacity: 1; - } - } - } -` diff --git a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/arrayDefaultEditor.js b/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/arrayDefaultEditor.js deleted file mode 100644 index 94e577f3651..00000000000 --- a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/arrayDefaultEditor.js +++ /dev/null @@ -1,41 +0,0 @@ -import React, { Component } from 'react' -import DefaultValueGenerator from './defaultValueGenerator' - -const ds = [ - { - label: '手动输入', - value: 'specify' - }, - { - label: '从url获取', - value: 'url' - } -] -class Editor extends Component { - handleChange = v => { - const { store } = this.props - store.setAttr('default', v) - } - - render() { - const { store, UI } = this.props - const { enums } = store - - return ( - - -
    - ) - }} - /> - ) - } -} - -export default Editor diff --git a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/boolDefaultEditor.js b/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/boolDefaultEditor.js deleted file mode 100644 index b5a79fdf7b9..00000000000 --- a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/boolDefaultEditor.js +++ /dev/null @@ -1,29 +0,0 @@ -import React, { Component } from 'react' -import DefaultValueGenerator from './defaultValueGenerator' - -const ds = [ - { - label: '手动输入', - value: 'specify' - }, - { - label: '从url获取', - value: 'url' - } -] -class Editor extends Component { - render() { - return ( - - }} - /> - ) - } -} - -export default Editor diff --git a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/dateDefaultEditor.js b/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/dateDefaultEditor.js deleted file mode 100644 index 238751ed11e..00000000000 --- a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/dateDefaultEditor.js +++ /dev/null @@ -1,79 +0,0 @@ -import React, { Component } from 'react' -import DefaultValueGenerator from './defaultValueGenerator' - -const ds = [ - { - label: '当前日期', - value: 'now' - }, - { - label: '未来', - value: 'future' - }, - { - label: '过去', - value: 'past' - }, - { - label: '固定日期', - value: 'specify' - }, - { - label: '从url获取', - value: 'url' - } -] - -const DatePickerDefault = props => ( - { - props.onChange(vStr) - }} - popupAlign={'tr br'} - style={{ - verticalAlign: 'top', - marginLeft: 10, - maxWidth: 90 - }} - /> -) - -class Editor extends Component { - render() { - const { UI } = this.props - return ( - , - future: ( - - ), - past: ( - - ), - specify: - }} - /> - ) - } -} - -export default Editor diff --git a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/dateRangeDefaultEditor.js b/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/dateRangeDefaultEditor.js deleted file mode 100644 index 4594664795b..00000000000 --- a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/dateRangeDefaultEditor.js +++ /dev/null @@ -1,47 +0,0 @@ -import React, { Component } from 'react' -import DateDefaultEditor from './dateDefaultEditor' - -class Editor extends Component { - constructor(props) { - super(props) - this.handleEndDateChange = this.handleEndDateChange.bind(this) - this.handleEndDateChange = this.handleEndDateChange.bind(this) - } - - handleStartDateChange = v => { - this.props.onChange([v, this.props.value[1]]) - } - - handleEndDateChange = v => { - this.props.onChange([this.props.value[0], v]) - } - - render() { - const { store = {}, value = [], UI } = this.props - const style = { - fontSize: 12, - marginBottom: 5 - } - - return ( -
    -
    开始日期
    - -
    结束日期
    - -
    - ) - } -} - -export default Editor diff --git a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/dateTimeDefaultEditor.js b/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/dateTimeDefaultEditor.js deleted file mode 100644 index 990d2a30e8f..00000000000 --- a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/dateTimeDefaultEditor.js +++ /dev/null @@ -1,52 +0,0 @@ -import React, { Component } from 'react' -import DefaultValueGenerator from './defaultValueGenerator' - -const ds = [ - { - label: '当前时间', - value: 'now' - }, - { - label: '固定时间', - value: 'specify' - }, - { - label: '从url获取', - value: 'url' - } -] - -const DatePickerDefault = props => ( - { - if (vStr) { - props.onChange(vStr) - } else { - props.onChange(v.format('HH:mm:ss')) - } - }} - style={{ - verticalAlign: 'top', - marginLeft: 5 - }} - /> -) - -class Editor extends Component { - render() { - const { UI } = this.props - return ( - , - specify: - }} - /> - ) - } -} - -export default Editor diff --git a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/dateTimeRangeDefaultEditor.js b/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/dateTimeRangeDefaultEditor.js deleted file mode 100644 index de31a6bb0f8..00000000000 --- a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/dateTimeRangeDefaultEditor.js +++ /dev/null @@ -1,35 +0,0 @@ -import React, { Component } from 'react' -import DateTimeDefaultEditor from './dateTimeDefaultEditor' - -class Editor extends Component { - handleStartDateChange = (v, vStr) => { - this.props.onChange([vStr, this.props.value[1]]) - } - - handleEndDateChange = (v, vStr) => { - this.props.onChange([this.props.value[0], vStr]) - } - - render() { - return ( -
    -
    - 开始时间 -
    - -
    - 结束时间 -
    - -
    - ) - } -} - -export default Editor diff --git a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/defaultValueGenerator.js b/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/defaultValueGenerator.js deleted file mode 100644 index 132ed49866f..00000000000 --- a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/defaultValueGenerator.js +++ /dev/null @@ -1,110 +0,0 @@ -import React, { Component } from 'react' - -class DefaultValueGenerator extends Component { - static defaultProps = { - customEditor: {} - } - - constructor(props) { - super(props) - const value = props.value || {} - this.state = { - type: value.type || '', - value: value.value || '', - flag: value.flag || props.flag - } - } - - componentDidUpdate(prevProps, prevState) { - const value = this.props.value || {} - const preValue = prevProps.value || {} - if (JSON.stringify(value) !== JSON.stringify(preValue)) { - this.setState({ - type: value.type || '', - value: value.value || '', - flag: value.flag || this.props.flag - }) - } - } - - handleValueTypeChange = v => { - const { store } = this.props - const { name } = store - - this.setState( - { - type: v, - value: v === 'url' ? name : '' - }, - () => { - this.props.onChange({ - ...this.state - }) - } - ) - } - - handleValueChange = v => { - this.setState( - { - value: v - }, - () => { - this.props.onChange({ - ...this.state - }) - } - ) - } - - getValueEditor = (type, value) => { - if (this.props.customEditor[type]) { - return React.cloneElement(this.props.customEditor[type], { - onChange: this.handleValueChange, - value - }) - } - - if (type === 'url') { - return ( - - - - ) - } else { - return null - } - } - - render() { - const { type, value, flag } = this.state - return ( -
    - - {this.getValueEditor(type, value, flag)} -
    - ) - } -} - -export default DefaultValueGenerator diff --git a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/index.js b/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/index.js deleted file mode 100644 index c089fe8f4ef..00000000000 --- a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/index.js +++ /dev/null @@ -1,118 +0,0 @@ -import React, { Component } from 'react' -import ArrayDefaultEditor from './arrayDefaultEditor' -import BoolDefaultEditor from './boolDefaultEditor' -import DateDefaultEditor from './dateDefaultEditor' -import DateRangeDefaultEditor from './dateRangeDefaultEditor' -import DateTimeDefaultEditor from './dateTimeDefaultEditor' -import DateTimeRangeDefaultEditor from './dateTimeRangeDefaultEditor' -import MonthDefaultEditor from './monthDefaultEditor' -import StringDefaultEditor from './stringDefaultEditor' - -class DefaultValueEditor extends Component { - constructor(props) { - super(props) - this.handleOnChange = this.handleOnChange.bind(this) - } - - getEditorType = () => { - const { store } = this.props - const { type, enums } = store - const xComponent = store['x-component'] - if (type === 'date') { - return 'date' // 当前日期/前${d}天/未来${d}天/日期选择 - } else if (type === 'month') { - return 'month' // 当前月/前${d}月/后${d}月/月份选择 - } else if (type === 'daterange') { - return 'daterange' // 当前日期/前${d}天/未来${d}天/日期选择- 当前日期/前${d}天/未来${d}天/日期选择 - } else if (type === 'time') { - return 'time' // 当前时间/时间选择 - } else if (type === 'datetimerange') { - return 'datetimerange' // 当前时间/时间选择 - 当前时间/时间选择 - } else if (type === 'boolean') { - return 'boolean' // switch - } else if (type === 'string') { - if (xComponent === 'radio' || xComponent === 'checkbox' || enums) { - return 'array' // 数组选择 - } else { - return 'string' - } - } else { - return 'string' - } - } - - handleOnChange = v => { - this.props.onChange(v) - } - - createDefaultValueByType = editorType => { - const { store, value } = this.props - const { enums } = store - const xComponent = store['x-component'] - - switch (editorType) { - case 'date': - case 'time': - case 'month': - return value || {} - case 'daterange': - case 'datetimerange': - // @todo: 不知道为啥这里会传入一个object - return Array.isArray(value) && value.length ? value : [{}, {}] - case 'boolean': - return value || {} - case 'string': - if (xComponent === 'radio' || xComponent === 'checkbox' || enums) { - return value || [] // 数组选择 - } else { - return value || {} - } - default: - return value || {} - } - } - - createEditorProps = editorType => { - const { store } = this.props - - return { - store, - value: this.createDefaultValueByType(editorType), - onChange: this.handleOnChange - } - } - - getEditor = editorType => { - const props = this.createEditorProps(editorType) - const { UI } = this.props - switch (editorType) { - case 'string': - return - case 'array': - return - case 'boolean': - return - case 'month': - return - case 'date': - return - case 'daterange': - return - case 'time': - return - case 'datetimerange': - return - default: - return - } - } - - render() { - const editorType = this.getEditorType() - const editor = this.getEditor(editorType) - - return
    {editor}
    - } -} - -export default DefaultValueEditor diff --git a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/monthDefaultEditor.js b/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/monthDefaultEditor.js deleted file mode 100644 index 1897c11c4b1..00000000000 --- a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/monthDefaultEditor.js +++ /dev/null @@ -1,80 +0,0 @@ -import React, { Component } from 'react' -import DefaultValueGenerator from './defaultValueGenerator' - -const ds = [ - { - label: '当前月', - value: 'now' - }, - { - label: '未来', - value: 'future' - }, - { - label: '过去', - value: 'past' - }, - { - label: '固定月', - value: 'specify' - }, - { - label: '从url获取', - value: 'url' - } -] - -const MonthPickerDefault = props => { - const { UI } = props - const { MonthPicker } = UI.DatePicker - return ( - { - props.onChange(vStr) - }} - style={{ - verticalAlign: 'top', - marginLeft: 20 - }} - /> - ) -} - -class Editor extends Component { - render() { - const { UI } = this.props - return ( - , - future: ( - - ), - past: ( - - ), - specify: - }} - /> - ) - } -} - -export default Editor diff --git a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/stringDefaultEditor.js b/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/stringDefaultEditor.js deleted file mode 100644 index 1a911677a16..00000000000 --- a/packages/builder/src/components/props/editors/fieldAttrEditors/defaultValueEditor/stringDefaultEditor.js +++ /dev/null @@ -1,39 +0,0 @@ -import React, { Component } from 'react' -import DefaultValueGenerator from './defaultValueGenerator' - -const ds = [ - { - label: '手动输入', - value: 'specify' - }, - { - label: '从url获取', - value: 'url' - } -] - -class Editor extends Component { - render() { - const { UI } = this.props - - return ( - - ) - }} - /> - ) - } -} - -export default Editor diff --git a/packages/builder/src/components/props/fileSetting.js b/packages/builder/src/components/props/fileSetting.js deleted file mode 100644 index 92b517bc4e6..00000000000 --- a/packages/builder/src/components/props/fileSetting.js +++ /dev/null @@ -1,196 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { SchemaForm, Field } from '../../utils/baseForm' -import remove from 'lodash.remove' - -const showUploadListData = [ - { value: 'text', label: '文字' }, - { value: 'text-image', label: '图文' }, - { value: 'picture-card', label: '卡片' } -] - -const apiHost = - window.location.href.indexOf('.tmall.com') > -1 - ? '//sop.tmall.com' - : '//sop.daily.tmall.net' -const uploadUrl = `${apiHost}/workflow/instance/formUpload.do` - -// 文件特殊设置 -class fileSetting extends Component { - constructor(props) { - super(props) - this.state = { - xprops: props.xprops || {} - } - } - - componentDidUpdate(prevProps) { - const preXprops = prevProps.xprops || {} - const curXprops = this.props.xprops || {} - if (JSON.stringify(preXprops) !== JSON.stringify(curXprops)) { - this.setState({ - xprops: curXprops - }) - } - } - - onChangeDefaultFile(newDefaultFileList) { - const { onChange } = this.props - const newXprops = this.state.xprops - newXprops.defaultFileList = newDefaultFileList - - this.setState( - { - xprops: newXprops - }, - () => { - onChange && onChange(newXprops) - } - ) - } - - onChangeHandler = formdata => { - const { onChange } = this.props - const newXprops = Object.assign({}, this.state.xprops, formdata) - - if (newXprops.file) { - newXprops.defaultFileList = newXprops.file - delete newXprops.file - } - - this.setState( - { - xprops: newXprops - }, - () => { - onChange && onChange(newXprops) - } - ) - } - - getSchemaValue() { - const { xprops = {} } = this.props - const { - action = '', - limit = 10, - showUploadList = true, - listType = 'text', - defaultFileList = [] - } = xprops - return { - action, - limit, - showUploadList, - listType, - file: defaultFileList - } - } - - renderProps() { - return ( - - - - - - - ) - } - - renderUploadComp() { - const { xprops = {}, UI } = this.props - const { defaultFileList = [] } = xprops - return ( -
    -
    - -
    -
    -
    - { - const imgKey = - res.data && Object.keys(res.data).length - ? Object.keys(res.data)[0] - : null - - return { - code: res.succ === true ? '0' : '1', - name: imgKey, - imgURL: imgKey && res.data[imgKey], - downloadURL: imgKey && res.data[imgKey], - fileURL: imgKey && res.data[imgKey] - } - }} - fileList={defaultFileList} - onRemove={res => { - const newDefaultFileList = - this.props.xprops.defaultFileList || [] - remove( - newDefaultFileList, - item => item.fileURL === res.fileURL - ) - this.onChangeDefaultFile(newDefaultFileList) - }} - onSuccess={res => { - const newDefaultFileList = - this.props.xprops.defaultFileList || [] - newDefaultFileList.push(res) - this.onChangeDefaultFile(newDefaultFileList) - }} - onError={() => { - this.props.UI.Toast.error('文件上传失败') - }} - > - 上传文件 - -
    -
    -
    -
    -
    - ) - } - - render() { - return ( -
    - {this.renderProps()} - {this.renderUploadComp()} -
    - ) - } -} - -fileSetting.propTypes = { - // eslint-disable-next-line - xprops: PropTypes.object, - onChange: PropTypes.func -} - -fileSetting.defaultProps = { - xprops: {}, - // eslint-disable-next-line - onChange: xprops => {} -} - -export default fileSetting diff --git a/packages/builder/src/components/props/propsSetting.js b/packages/builder/src/components/props/propsSetting.js deleted file mode 100644 index 357c270d4e0..00000000000 --- a/packages/builder/src/components/props/propsSetting.js +++ /dev/null @@ -1,263 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { - SchemaForm, - registerFormFields, - connect as formConnect -} from '../../utils/baseForm' -import { connect } from 'react-redux' -import { - showComponentProps, - editComponentProps, - editComponent -} from '../../actions' -import { State } from 'react-powerplug' -import { getCompDetailById, flatObj } from '../../utils/util' - -import PropsStyle from './style' - -import FileSetting from './fileSetting' - -import './defaultValueCascader/index' -import './dataSourceEditor/index' -import ColsDetail from './colsDetail' - -import pickBy from 'lodash.pickby' - -// 属性设置 -class PropsSetting extends Component { - static propTypes = { - componentId: PropTypes.arrayOf(PropTypes.string), - editComponent: PropTypes.func, - editComponentProps: PropTypes.func, - componentProps: PropTypes.object - } - - constructor(props) { - super(props) - registerFormFields({ - colsDetail: formConnect({ - defaultProps: { - UI: this.props.UI - } - })(ColsDetail) - }) - } - - onChangeHandler = formdata => { - const { componentId = [] } = this.props - if (!componentId.length) return false - - const propsData = pickBy(formdata, x => x !== undefined) - - // 是否隐藏属性 - propsData['x-props'] = propsData['x-props'] || {} - propsData['x-props'].style = propsData['x-props'].style || {} - - if (propsData['x-props.htmltype'] === true) { - propsData['x-props'].htmlType = 'hidden' - propsData['x-props'].style.display = 'none' - } else { - propsData['x-props'].htmlType = '' - propsData['x-props'].style.display = 'block' - } - - // 配置选项 - if (propsData.dataSource !== undefined) { - propsData['x-props'] = { - ...propsData['x-props'], - ...propsData.dataSource - } - } - - // key1.key2.key3...keyx 格式转换 - const submitPropsData = flatObj(propsData) - - this.updateComponentPropsData(componentId, submitPropsData) - } - - getSchemaValue() { - const { componentId, componentProps = {}, initSchemaData = {} } = this.props - - if (!componentId.length) return {} - - const curComponentProps = componentProps[componentId.toString()] || [] - - const result = {} - curComponentProps.forEach(compProp => { - const { name, value } = compProp - result[name] = value - }) - - const curComponentAttr = getCompDetailById(componentId, initSchemaData) - - if (curComponentAttr['x-props']) { - result['x-props'] = curComponentAttr['x-props'] - Object.keys(result['x-props']).forEach(key => { - if ( - Object.hasOwnProperty.call(result, `x-props.${key}`) && - result[`x-props.${key}`] === undefined - ) { - result[`x-props.${key}`] = result['x-props'][key] - } - }) - } - - return result - } - - updateComponentPropsData = (componentId, propsData) => { - this.props.editComponent(componentId, propsData) - this.props.editComponentProps(componentId, propsData) - } - - generatePropsSchema() { - const { - initSchemaData = {}, - componentId, - componentProps = {}, - UI - } = this.props - - if (!componentId.length) { - return { - type: 'object', - properties: {} - } - } - - const curComponentProps = componentProps[componentId.toString()] || [] - const curComponentAttr = getCompDetailById(componentId, initSchemaData) - - const finalSchema = {} - curComponentProps.forEach(configItem => { - const xcomponent = configItem['x-component'] - const xProps = configItem['x-props'] || {} - let newXprops = { - ...xProps - } - - if ( - ['defaultValueCascader', 'dataSourceEditor'].indexOf(xcomponent) > -1 - ) { - newXprops = { - ...newXprops, - fieldStore: curComponentAttr, - UI - } - } - - finalSchema[configItem.name] = { - ...configItem, - 'x-props': newXprops - } - }) - - return { - type: 'object', - properties: finalSchema - } - } - - renderConfigList() { - const { componentId } = this.props - - if (!componentId.length) { - return

    请选择待编辑的表单字段

    - } - - return ( - - {({ state, setState }) => ( - { - $('onFormInit').subscribe(() => { - setFieldState('x-props.cols', state => { - if ( - !state.value || - (Array.isArray(state.value) && !state.length) - ) { - state.visible = false - } else { - state.visible = true - } - }) - }) - $('onFieldChange', 'x-props.cols-num').subscribe(fieldState => { - if (!fieldState.value) return - setFieldState('x-props.cols', state => { - const arr = new Array(fieldState.value).fill( - 24 / fieldState.value - ) - state.visible = true - state.value = arr - }) - }) - }} - value={this.getSchemaValue()} - onChange={this.onChangeHandler} - schema={this.generatePropsSchema()} - labelAlign="left" - labelTextAlign="right" - labelCol={8} - > - {' '} - - )} - - ) - } - - renderOptions() { - const { componentId, initSchemaData = {} } = this.props - - if (!componentId.length) return null - - const curComponentAttr = getCompDetailById(componentId, initSchemaData) - - switch (curComponentAttr.type) { - case 'upload': - return ( - { - this.props.editComponent(componentId, { - 'x-props': xprops - }) - }} - /> - ) - default: - return null - } - } - - render() { - return ( - - {this.renderConfigList()} - {this.renderOptions()} - - ) - } -} - -const mapStateToProps = state => state - -const mapDispatchToProps = dispatch => ({ - showComponentProps: (id, comp) => dispatch(showComponentProps(id, comp)), - editComponentProps: (...args) => dispatch(editComponentProps(...args)), - editComponent: (...args) => dispatch(editComponent(...args)) -}) - -class StyledPropsSettingComp extends React.Component { - render() { - return - } -} - -export default connect( - mapStateToProps, - mapDispatchToProps -)(StyledPropsSettingComp) diff --git a/packages/builder/src/components/props/style.js b/packages/builder/src/components/props/style.js deleted file mode 100644 index ce83188a332..00000000000 --- a/packages/builder/src/components/props/style.js +++ /dev/null @@ -1,60 +0,0 @@ -import styled from 'styled-components' - -export default styled.div` - .schema-form-container .schema-form-content > .schema-form-field { - &.option-item { - padding: 10px !important; - border: 1px solid transparent; - white-space: nowrap; - .option-item-row { - &:last-child { - margin-bottom: 0 !important; - } - } - &.hover { - border-color: #ccc; - } - &.active { - border-color: #419bf9; - background: rgba(16, 141, 233, 0.1); - } - } - .option-action { - position: relative; - z-index: 1000; - display: inline-block; - padding: 3px 8px; - vertical-align: middle; - cursor: pointer; - &.option-del { - &::after { - content: ''; - position: absolute; - right: 0; - top: 50%; - transform: translate3d(0, -50%, 0); - width: 1px; - height: 15px; - background: #ccc; - } - } - } - } - - .props-tips { - padding-top: 60px; - text-align: center; - color: #999; - } - - .schema-form-container - .schema-form-field.schema-object - .schema-object-item - > .next-form-item-label { - font-size: 14px !important; - font-weight: normal !important; - margin-top: 0 !important; - border-bottom: none !important; - padding-bottom: 0 !important; - } -` diff --git a/packages/builder/src/configs/index.js b/packages/builder/src/configs/index.js deleted file mode 100644 index d9bba17fb35..00000000000 --- a/packages/builder/src/configs/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import defaultSupportFieldList from './supportFieldList' -import supportGlobalCfgList from './supportGlobalCfgList' - -export default { - defaultSupportFieldList: defaultSupportFieldList, - supportGlobalCfgList: supportGlobalCfgList -} diff --git a/packages/builder/src/configs/supportConfigList.js b/packages/builder/src/configs/supportConfigList.js deleted file mode 100644 index 452b16dbefa..00000000000 --- a/packages/builder/src/configs/supportConfigList.js +++ /dev/null @@ -1,284 +0,0 @@ -// 可配置属性 -const FIELDLIST = { - ID: { - name: '__id__', - title: '字段名称', - type: 'string', - description: '字段名称:发起请求时带上的参数id,必填,全局保证唯一。', - required: true - }, - PLACEHOLDER: { - name: 'x-props.placeholder', - title: '占位符', - type: 'string' - }, - DESCRIPTION: { - name: 'description', - title: '提示文案', - type: 'string' - }, - TITLE: { - name: 'title', - title: '标题', - type: 'string', - placeholder: '请输入字段名称,不超过50个字符' - }, - DEFAULT: { - name: 'default', - title: '默认值', - type: 'object', - 'x-component': 'defaultValueCascader' - }, - DATASOURCE: { - name: 'dataSource', - title: '配置选项', - type: 'object', - 'x-component': 'dataSourceEditor' - }, - REQUIRED: { - name: 'required', - title: '是否必填', - type: 'boolean' - }, - READONLY: { - name: 'x-props.readOnly', - title: '是否只读', - type: 'boolean' - }, - DISABLED: { - name: 'x-props.disabled', - title: '是否禁用', - type: 'boolean' - }, - HIDDEN: { - name: 'x-props.htmltype', - title: '是否隐藏', - type: 'boolean' - } -} - -// 远程获取数据源请求选项,返回的数据的value/label根据下面设置进行转换 -const REMOTE_OPT = [ - { - name: 'x-props.labelKey', - title: 'labelKey', - type: 'string', - description: '默认为label' - }, - { - name: 'x-props.valueKey', - title: 'valueKey', - type: 'string', - description: '默认为value' - } -] - -// 默认组件配置 -const defaultProps = Object.keys(FIELDLIST).map(item => FIELDLIST[item]) - -// 默认各个组件的key配置项,key跟supportConfigList的key做映射 -export const getPropsByKey = key => { - switch (key) { - case 'multipleSelect': - return generateProps(null, [ - { - name: 'x-props.multiple', - 'x-props': { - htmlType: 'hidden' - }, - type: 'string' - }, - ...REMOTE_OPT - ]) - case 'select': - case 'cascaderSelect': - case 'treeSelect': - return generateProps(null, REMOTE_OPT) - case 'multipleInput': - return generateProps( - [ - 'ID', - 'TITLE', - 'DEFAULT', - 'DESCRIPTION', - 'PLACEHOLDER', - 'REQUIRED', - 'READONLY', - 'DISABLED', - 'HIDDEN' - ], - [ - { - name: 'x-props.multiple', - 'x-props': { - style: { - display: 'none' - } - }, - type: 'boolean' - } - ] - ) - case 'input': - case 'number': - return generateProps([ - 'ID', - 'TITLE', - 'DEFAULT', - 'DESCRIPTION', - 'PLACEHOLDER', - 'REQUIRED', - 'READONLY', - 'DISABLED', - 'HIDDEN' - ]) - case 'date': - case 'month': - case 'daterange': - case 'time': - return generateProps( - [ - 'ID', - 'TITLE', - 'DEFAULT', - 'DESCRIPTION', - 'REQUIRED', - 'READONLY', - 'DISABLED', - 'HIDDEN', - 'PLACEHOLDER' - ], - [ - { - name: 'x-props.showTime', - title: '是否展示时间', - type: 'boolean' - } - ] - ) - case 'imgUpload': - case 'fileUpload': - return generateProps( - [ - 'ID', - 'TITLE', - 'DESCRIPTION', - 'REQUIRED', - 'READONLY', - 'DISABLED', - 'HIDDEN' - ], - [] - ) - case 'wrapper_layout': - return generateProps( - ['ID'], - [ - { - name: 'x-props.labelCol', - title: '文本占比', - type: 'number', - description: '按照24份等比分,输入需要占的份数' - }, - { - name: 'x-props.wrapperCol', - title: '容器占比', - type: 'number', - description: '按照24份等比分,输入需要占的份数' - } - ] - ) - case 'wrapper_grid': - return generateProps( - ['ID'], - [ - { - name: 'x-props.gutter', - title: '组件间距', - type: 'number' - }, - { - name: 'x-props.cols-num', - title: '组件列数', - type: 'string', - enum: [2, 3, 4, 6, 8, 12, 24], - description: - '默认根据组件个数动态计算等比分列,若有精确控制列宽度请指定列数' - }, - { - name: 'x-props.cols', - title: '列宽度', - type: 'string', - 'x-component': 'colsDetail' - }, - { - name: 'x-props.title', - title: '标题', - type: 'string' - } - ] - ) - case 'wrapper_card': - return generateProps( - ['ID'], - [ - { - name: 'x-props.title', - title: '标题', - type: 'string' - }, - { - name: 'x-props.subTitle', - title: '副标题', - type: 'string' - }, - { - name: 'x-props.showHeadDivider', - title: '是否展示标题底部横线', - type: 'boolean' - } - ] - ) - default: - return generateProps([ - 'ID', - 'TITLE', - 'DEFAULT', - 'DESCRIPTION', - 'PLACEHOLDER', - 'REQUIRED', - 'READONLY', - 'DISABLED', - 'HIDDEN' - ]) - } -} - -/** - * 自由组合需要展示的配置项 - * @param {Array} includeKeys - */ -export const generateProps = (includeKeys = [], extraProps = []) => { - const result = [] - - // 若不传入则返回默认的组件配置 - if (!includeKeys || !includeKeys.length) { - // 简单不去重合并 - return [...defaultProps, ...extraProps] - } - - includeKeys.forEach(key => { - const item = FIELDLIST[key] - if (key && item) { - result.push(FIELDLIST[key.toUpperCase()]) - } - }) - return [...result, ...extraProps] -} - -export const maxShowFieldListLen = 8 - -export default { - getPropsByKey, - generateProps -} diff --git a/packages/builder/src/configs/supportFieldList.js b/packages/builder/src/configs/supportFieldList.js deleted file mode 100644 index 0915521ff86..00000000000 --- a/packages/builder/src/configs/supportFieldList.js +++ /dev/null @@ -1,246 +0,0 @@ -export default [ - { - key: 'input', - icon: 'info', - iconUrl: '//gw.alicdn.com/tfs/TB11eW6DmzqK1RjSZFpXXakSXXa-116-56.png', - width: '58', - height: '28', - type: 'string', - title: '单行文本框', - placeholder: '请输入' - }, - { - key: 'multipleInput', - icon: 'file-text', - iconUrl: '//gw.alicdn.com/tfs/TB1zk14DjTpK1RjSZKPXXa3UpXa-116-78.png', - width: '58', - height: '39', - type: 'string', - title: '多行文本框', - placeholder: '请输入', - 'x-props.multiple': true, - 'x-props': { - multiple: true - } - }, - { - key: 'number', - icon: 'file-text', - iconUrl: '//gw.alicdn.com/tfs/TB1f7i1DhTpK1RjSZFGXXcHqFXa-116-56.png', - width: '58', - height: '28', - type: 'number', - title: '数字选择器' - }, - { - key: 'radio', - icon: 'check-circle-o', - iconUrl: '//gw.alicdn.com/tfs/TB1zQaYDlLoK1RjSZFuXXXn0XXa-56-56.png', - width: '28', - height: '28', - type: 'radio', - title: '单选框', - 'x-component': 'radio', - enum: [ - { - value: '1', - label: '选项1' - }, - { - value: '2', - label: '选项2' - } - ], - 'x-props': { - enum: [ - { - value: '1', - label: '选项1' - }, - { - value: '2', - label: '选项2' - } - ] - } - }, - { - key: 'checkbox', - icon: 'check-square-o', - iconUrl: '//gw.alicdn.com/tfs/TB1ELO7DgHqK1RjSZFPXXcwapXa-56-56.png', - width: '28', - height: '28', - type: 'checkbox', - title: '复选框', - 'x-component': 'checkbox', - enum: [ - { - value: '1', - label: '选项1' - }, - { - value: '2', - label: '选项2' - } - ], - 'x-props': { - enum: [ - { - value: '1', - label: '选项1' - }, - { - value: '2', - label: '选项2' - } - ] - } - }, - { - key: 'imgUpload', - icon: 'picture', - iconUrl: '//gw.alicdn.com/tfs/TB1YC52DkvoK1RjSZPfXXXPKFXa-128-66.png', - width: '64', - height: '33', - type: 'upload', - title: '图片上传' - }, - { - key: 'fileUpload', - icon: 'file', - iconUrl: '//gw.alicdn.com/tfs/TB17eq5DcbpK1RjSZFyXXX_qFXa-128-62.png', - width: '64', - height: '31', - type: 'upload', - title: '文件上传' - }, - { - key: 'select', - icon: 'down-square-o', - iconUrl: '//gw.alicdn.com/tfs/TB1MA14DjTpK1RjSZKPXXa3UpXa-116-56.png', - width: '58', - height: '28', - type: 'string', - title: '单选下拉框', - 'x-component': 'enhanceSelect', - 'x-props': { - url: '', - enum: [ - { - value: '1', - label: '选项1' - } - ] - } - }, - { - key: 'multipleSelect', - icon: 'down-square-o', - iconUrl: '//gw.alicdn.com/tfs/TB1Ysm3DbvpK1RjSZPiXXbmwXXa-140-56.png', - width: '70', - height: '28', - type: 'string', - title: '多选下拉框', - 'x-component': 'enhanceSelect', - 'x-props': { - multiple: true, - url: '', - enum: [ - { - value: '1', - label: '选项1' - } - ] - } - }, - { - key: 'date', - icon: 'calendar', - iconUrl: '//gw.alicdn.com/tfs/TB10ZW.DkzoK1RjSZFlXXai4VXa-116-56.png', - width: '58', - height: '28', - type: 'date', - title: '日期选择器' - }, - { - key: 'month', - icon: 'calendar', - iconUrl: '//gw.alicdn.com/tfs/TB1aey5DbvpK1RjSZFqXXcXUVXa-116-56.png', - width: '58', - height: '28', - type: 'month', - title: '月份选择器' - }, - { - key: 'daterange', - icon: 'calendar', - iconUrl: '//gw.alicdn.com/tfs/TB1k4C3Db2pK1RjSZFsXXaNlXXa-140-56.png', - width: '70', - height: '28', - type: 'daterange', - title: '日期范围选择器' - }, - { - key: 'time', - icon: 'clock-circle-o', - iconUrl: '//gw.alicdn.com/tfs/TB1D4a8DkPoK1RjSZKbXXX1IXXa-116-56.png', - width: '58', - height: '28', - type: 'time', - title: '时间选择器' - }, - { - key: 'cascaderSelect', - icon: 'clock-circle-o', - iconUrl: '//gw.alicdn.com/tfs/TB1nGG8DkvoK1RjSZFDXXXY3pXa-100-72.png', - width: '50', - height: '36', - type: 'string', - title: '级联选择器', - 'x-component': 'cascaderSelect', - 'x-props': { - url: '' - } - }, - { - key: 'multiInput', - icon: 'clock-circle-o', - iconUrl: '//gw.alicdn.com/tfs/TB1Pym.DcfpK1RjSZFOXXa6nFXa-140-56.png', - width: '70', - height: '28', - type: 'string', - title: '批量输入框', - 'x-component': 'multiInput' - }, - { - key: 'treeSelect', - icon: 'clock-circle-o', - iconUrl: '//gw.alicdn.com/tfs/TB1PWG8DkvoK1RjSZFDXXXY3pXa-116-56.png', - width: '58', - height: '28', - type: 'string', - title: '树形下拉框', - 'x-component': 'treeSelect', - 'x-props': { - url: '' - } - }, - { - key: 'tabSelect', - icon: 'clock-circle-o', - iconUrl: '//gw.alicdn.com/tfs/TB1ch9.DgHqK1RjSZFEXXcGMXXa-132-58.png', - width: '67', - height: '29', - type: 'string', - title: 'tab选择框', - 'x-component': 'tabSelect', - 'x-props': { - dataSource: [ - { - value: 1, - label: 1 - } - ] - } - } -] diff --git a/packages/builder/src/configs/supportGlobalCfgList.js b/packages/builder/src/configs/supportGlobalCfgList.js deleted file mode 100644 index 10fbdd98475..00000000000 --- a/packages/builder/src/configs/supportGlobalCfgList.js +++ /dev/null @@ -1,80 +0,0 @@ -// 全局属性配置 - -const labelAlignEnum = [ - { value: 'left', label: '左侧' }, - { value: 'top', label: '上方' } -] -const labelTextAlignEnum = [ - { value: 'left', label: '左对齐' }, - { value: 'right', label: '右对齐' } -] -const autoAddColonEnum = [ - { value: true, label: '是' }, - { value: false, label: '否' } -] -const needFormButtonGroupEnum = [...autoAddColonEnum] -const inlineEnum = [...autoAddColonEnum] -const sizeEnum = [ - { value: 'large', label: '大' }, - { value: 'medium', label: '中' }, - { value: 'small', label: '小' } -] - -// 默认全局配置值 -export const defaultGlobalCfgValue = { - labelAlign: 'left', - labelTextAlign: 'right', - autoAddColon: true, - needFormButtonGroup: false, - inline: false, - size: 'medium', - labelCol: 8, - wrapperCol: 16, - editable: true -} - -// 默认全局配置属性列表 -export default [ - { - name: 'labelAlign', - title: '标签位置', - type: 'string', - enum: labelAlignEnum, - 'x-component': 'radio' - }, - { - name: 'labelTextAlign', - title: '标签对齐方式', - type: 'string', - enum: labelTextAlignEnum, - 'x-component': 'radio' - }, - { - name: 'inline', - title: '是否单行布局', - type: 'string', - enum: inlineEnum, - 'x-component': 'radio' - }, - { - name: 'size', - title: '组件尺寸', - type: 'string', - enum: sizeEnum, - 'x-component': 'radio' - }, - { - name: 'autoAddColon', - title: '是否加冒号', - type: 'string', - enum: autoAddColonEnum, - 'x-component': 'radio' - }, - { - name: 'needFormButtonGroup', - title: '有提交按钮', - type: 'string', - enum: needFormButtonGroupEnum, - 'x-component': 'radio' - } -] diff --git a/packages/builder/src/configs/supportLayoutList.js b/packages/builder/src/configs/supportLayoutList.js deleted file mode 100644 index 8c2172744cf..00000000000 --- a/packages/builder/src/configs/supportLayoutList.js +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable @typescript-eslint/camelcase */ -export default [ - { - key: 'wrapper_layout', - icon: 'clock-circle-o', - type: 'object', - title: 'Layout布局', - __key__: 'layout', - __key__data__: { - 'x-component': 'layout', - 'x-props': { - labelCol: 8, - wrapperCol: 6 - } - } - }, - { - key: 'wrapper_grid', - icon: 'clock-circle-o', - type: 'object', - title: 'Grid布局', - __key__: 'layout', - __key__data__: { - 'x-component': 'grid', - 'x-props': { - gutter: 20 - } - } - }, - { - key: 'wrapper_card', - icon: 'clock-circle-o', - type: 'object', - title: 'FormCard卡片式布局', - __key__: 'layout', - __key__data__: { - 'x-component': 'card', - 'x-props': { - title: '卡片式布局', - showHeadDivider: true - } - } - } -] diff --git a/packages/builder/src/configs/theme.js b/packages/builder/src/configs/theme.js deleted file mode 100644 index b6276f336fa..00000000000 --- a/packages/builder/src/configs/theme.js +++ /dev/null @@ -1,57 +0,0 @@ -// 主题 -export const dark = { - headerBgColor: '#3F486B', - leftColBgColor: '#272D44', - mainColBgColor: '#3F486B', - rightColBgColor: '#313853', - whiteColor: '#fff', - backIconUrl: '//gw.alicdn.com/tfs/TB1mw6TBhjaK1RjSZKzXXXVwXXa-18-34.png', - compHoverBgColor: '#1D2236', - dividerBgColor: '#1E2336', - dividerShadow: '0 1px 0 0 #313853', - backDividerBgColor: '#3F486B', - backDividerShadow: '1px 0 0 0 #313853, 2px 0 0 0 rgba(96, 107, 149, 0.65)', - btnNormalBgColor: '#48527A', - btnPrimaryBgColor: '#5A60FF', - btnPrimaryTxtColor: '#fff' -} - -export const light = { - headerBgColor: '#fff', - leftColBgColor: '#fff', - mainColBgColor: '#f2f2f2', - rightColBgColor: '#fff', - whiteColor: '#555', - backIconUrl: '//gw.alicdn.com/tfs/TB1t0bMBhTpK1RjSZFMXXbG_VXa-18-34.png', - compHoverBgColor: '#f3f3f3', - dividerBgColor: '#f9f9f9', - dividerShadow: '0 1px 0 0 #f2f2f2', - backDividerBgColor: '#f9f9f9', - backDividerShadow: '1px 0 0 0 #999', - btnNormalBgColor: '#f7f8fa', - btnPrimaryBgColor: '#5A60FF', - btnPrimaryTxtColor: '#fff' -} - -export const THEME_ENUM = ['dark', 'light'] - -export const DEFAULT_THEME = THEME_ENUM[0] - -// 全局操作栏按钮icon -export const GLOBAL_BTN_ICON_URL = { - [THEME_ENUM[0]]: { - preview: '//gw.alicdn.com/tfs/TB1t3egCkvoK1RjSZPfXXXPKFXa-32-32.png', - submit: '//gw.alicdn.com/tfs/TB1UnehCcbpK1RjSZFyXXX_qFXa-30-30.png', - code: '//gw.alicdn.com/tfs/TB1lT5hCXzqK1RjSZFvXXcB7VXa-42-32.png' - }, - [THEME_ENUM[1]]: { - preview: '//gw.alicdn.com/tfs/TB1y1GKCiLaK1RjSZFxXXamPFXa-32-32.png', - submit: '//gw.alicdn.com/tfs/TB1UnehCcbpK1RjSZFyXXX_qFXa-30-30.png', - code: '//gw.alicdn.com/tfs/TB15bGkCXzqK1RjSZSgXXcpAVXa-42-32.png' - } -} - -export default { - dark, - light -} diff --git a/packages/builder/src/constants/context.js b/packages/builder/src/constants/context.js deleted file mode 100644 index 5de51647409..00000000000 --- a/packages/builder/src/constants/context.js +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react' -export const { - Consumer: FormConsumer, - Provider: FormProvider -} = React.createContext() diff --git a/packages/builder/src/constants/itemType.js b/packages/builder/src/constants/itemType.js deleted file mode 100644 index 72ceaa9d424..00000000000 --- a/packages/builder/src/constants/itemType.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - CARD: 'card', - FIELD: 'field', - LAYOUT: 'layout' -} diff --git a/packages/builder/src/demo/index-1-x.js b/packages/builder/src/demo/index-1-x.js deleted file mode 100644 index 30d64dbb145..00000000000 --- a/packages/builder/src/demo/index-1-x.js +++ /dev/null @@ -1,147 +0,0 @@ -import React from 'react' -import SchemaForm, { FormButtonGroup, Submit, Reset } from '@uform/next' -import Index from '../index' -import { - Button, - Collapse, - Message, - Upload, - Input, - Select, - DatePicker, - Icon, - Checkbox, - NumberPicker, - TimePicker, - Radio, - Form, - Tab -} from '@alifd/next' - -// style -import '@alifd/next/dist/next.css' - -SchemaForm.FormButtonGroup = FormButtonGroup -SchemaForm.Submit = Submit -SchemaForm.Reset = Reset - -const renderSchema = {} - -const props = { - UI: { - version: '1.x', - Button, - Accordion: Collapse, - Toast: Message, - Upload, - Input, - Select, - Icon, - DatePicker, - TimePicker, - Checkbox, - NumberPicker, - Radio, - RadioGroup: Radio.Group, - TabPane: Tab.Item, - Form, - Tab - }, - // 主题: dark/light,默认dark - // themeStyle: 'light', - // 是否展示布局组件,默认为false - showLayoutField: false, - showPreviewBtn: true, - showSourceCodeBtn: true, - // 控制返回按钮点击事件 - onBackBtnClick: () => { - alert('点击了返回') - }, - // 额外全局按钮 - globalButtonList: [ - // { - // key: 'submit', - // title: '自定义保存', - // render: (props) => { - // return {props.children} - // }, - // props: { - // // loading: true, - // }, - // }, { - // key: 'cancel', - // title: '取消', - // props: { - // onClick: () => { - // alert('点击取消'); - // } - // }, - // } - ], - // 是否展示全局配置 - showGlobalCfg: true, - // 全局配置额外项 - extraGlobalCfgList: [ - { - name: 'labelCol', - title: 'label宽度占比', - type: 'string' - }, - { - name: 'wrapperCol', - title: 'wrapper宽度占比', - type: 'string' - }, - { - name: 'editable', - title: '表单是否可编辑', - description: - '若设置为false,则可快速搭建出表单详情页,只需设置每个组件的默认值', - type: 'boolean' - } - ], - globalCfg: {}, - supportFieldList: [], - includeFieldListKeyList: [ - 'input', - 'multipleInput', - 'number', - 'radio', - 'checkbox', - 'date', - 'month', - 'daterange', - 'time' - ], - - // 渲染引擎 - renderEngine: SchemaForm, - - schema: renderSchema, - // onChange: (data) => { - // console.info('index onChange data', data); - // }, - onSubmit: data => { - alert(`保存数据:${JSON.stringify(data)}`) - console.info('index onSubmit data', data) - } -} - -class Comp extends React.Component { - constructor(props) { - super(props) - this.state = { - schema: renderSchema - } - } - - render() { - return ( -
    - -
    - ) - } -} - -export default Comp diff --git a/packages/builder/src/demo/index.js b/packages/builder/src/demo/index.js deleted file mode 100644 index 6735d2193f7..00000000000 --- a/packages/builder/src/demo/index.js +++ /dev/null @@ -1,113 +0,0 @@ -import React from 'react' -import SchemaForm from '@uform/next' -import Index from '../index' -import { - Button, - Accordion, - Feedback, - Upload, - Input, - Select, - DatePicker, - Icon, - Checkbox, - NumberPicker, - Radio, - Form, - Tab -} from '@alife/next' - -import '@alife/next/dist/next.min.css' - -const renderSchema = {} - -const props = { - UI: { - version: '0.x', - Button, - Accordion, - Toast: Feedback.toast, - Upload, - Input, - Select, - Icon, - DatePicker, - Checkbox, - NumberPicker, - Radio, - RadioGroup: Radio.Group, - TabPane: Tab.TabPane, - Form, - Tab - }, - // 主题: dark/light,默认dark - // themeStyle: 'light', - // 是否展示布局组件,默认为false - showLayoutField: false, - showPreviewBtn: true, - showSourceCodeBtn: true, - // 控制返回按钮点击事件 - onBackBtnClick: () => { - alert('点击了返回') - }, - includeFieldListKeyList: ['input', 'number', 'radio', 'date', 'month'], - // 额外全局按钮 - globalButtonList: [ - // { - // key: 'submit', - // title: '自定义保存', - // render: (props) => { - // return {props.children} - // }, - // props: { - // // loading: true, - // }, - // }, { - // key: 'cancel', - // title: '取消', - // props: { - // onClick: () => { - // alert('点击取消'); - // } - // }, - // } - ], - // 是否展示全局配置 - showGlobalCfg: true, - // 全局配置额外项 - extraGlobalCfgList: [], - globalCfg: {}, - supportFieldList: [], - // includeFieldListKeyList: ['input', 'select'], - - // 渲染引擎 - renderEngine: SchemaForm, - - schema: renderSchema, - // onChange: (data) => { - // console.info('index onChange data', data); - // }, - onSubmit: data => { - // eslint-disable-next-line no-console - console.info('index onSubmit data', data) - } -} - -class Comp extends React.Component { - constructor(props) { - super(props) - this.state = { - schema: renderSchema - } - } - - render() { - return ( -
    - -
    - ) - } -} - -export default Comp diff --git a/packages/builder/src/index.js b/packages/builder/src/index.js deleted file mode 100644 index 7cbad14a487..00000000000 --- a/packages/builder/src/index.js +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { Provider } from 'react-redux' -import { createStore, applyMiddleware } from 'redux' -import { createLogger } from 'redux-logger' -import thunk from 'redux-thunk' -import rootReducer from './reducers' -import App from './App' -import ThemeList, { THEME_ENUM, DEFAULT_THEME } from './configs/theme' -import { ThemeProvider } from 'styled-components' -import { DragDropContext } from 'react-dnd' -import HTML5Backend from 'react-dnd-html5-backend' - -const logger = createLogger({ - collapsed: true -}) - -const middleware = [ - thunk, - // The address has av_debug=true to play the logger - window.location.href.indexOf('av_debug=true') > -1 && logger -].filter(Boolean) - -const initialState = { - componentId: [], - preview: false, - codemode: false, - componentProps: {}, - gbConfig: { - action: '', - labelCol: 8, - wrapperCol: 8, - labelAlign: 'left', - labelTextAlign: 'right', - autoAddColon: true, - needFormButtonGroup: false, - inline: false, - size: 'medium' - }, - initSchemaData: { - type: 'object', - properties: {} - } -} - -const store = createStore( - rootReducer, - initialState, - applyMiddleware(...middleware) -) - -class Component extends React.Component { - static propTypes = { - themeStyle: PropTypes.string - } - - static defaultProps = { - themeStyle: DEFAULT_THEME - } - - render() { - const props = { ...this.props } - let { themeStyle } = props - - // Can only pass in one of the two enumerated values of dark/light - if (THEME_ENUM.indexOf(themeStyle) === -1) { - console.error('themeStyle must be dark/light') - themeStyle = DEFAULT_THEME - } - - // Avoid the theme attribute passing in conflict with style-components - if (props.theme) { - console.warn('the theme attribute will be ignore') - delete props.theme - } - - return ( - - - - - - ) - } -} - -export default DragDropContext(HTML5Backend)(Component) diff --git a/packages/builder/src/reducers/codemode.js b/packages/builder/src/reducers/codemode.js deleted file mode 100644 index 65bdff5bb28..00000000000 --- a/packages/builder/src/reducers/codemode.js +++ /dev/null @@ -1,12 +0,0 @@ -export default (state = false, action) => { - let newState - const { data = {} } = action - const _codemode = data.codemode || false - switch (action.type) { - case 'CHANGE_CODEMODE': - newState = _codemode - return newState - default: - return state - } -} diff --git a/packages/builder/src/reducers/componentId.js b/packages/builder/src/reducers/componentId.js deleted file mode 100644 index 5e38e2550af..00000000000 --- a/packages/builder/src/reducers/componentId.js +++ /dev/null @@ -1,12 +0,0 @@ -export default (state = [], action) => { - let newState = [...state] - const { data: { componentId = [] } = {}, type } = action - - switch (type) { - case 'CHANGE_COMPONENT': - newState = Array.isArray(componentId) ? componentId : [componentId] - return newState - default: - return state - } -} diff --git a/packages/builder/src/reducers/componentProps.js b/packages/builder/src/reducers/componentProps.js deleted file mode 100644 index 5f02182220d..00000000000 --- a/packages/builder/src/reducers/componentProps.js +++ /dev/null @@ -1,50 +0,0 @@ -import { getPropsByKey } from '../configs/supportConfigList' - -export default (state = {}, action) => { - const newState = Object.assign({}, state) - const { data = {} } = action - const { id, propsData = {}, comp = {} } = data - - switch (action.type) { - case 'SHOW_COMPONENT_PROPS': - if (id && !newState[id]) { - newState[id] = getPropsByKey(comp.key).map(item => { - const { name } = item - return Object.assign( - {}, - item, - comp[name] - ? { - value: comp[name] - } - : {} - ) - }) - } - return newState - case 'DELETE_COMPONENT': - if (id && newState[id]) { - delete newState[id] - } - return newState - case 'EDIT_COMPONENT_PROPS': - if (id && newState[id]) { - newState[id] = newState[id].map(item => { - const { name } = item - const value = propsData[name] - return Object.assign( - {}, - item, - value !== undefined - ? { - value - } - : {} - ) - }) - } - return newState - default: - return state - } -} diff --git a/packages/builder/src/reducers/gbConfig.js b/packages/builder/src/reducers/gbConfig.js deleted file mode 100644 index 3ad0324dbf2..00000000000 --- a/packages/builder/src/reducers/gbConfig.js +++ /dev/null @@ -1,17 +0,0 @@ -import { defaultGlobalCfgValue } from '../configs/supportGlobalCfgList' - -export default (state = {}, action) => { - let newState = { - ...defaultGlobalCfgValue, - ...state - } - const { data = {} } = action - - switch (action.type) { - case 'CHANGE_GB_CONFIG': - newState = Object.assign({}, newState, data) - return newState - default: - return newState - } -} diff --git a/packages/builder/src/reducers/index.js b/packages/builder/src/reducers/index.js deleted file mode 100644 index 073d0d18560..00000000000 --- a/packages/builder/src/reducers/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import { combineReducers } from 'redux' - -import preview from './preview' -import codemode from './codemode' -import componentId from './componentId' -import componentProps from './componentProps' -import gbConfig from './gbConfig' -import initSchemaData from './initSchemaData' - -export default combineReducers({ - componentId, - preview, - codemode, - componentProps, - gbConfig, - initSchemaData -}) diff --git a/packages/builder/src/reducers/initSchemaData.js b/packages/builder/src/reducers/initSchemaData.js deleted file mode 100644 index ff8bb1d0ee5..00000000000 --- a/packages/builder/src/reducers/initSchemaData.js +++ /dev/null @@ -1,193 +0,0 @@ -import { getOrderProperties, initOrderProperties } from '../utils/util' -import merge from 'lodash.merge' - -export default (state = {}, action) => { - let newState = { - ...state - } - const { data = {} } = action - const { - component, - id, - targetId, - propsData = {}, - existId = null, - containerId = [] - } = data - - const loop = (obj, idArr = []) => { - const _idArr = [...idArr] - const _id = _idArr.shift() - if (!_idArr.length) return obj.properties[_id] - return loop(obj.properties[_id], _idArr) - } - - const getProperties = (obj, idArr = []) => { - const _idArr = [...idArr] - const _id = _idArr.shift() - if (!_idArr.length) return obj.properties - return getProperties(obj.properties[_id], _idArr) - } - - const setProperties = (obj, idArr = [], prop) => { - const _idArr = [...idArr] - if (!_idArr.length) { - obj.properties = prop - } else { - const _id = _idArr.shift() - setProperties(obj.properties[_id], _idArr, prop) - } - } - - const deleteItem = (obj, idArr = []) => { - const _idArr = [...idArr] - const _id = _idArr.shift() - if (!_idArr.length) { - delete obj.properties[_id] - } else { - deleteItem(obj.properties[_id], _idArr) - } - } - - switch (action.type) { - case 'INIT_SCHEMA': - // 自动生成z-index顺序 - const newSchema = initOrderProperties(data) - newState = { - ...newState, - ...newSchema - } - return newState - case 'MOVE_COMOPNENT': - const sourceItem = loop(newState, [...id]) - const targetItem = loop(newState, [...targetId]) - - deleteItem(newState, [...id]) - - targetItem.properties[sourceItem.id] = sourceItem - - return newState - case 'CHANGE_COMPONENT_ORDER': - const _propertiesList = getOrderProperties(newState, [...containerId]) - const _sourceItem = loop(newState, [...id]) - const _targetItem = loop(newState, [...targetId]) - const targetIdx = _targetItem['x-index'] - const sourceIdx = _sourceItem['x-index'] - - if (id.length !== targetId.length) { - alert('目前只支持同级别组件的顺序替换') - return newState - } - - _propertiesList[targetIdx] = { - ..._sourceItem, - 'x-index': targetIdx - } - _propertiesList[sourceIdx] = { - ..._targetItem, - 'x-index': sourceIdx - } - - const _properties11 = {} - _propertiesList.forEach(item => { - _properties11[item.id] = { - ...item - } - }) - - setProperties(newState, containerId, _properties11) - - return newState - case 'ADD_COMPONENT': - const propertiesList1 = getOrderProperties(newState, [...containerId]) - - if (existId) { - // 在特定的existId之前插入新的组件 - const propLen = propertiesList1.length - const item = propertiesList1.find(_item => _item.id === existId) - const idx = item['x-index'] - for (let i = propLen; i > idx; i--) { - propertiesList1[i] = { - ...propertiesList1[i - 1], - 'x-index': i - } - } - propertiesList1[idx] = { - ...component, - id, - 'x-index': idx - } - } else { - // 在最后插入新的组件 - propertiesList1[propertiesList1.length] = { - ...component, - id, - 'x-index': propertiesList1.length - } - } - - const _properties1 = {} - propertiesList1.forEach(item => { - _properties1[item.id] = { - ...item - } - }) - - setProperties(newState, containerId, _properties1) - - if (!newState.type) { - newState.type = 'object' - } - - return newState - case 'EDIT_COMPONENT': - const _data_ = getProperties(newState, id) - const lastId = [...id].pop() - - Object.keys(_data_).forEach(compId => { - if (compId) { - _data_[compId] = merge( - {}, - _data_[compId], - id === null || compId === lastId - ? { - active: true, - ...propsData - } - : { - active: false - } - ) - - // hack - if ( - propsData['x-props'] && - propsData['x-props'].enum && - Array.isArray(propsData['x-props'].enum) && - (id === null || compId === lastId) - ) { - _data_[compId]['x-props'] = _data_[compId]['x-props'] || {} - _data_[compId]['x-props'].enum = propsData['x-props'].enum - _data_[compId].enum = propsData['x-props'].enum - } - if ( - propsData['x-props'] && - propsData['x-props'].requestOptions && - propsData['x-props'].requestOptions.data && - (id === null || compId === lastId) - ) { - _data_[compId]['x-props'].requestOptions = - _data_[compId]['x-props'].requestOptions || {} - _data_[compId]['x-props'].requestOptions.data = - propsData['x-props'].requestOptions.data - } - } - }) - return newState - case 'DELETE_COMPONENT': - deleteItem(newState, [...id]) - return newState - default: - return state - } -} diff --git a/packages/builder/src/reducers/preview.js b/packages/builder/src/reducers/preview.js deleted file mode 100644 index 69c236b4a6c..00000000000 --- a/packages/builder/src/reducers/preview.js +++ /dev/null @@ -1,12 +0,0 @@ -export default (state = false, action) => { - let newState - const { data = {} } = action - const _preview = data.preview || false - switch (action.type) { - case 'CHANGE_PREVIEW': - newState = _preview - return newState - default: - return state - } -} diff --git a/packages/builder/src/style.js b/packages/builder/src/style.js deleted file mode 100644 index fede0d2cc1c..00000000000 --- a/packages/builder/src/style.js +++ /dev/null @@ -1,174 +0,0 @@ -import styled from 'styled-components' - -export default styled.div` - position: relative; - min-width: 600px; - overflow: hidden; - .next-form-item { - margin-bottom: 0; - } - .next-checkbox-label { - color: ${props => props.theme.whiteColor}; - } - .preview-main .next-checkbox-label { - color: #333; - } - .schemaform-header { - position: relative; - height: 64px; - background: ${props => props.theme.headerBgColor}; - overflow: hidden; - &::after { - content: ""; - clear: both; - display: table; - } - box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.15), 0 0 16px 0 rgba(0, 0, 0, 0.15); - z-index: 2; - - } - .schemaform-back { - position: absolute; - left: 0; - top: 0; - width: 64px; - height: 100%; - text-indent: -999em; - &::before { - content: ""; - position: absolute; - left: 27px; - top: 24px; - width: 9px; - height: 17px; - background: url('${props => - props.theme.backIconUrl}') no-repeat center center; - background-size: 9px 17px; - } - &::after { - content: ""; - position: absolute; - top: 20px; - right: 0; - height: 24px; - width: 1px; - background: ${props => props.theme.backDividerBgColor}; - box-shadow: ${props => props.theme.backDividerShadow}; - } - } - h1 { - position: absolute; - left: 88px; - top: 0; - margin: 0; - font-size: 24px; - font-weight: normal; - line-height: 64px; - color: ${props => props.theme.whiteColor}; - } - .schemaform-header-btns { - float: right; - margin: 14px 24px 0 0; - button { - margin-left: 24px; - height: 36px; - line-height: 36px; - background: ${props => props.theme.btnNormalBgColor}; - color: ${props => props.theme.whiteColor}; - border: none; - box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.25); - &.next-btn-primary { - background: ${props => props.theme.btnPrimaryBgColor}; - color: ${props => props.theme.btnPrimaryTxtColor}; - } - } - } - .schamaform-content { - position: relative; - overflow: hidden; - padding: 0 340px 0 240px; - - &::after { - content: ""; - clear: both; - display: table; - } - .content-col-left { - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 240px; - overflow-y: scroll; - background: ${props => props.theme.leftColBgColor}; - } - .content-col-right { - position: absolute; - top: 0; - right: 0; - width: 340px; - bottom: 0; - background: ${props => props.theme.rightColBgColor}; - overflow-y: scroll; - } - .content-col-main { - position: relative; - height: 100%; - background: ${props => props.theme.mainColBgColor}; - overflow-y: scroll; - } - } - // 复写文件上传组件宽度 - .next-upload-list-text .next-upload-list-item { - max-width: 200px; - } - .schema-form-container .next-form-top .next-form-item-label { - margin-bottom: 0 !important; - } - .next-accordion, .next-collapse { - border: none; - } - .next-accordion-section-title, .next-collapse-panel-title { - background: none; - user-select: none; - color: ${props => props.theme.whiteColor}; - border: none; - &:hover { - background: none; - } - } - .next-collapse-panel:not(:first-child) { - border-top: none; - } - .next-accordion-section, .next-collapse-panel { - position: relative; - &::after { - content: ""; - position: absolute; - left: 0; - bottom: 0; - width: 100%; - height: 1px; - background: ${props => props.theme.dividerBgColor}; - box-shadow: ${props => props.theme.dividerShadow}; - } - &:last-child { - &::after { - display: none; - } - } - } - .next-accordion-section-content, .next-collapse-panel-content { - background: none; - .next-form .next-form-item-label, .next-radio-group .next-radio-label { - color: ${props => props.theme.whiteColor}; - font-size: 12px; - } - } - .next-collapse .next-collapse-panel-icon { - color: ${props => props.theme.whiteColor}; - } - .next-accordion .next-accordion-icon:before { - color: ${props => props.theme.whiteColor}; - } -` diff --git a/packages/builder/src/utils/arg.js b/packages/builder/src/utils/arg.js deleted file mode 100644 index 3cf6a5c922d..00000000000 --- a/packages/builder/src/utils/arg.js +++ /dev/null @@ -1,313 +0,0 @@ -/* - arg.js - v1.4 - JavaScript URL argument processing once and for all. - by Mat Ryer and Ryan Quinn - Copyright (c) 2013 Stretchr, Inc. - Please consider promoting this project if you find it useful. - Permission is hereby granted, free of charge, to any person obtaining a copy of this - software and associated documentation files (the "Software"), to deal in the Software - without restriction, including without limitation the rights to use, copy, modify, merge, - publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons - to whom the Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in all copies - or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR - PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE - FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR - OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. -*/ - -/* eslint-disable */ -export default (function () { - - /** @namespace - */ - var Arg = function () { - return Arg.get.apply(global, arguments); - }; - Arg.version = "1.4.0"; - - /** - * Parses the arg string into an Arg.Arg object. - */ - Arg.parse = function (s) { - if (!s) return {}; - if (s.indexOf("=") === -1 && s.indexOf("&") === -1) return {}; - s = Arg._cleanParamStr(s); - var obj = {}; - var pairs = s.split("&"); - for (var pi in pairs) { - if (pairs.hasOwnProperty(pi)) { - var kvsegs = pairs[pi].split("="); - var key = decodeURIComponent(kvsegs[0]), val = Arg.__decode(kvsegs[1]); - Arg._access(obj, key, val); - } - } - return obj; - }; - - /** - * Decodes a URL component (including resolving + to spaces) - */ - Arg.__decode = function (s) { - while (s && s.indexOf("+") > -1) { - s = s.replace("+", " "); - } - s = decodeURIComponent(s); - return s; - }; - - /** - * Helper method to get/set deep nested values in an object based on a string selector - * - * @param {Object} obj Based object to either get/set selector on - * @param {String} selector Object selector ie foo[0][1].bar[0].baz.foobar - * @param {Mixed} value (optional) Value to set leaf located at `selector` to. - * If value is undefined, operates in 'get' mode to return value at obj->selector - * @return {Mixed} - */ - Arg._access = function (obj, selector, value) { - var shouldSet = typeof value !== "undefined"; - var selectorBreak = -1; - var coerce_types = { - 'true': true, - 'false': false, - 'null': null - }; - - // selector could be a number if we're at a numerical index leaf in which case selector.search is not valid - if (typeof selector == 'string' || Object.prototype.toString.call(selector) == '[object String]') { - selectorBreak = selector.search(/[\.\[]/); - } - - // No dot or array notation so we're at a leaf, set value - if (selectorBreak === -1) { - if (Arg.coerceMode) { - value = value && !isNaN(value) ? +value // number - : value === 'undefined' ? undefined // undefined - : coerce_types[value] !== undefined ? coerce_types[value] // true, false, null - : value; // string - } - return shouldSet ? (obj[selector] = value) : obj[selector]; - } - - // Example: - // selector = 'foo[0].bar.baz[2]' - // currentRoot = 'foo' - // nextSelector = '0].bar.baz[2]' -> will be converted to '0.bar.baz[2]' in below switch statement - var currentRoot = selector.substr(0, selectorBreak); - var nextSelector = selector.substr(selectorBreak + 1); - - switch (selector.charAt(selectorBreak)) { - case '[': - // Intialize node as an array if we haven't visted it before - obj[currentRoot] = obj[currentRoot] || []; - nextSelector = nextSelector.replace(']', ''); - - if (nextSelector.search(/[\.\[]/) === -1 && nextSelector.search(/^[0-9]+$/) > -1) { - nextSelector = parseInt(nextSelector, 10); - } - - return Arg._access(obj[currentRoot], nextSelector, value); - case '.': - // Intialize node as an object if we haven't visted it before - obj[currentRoot] = obj[currentRoot] || {}; - return Arg._access(obj[currentRoot], nextSelector, value); - } - - return obj; - }; - - /** - * Turns the specified object into a URL parameter string. - */ - Arg.stringify = function (obj, keyPrefix) { - - switch (typeof (obj)) { - case "object": - var segs = []; - var thisKey; - for (var key in obj) { - - if (!obj.hasOwnProperty(key)) continue; - var val = obj[key]; - - if (typeof (key) === "undefined" || key.length === 0 || typeof (val) === "undefined" || val === null || val.length === 0) continue; - - thisKey = keyPrefix ? keyPrefix + "." + key : key; - - if (typeof obj.length !== "undefined") { - thisKey = keyPrefix ? keyPrefix + "[" + key + "]" : key; - } - - if (typeof val === "object") { - segs.push(Arg.stringify(val, thisKey)); - } else { - segs.push(encodeURIComponent(thisKey) + "=" + encodeURIComponent(val)); - } - - } - return segs.join("&"); - } - - return encodeURIComponent(obj); - - }; - - /** - * Generates a URL with the given parameters. - * (object) = A URL to the current page with the specified parameters. - * (path, object) = A URL to the specified path, with the object of parameters. - * (path, object, object) = A URL to the specified path with the first object as query parameters, - * and the second object as hash parameters. - */ - Arg.url = function () { - - var sep = (Arg.urlUseHash ? Arg.hashQuerySeperator : Arg.querySeperator); - var segs = [window.location.pathname, sep]; - var args = {}; - - switch (arguments.length) { - case 1: // Arg.url(params) - segs.push(Arg.stringify(arguments[0])); - break; - case 2: // Arg.url(path, params) - segs[0] = Arg._cleanPath(arguments[0]); - args = Arg.parse(arguments[0]); - args = Arg.merge(args, arguments[1]); - segs.push(Arg.stringify(args)); - break; - case 3: // Arg.url(path, query, hash) - segs[0] = Arg._cleanPath(arguments[0]); - segs[1] = Arg.querySeperator; - segs.push(Arg.stringify(arguments[1])); - (typeof (arguments[2]) === "string") ? segs.push(Arg.hashSeperator) : segs.push(Arg.hashQuerySeperator); - segs.push(Arg.stringify(arguments[2])); - } - - var s = segs.join(""); - - // trim off sep if it's the last thing - if (s.indexOf(sep) == s.length - sep.length) { - s = s.substr(0, s.length - sep.length); - } - - return s; - - }; - - /** urlUseHash tells the Arg.url method to always put the parameters in the hash. */ - Arg.urlUseHash = false; - - /** The string that seperates the path and query parameters. */ - Arg.querySeperator = "?"; - - /** The string that seperates the path or query, and the hash property. */ - Arg.hashSeperator = "#"; - - /** The string that seperates the the path or query, and the hash query parameters. */ - Arg.hashQuerySeperator = "#?"; - - /** When parsing values if they should be coerced into primitive types, ie Number, Boolean, Undefined */ - Arg.coerceMode = true; - - /** - * Gets all parameters from the current URL. - */ - Arg.all = function () { - var merged = Arg.parse(Arg.querystring() + "&" + Arg.hashstring()); - return Arg._all ? Arg._all : Arg._all = merged; - }; - - /** - * Gets a parameter from the URL. - */ - Arg.get = function (selector, def) { - var val = Arg._access(Arg.all(), selector); - return typeof (val) === "undefined" ? def : val; - }; - - /** - * Gets the query string parameters from the current URL. - */ - Arg.query = function () { - return Arg._query ? Arg._query : Arg._query = Arg.parse(Arg.querystring()); - }; - - /** - * Gets the hash string parameters from the current URL. - */ - Arg.hash = function () { - return Arg._hash ? Arg._hash : Arg._hash = Arg.parse(Arg.hashstring()); - }; - - /** - * Gets the query string from the URL (the part after the ?). - */ - Arg.querystring = function () { - return Arg._cleanParamStr(window.location.search); - }; - - /** - * Gets the hash param string from the URL (the part after the #). - */ - Arg.hashstring = function () { - var rawHref = window.location.href; - var hashIndex = rawHref.indexOf("#"); - var hash = hashIndex >= 0 ? rawHref.substr(hashIndex) : ""; - return Arg._cleanParamStr(hash); - }; - - /* - * Cleans the URL parameter string stripping # and ? from the beginning. - */ - Arg._cleanParamStr = function (s) { - - if (s.indexOf(Arg.querySeperator) > -1) - s = s.split(Arg.querySeperator)[1]; - - if (s.indexOf(Arg.hashSeperator) > -1) - s = s.split(Arg.hashSeperator)[1]; - - if (s.indexOf("=") === -1 && s.indexOf("&") === -1) - return ""; - - while (s.indexOf(Arg.hashSeperator) === 0 || s.indexOf(Arg.querySeperator) === 0) - s = s.substr(1); - - return s; - }; - - Arg._cleanPath = function (p) { - - if (p.indexOf(Arg.querySeperator) > -1) - p = p.substr(0, p.indexOf(Arg.querySeperator)); - - if (p.indexOf(Arg.hashSeperator) > -1) - p = p.substr(0, p.indexOf(Arg.hashSeperator)); - - return p; - }; - - /** - * Merges all the arguments into a new object. - */ - Arg.merge = function () { - var all = {}; - for (var ai in arguments) { - if (arguments.hasOwnProperty(ai)) { - for (var k in arguments[ai]) { - if (arguments[ai].hasOwnProperty(k)) { - all[k] = arguments[ai][k]; - } - } - } - } - return all; - }; - - return Arg; - -})(); \ No newline at end of file diff --git a/packages/builder/src/utils/baseForm.js b/packages/builder/src/utils/baseForm.js deleted file mode 100644 index c0708c9b0b5..00000000000 --- a/packages/builder/src/utils/baseForm.js +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 统一从这里引用uform,方便后续底层升级做适配 - */ -export * from '@uform/react' diff --git a/packages/builder/src/utils/comp.js b/packages/builder/src/utils/comp.js deleted file mode 100644 index b83a634b484..00000000000 --- a/packages/builder/src/utils/comp.js +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react' -import styled from 'styled-components' - -/** - * 分割线 - */ -export const Divider = styled.div` - height: 1px; - background: #1e2336; - box-shadow: 0 1px 0 0 #313853; -` - -/** - * 头部title - */ -export const Header = styled(props => ( -
    {props.children}
    -))` - padding-left: 16px; - height: 74px; - h2 { - margin: 0; - padding: 0; - font-size: 14px; - height: 40px; - line-height: 45px; - color: ${props => props.theme.whiteColor}; - } - p { - margin: 0; - padding: 0; - font-size: 12px; - color: #9096ad; - } -` - -export const CustomIcon = styled(props => ( - -))` - display: inline-block; - margin-right: 8px; - vertical-align: -2px; - width: ${props => props.width || '15'}px; - height: ${props => props.height || '15'}px; - background-repeat: no-repeat; - background-position: center center; - background-size: ${props => props.width || '15'}px - ${props => props.height || '15'}px; -` - -export default { - Divider, - Header, - CustomIcon -} diff --git a/packages/builder/src/utils/lang.js b/packages/builder/src/utils/lang.js deleted file mode 100644 index ab2361af761..00000000000 --- a/packages/builder/src/utils/lang.js +++ /dev/null @@ -1,143 +0,0 @@ -import { parseDesturctPath } from '@uform/utils' -import moment from 'moment' -import Arg from './arg' - -export const isType = type => obj => - obj != null && Object.prototype.toString.call(obj) === `[object ${type}]` - -export const isFn = isType('Function') - -export const isArr = Array.isArray || isType('Array') - -export const isObj = isType('Object') - -export const isStr = isType('String') - -export const isNum = isType('Number') - -export const isIter = obj => isArr(obj) || isObj(obj) - -const replaceSingleDefault = v => { - if (!isFlagValue(v)) return v - - const { type, flag, value } = v - - const now = moment(Date.now()) - const params = Arg.all() - - if (flag === 'weekRange') { - if (type === 'pastStart') { - return now.subtract(value, 'weeks').format('YYYY-MM-DD') - } else if (type === 'future') { - return now.add(value, 'weeks').format('YYYY-MM-DD') - } else if (type === 'specify') { - return value - } - } else if (flag === 'date') { - if (type === 'past') { - return now.subtract(value, 'days').format('YYYY-MM-DD') - } else if (type === 'future') { - return now.add(value, 'days').format('YYYY-MM-DD') - } else if (type === 'now') { - return now.format('YYYY-MM-DD') - } else if (type === 'specify') { - return value - } else if (type === 'url') { - return params[value] - } - } else if (flag === 'time') { - if (type === 'specify') { - return value - } else if (type === 'now') { - return now.format('HH:MM:SS') - } else if (type === 'url') { - return params[value] - } - } else if (flag === 'month') { - if (type === 'past') { - return now.subtract(value, 'months').format('YYYY-MM') - } else if (type === 'future') { - return now.add(value, 'months').format('YYYY-MM') - } else if (type === 'now') { - return now.format('YYYY-MM') - } else if (type === 'specify') { - return value - } else if (type === 'url') { - return params[value] - } - } else if (type === 'specify') { - return value - } else if (type === 'url') { - return params[value] - } -} - -const replaceDefault = declaredValue => { - if (isArr(declaredValue)) { - const v = [] - for (let i = 0; i < declaredValue.length; i++) { - const _v = replaceSingleDefault(declaredValue[i]) - v.push(_v) - } - return v - } else { - return replaceSingleDefault(declaredValue) - } -} - -const isFlagValue = o => { - if (isArr(o)) { - return o.some(i => isObj(i) && typeof i.flag !== 'undefined') - } else { - return isObj(o) && typeof o.flag !== 'undefined' - } -} - -const getDefault = (v, path) => { - const dPath = parseDesturctPath(path).destruct || path - if (isArr(v) && isArr(dPath)) { - return v.map(i => { - if (isFlagValue(i)) { - i = replaceDefault(i, path) - } - return i - }) - } else if (isFlagValue(v)) { - return replaceDefault(v, path) - } else { - return v - } -} - -const normalizeDefault = (properties, _buf = {}) => - Object.keys(properties).reduce((buf, k) => { - if ( - properties[k].properties && - Object.keys(properties[k].properties).length - ) { - buf[k] = { - type: 'object', - properties: {}, - ...properties[k] - } - normalizeDefault(properties[k].properties, buf[k].properties) - } else { - buf[k] = { - ...properties[k], - default: getDefault(properties[k].default, k) - } - } - return buf - }, _buf) - -export const normalizeSchema = schema => { - if (!schema) return null - const { properties = {}, type = 'object' } = schema - const _properties = normalizeDefault(properties) - - return { - type, - ...schema, - properties: _properties - } -} diff --git a/packages/builder/src/utils/util.js b/packages/builder/src/utils/util.js deleted file mode 100644 index a0f90834b7c..00000000000 --- a/packages/builder/src/utils/util.js +++ /dev/null @@ -1,267 +0,0 @@ -import React from 'react' -import merge from 'lodash.merge' - -export * from './comp' - -/** - * 判断对象是否为空 - * @param {Object} obj 对象 - */ -export const isEmptyObj = obj => { - if (!obj) return true - for (const i in obj) { - if (Object.prototype.hasOwnProperty.call(obj, i)) { - return false - } - } - return true -} - -/** - * 将enums数组格式化成[{value: xxx, label: xxx}]形式 - * @param {Array} enums 需要格式化的数组 - */ -export const wrapEnums = enums => - enums.map(item => - typeof item === 'object' - ? item - : { - value: item, - label: item - } - ) - -/** - * 初始化组件的属性默认值 - * @param {Array} propsList 属性数组 - * @param {Object} comp 组件数据 - */ - -/** - * 根据组件id获取组件信息 - * @param {Array} componentIdList 组件的id list - * @param {Array} schema 组件schema - */ -export const getCompDetailById = (componentIdList = [], schema = {}) => { - const _componentIdList = [...componentIdList] - const _componentId = _componentIdList.shift() - const { properties = {} } = schema - - if (!_componentIdList.length) { - return properties[_componentId] - ? { - id: _componentId, - ...properties[_componentId] - } - : {} - } - - return getCompDetailById(_componentIdList, properties[_componentId]) -} - -/** - * 补全回传的schema格式 - * @param {Object} schema - * @param {Boolean} keepAll 保留所有字段 - */ -export const wrapSubmitSchema = (schema, keepAll = false) => { - if (!schema || typeof schema !== 'object') { - return { - type: 'object', - properties: {} - } - } - - // 深拷贝一份 - const result = JSON.parse(JSON.stringify(schema)) - - if (!schema.type) { - result.type = 'object' - } - if (!schema.properties) { - result.properties = {} - } - - // 以__id__为主键转换一次数据 - const newProperties = {} - const loop = (_newProperties, _properties) => { - Object.keys(_properties).forEach(key => { - const item = JSON.parse(JSON.stringify(_properties[key])) - const newKey = item.__id__ || key - if (!keepAll) { - // 删除可视化配置产生的冗余字段 - Object.keys(item).forEach(itemKey => { - if ( - [ - '__id__', - 'width', - 'height', - 'icon', - 'iconWidth', - 'iconHeight', - 'iconUrl', - 'id', - 'active', - 'placeholder' - ].indexOf(itemKey) > -1 || - /^(x-props.)/gi.test(itemKey) - ) { - delete item[itemKey] - } - }) - } - - _newProperties[newKey] = JSON.parse(JSON.stringify(item)) - - if (item.properties) { - _newProperties[newKey].properties = {} - loop(_newProperties[newKey].properties, item.properties) - } - }) - } - - loop(newProperties, result.properties) - - result.properties = newProperties - - return result -} - -/** - * 根据schema获取有顺序的properties - * @param {Object} schema - * @param {String} containerId 相对容器id - */ -export const getOrderProperties = (schema = {}, containerId = []) => { - if (containerId.length) { - const id = containerId.shift() - return getOrderProperties(schema.properties[id], containerId) - } - - const { properties = {} } = schema - if (isEmptyObj(properties)) return [] - - let newProperties = [] - - Object.keys(properties).forEach(key => { - const item = properties[key] - const index = item['x-index'] - if (typeof index !== 'number') { - newProperties.push({ - ...item, - id: key, - 'x-index': newProperties.length - }) - } - }) - Object.keys(properties).forEach(key => { - const item = properties[key] - const index = item['x-index'] - if (typeof index === 'number') { - if (!newProperties[index]) { - const _key = - index > newProperties.length + 1 ? newProperties.length : index - newProperties[_key] = { - ...item, - id: key - } - } else { - const _tempProperties = newProperties.slice(0, index) - for (let i = index; i < newProperties.length; i++) { - _tempProperties[i + 1] = { - ...newProperties[i], - 'x-index': i + 1 - } - } - _tempProperties[index] = { - ...item, - id: key - } - newProperties = _tempProperties - } - } - }) - - return newProperties -} - -export const initOrderProperties = (schema = {}) => { - const newProperties = getOrderProperties(schema) - const properties = {} - newProperties.forEach(item => { - const newItem = { ...item } - if (newItem.active) { - delete newItem.active - } - properties[item.id] = newItem - }) - const newShema = { - ...schema, - properties - } - - return newShema -} - -export const flatObj = (obj = {}) => { - // 深拷贝一份 - const result = JSON.parse(JSON.stringify(obj)) - const setValueByLoopObj = (_obj, arr, value) => { - const _key = arr.shift() - if (!arr.length) { - if (value && typeof value === 'object') { - if (Array.isArray(value)) { - _obj[_key] = value - } else { - const tempValue = _obj[_key] || {} - _obj[_key] = merge({}, value, tempValue) - } - } else { - _obj[_key] = value - } - } else { - _obj[_key] = _obj[_key] || {} - setValueByLoopObj(_obj[_key], arr, value) - } - } - Object.keys(obj).forEach(originKey => { - const key = originKey.split('.') - setValueByLoopObj(result, key, obj[originKey]) - }) - return result -} - -// 校验schema id是否有重复的 -export const checkRepeatId = (schema = {}) => { - const result = {} - const loop = _schema => { - const temp = {} - const { properties = {} } = _schema - Object.keys(properties).forEach(propKey => { - const item = properties[propKey] - const key = item.__id__ ? item.__id__ : propKey - if (!temp[key]) { - temp[key] = item - } else { - result[key] = item - } - if (item.properties) { - loop(item) - } - }) - } - loop(schema) - return !!Object.keys(result).length -} - -export const wrapComp2Class = Comp => - class extends React.Component { - render() { - return - } - } - -export const isLayoutWrapper = comp => - comp['x-props'] && - comp['x-props']._extra && - comp['x-props']._extra.__key__ === 'layout' diff --git a/packages/builder/tsconfig.json b/packages/builder/tsconfig.json deleted file mode 100644 index 35d009e80e5..00000000000 --- a/packages/builder/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./lib", - "declaration": false, - "allowJs": true, - "skipLibCheck": true - }, - "include": ["./src/**/*.js"], - "exclude": ["./src/__tests__/*"] -} diff --git a/packages/core/README.md b/packages/core/README.md index 624c47a4d3a..2f71df748bb 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,2 +1,30 @@ # @uform/core -> UForm 内核包 \ No newline at end of file +> UForm 内核包 + +## quick start + +```jsx +import { createForm, LifeCycleTypes, FormLifeCycle, FormPath } from './src' + +const form = createForm() +// form.registerField({ path: 'a', rules: ['number'] }) // string +// form.registerField({ path: 'b', rules: [() => ({ type: 'warning', message: 'warning msg' })] }) // CustomValidator +// form.registerField({ path: 'c', rules: [() => ({ type: 'error', message: 'warning msg' })] }) // CustomValidator +// form.registerField({ path: 'd', rules: [() => 'straight error msg'] }) // CustomValidator +// form.registerField({ path: 'e', rules: [{ required: true, message: 'desc msg' }] }) // ValidateDescription + +form.registerField({ path: 'a', rules: [(value) => { + console.log('==>valuevalue', value); + return value === undefined ? { type: 'error', message: 'a is required' } : null +}] }) +form.registerField({ path: 'b', rules: [(value) => { + return value === undefined ? { type: 'warning', message: 'b is required' } : null +}] }) +// form.setFieldValue('a', 1) +const result = form.validate(); +result.then(({ warnings, errors }) => { + console.log('warnings', warnings); + console.log('errors', errors); +}); + +``` \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index 404fd992d68..41fa458efb1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@uform/core", - "version": "0.4.3", + "version": "0.4.0", "license": "MIT", "main": "lib", "repository": { @@ -16,7 +16,7 @@ "npm": ">=3.0.0" }, "scripts": { - "build": "tsc --declaration" + "build": "rm -rf lib && tsc --declaration" }, "devDependencies": { "typescript": "^3.5.2" @@ -26,11 +26,10 @@ "scheduler": ">=0.11.2" }, "dependencies": { - "@uform/types": "^0.4.3", - "@uform/utils": "^0.4.3", - "@uform/validator": "^0.4.3", - "dot-match": "^0.1.18", - "rxjs": "^6.3.3" + "@uform/types": "^0.4.0", + "@uform/shared": "^0.4.0", + "@uform/validator": "^0.4.0", + "immer": "^3.2.0" }, "publishConfig": { "access": "public" diff --git a/packages/core/src/__test__/form.spec.js b/packages/core/src/__test__/form.spec.js deleted file mode 100644 index 097cf68678f..00000000000 --- a/packages/core/src/__test__/form.spec.js +++ /dev/null @@ -1,46 +0,0 @@ -import { createForm } from '../index' - -test('Increase lastValidateValue value processing during initialization', async () => { - const inpueFieldValidate = jest.fn() - const requriedFieldValidate = jest.fn() - - const form = createForm({ - initialValues: { - requriedField: 'defaultValue' - } - }) - - form.registerField('inpueField', { - props: { - requried: true, - 'x-rules': () => - new Promise(resolve => { - inpueFieldValidate() - - resolve() - }) - } - }) - - form.registerField('requriedField', { - props: { - requried: true, - 'x-rules': () => - new Promise(resolve => { - requriedFieldValidate() - - resolve() - }) - } - }) - - form.setValue('inpueField', 1111) - await sleep(1000) - expect(inpueFieldValidate).toHaveBeenCalledTimes(1) - expect(requriedFieldValidate).toHaveBeenCalledTimes(0) - - form.setValue('requriedField', 2222) - await sleep(1000) - expect(inpueFieldValidate).toHaveBeenCalledTimes(1) - expect(requriedFieldValidate).toHaveBeenCalledTimes(1) -}) diff --git a/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap b/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap new file mode 100644 index 00000000000..09f00018cff --- /dev/null +++ b/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap @@ -0,0 +1,5106 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`createForm initialValue 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": 111, + "bb": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": 111, + "bb": 222, + }, + "warnings": Array [], + }, +} +`; + +exports[`createForm initialValues after init 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": 111, + "bb": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": 111, + "bb": 222, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 111, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 111, + "values": Array [ + 111, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 222, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "bb", + "path": "bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 222, + "values": Array [ + 222, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`createForm initialValues on init 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": 111, + "bb": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": 111, + "bb": 222, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 111, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 111, + "values": Array [ + 111, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 222, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "bb", + "path": "bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 222, + "values": Array [ + 222, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`createForm lifecycles 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": 111, + "bb": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": "change aa", + "bb": "change bb", + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 111, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change aa", + "values": Array [ + "change aa", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 222, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "bb", + "path": "bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change bb", + "values": Array [ + "change bb", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`createForm merge values and initialValues 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "a": "x", + "b": "y", + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "a": 1, + "b": 2, + }, + "warnings": Array [], + }, +} +`; + +exports[`createForm values 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": 111, + "bb": 222, + }, + "warnings": Array [], + }, +} +`; + +exports[`createMutators blur 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object {}, + "warnings": Array [], + }, + "a": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "a", + "path": "a", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`createMutators blur 2`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object {}, + "warnings": Array [], + }, + "a": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "a", + "path": "a", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": true, + "warnings": Array [], + }, +} +`; + +exports[`createMutators focus 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object {}, + "warnings": Array [], + }, + "a": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "a", + "path": "a", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`createMutators focus 2`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object {}, + "warnings": Array [], + }, + "a": Object { + "active": true, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "a", + "path": "a", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": true, + "warnings": Array [], + }, +} +`; + +exports[`graph getFormGraph 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": 111, + "bb": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": "change aa", + "bb": "change bb", + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 111, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change aa", + "values": Array [ + "change aa", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 222, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "bb", + "path": "bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change bb", + "values": Array [ + "change bb", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`graph setFormGraph 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": 111, + "bb": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "bb": "change bb", + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 111, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change aa", + "values": Array [ + "change aa", + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 222, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "bb", + "path": "bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change bb", + "values": Array [ + "change bb", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences deep nested visible with VirtualField 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object {}, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "cc": 123, + }, + "values": Array [ + Object { + "cc": 123, + }, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "display": true, + "displayName": "VirtualFieldState", + "initialized": true, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "props": Object {}, + "unmounted": false, + "visible": false, + }, + "aa.bb.cc": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.cc", + "path": "aa.bb.cc", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences deep nested visible(middle part) 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object {}, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Object { + "cc": 123, + }, + }, + "values": Array [ + Object { + "bb": Object { + "cc": 123, + }, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "cc": 123, + }, + "values": Array [ + Object { + "cc": 123, + }, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "aa.bb.cc": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.cc", + "path": "aa.bb.cc", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences deep nested visible(root) 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object {}, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Object { + "cc": 123, + }, + }, + "values": Array [ + Object { + "bb": Object { + "cc": 123, + }, + }, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "cc": 123, + }, + "values": Array [ + Object { + "cc": 123, + }, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "aa.bb.cc": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.cc", + "path": "aa.bb.cc", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences dynamic remove with intialValues 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": 345, + "bb": 678, + }, + ], + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": "change aa", + "bb": 678, + }, + ], + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": 345, + "bb": 678, + }, + ], + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": "change aa", + "bb": 678, + }, + ], + "values": Array [ + Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": "change aa", + "bb": 678, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 123, + "bb": 321, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0", + "path": "aa.0", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 123, + "bb": 321, + }, + "values": Array [ + Object { + "aa": 123, + "bb": 321, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0.aa", + "path": "aa.0.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 321, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0.bb", + "path": "aa.0.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 321, + "values": Array [ + 321, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 345, + "bb": 678, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.1", + "path": "aa.1", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": "change aa", + "bb": 678, + }, + "values": Array [ + Object { + "aa": "change aa", + "bb": 678, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 345, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.1.aa", + "path": "aa.1.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change aa", + "values": Array [ + "change aa", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 678, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.1.bb", + "path": "aa.1.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 678, + "values": Array [ + 678, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences dynamic remove with intialValues 2`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": 345, + "bb": 678, + }, + ], + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Array [ + Object { + "aa": "change aa", + "bb": 678, + }, + ], + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": 345, + "bb": 678, + }, + ], + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": "change aa", + "bb": 678, + }, + ], + "values": Array [ + Array [ + Object { + "aa": "change aa", + "bb": 678, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 123, + "bb": 321, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.0", + "path": "aa.0", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": "change aa", + "bb": 678, + }, + "values": Array [ + Object { + "aa": "change aa", + "bb": 678, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.0.aa", + "path": "aa.0.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change aa", + "values": Array [ + "change aa", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 321, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.0.bb", + "path": "aa.0.bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 678, + "values": Array [ + 678, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences dynamic remove with intialValues 3`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": 345, + "bb": 678, + }, + ], + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": "change aa", + "bb": 678, + }, + ], + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": 345, + "bb": 678, + }, + ], + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": "change aa", + "bb": 678, + }, + ], + "values": Array [ + Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": "change aa", + "bb": 678, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 123, + "bb": 321, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0", + "path": "aa.0", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 123, + "bb": 321, + }, + "values": Array [ + Object { + "aa": 123, + "bb": 321, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0.aa", + "path": "aa.0.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 321, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0.bb", + "path": "aa.0.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 321, + "values": Array [ + 321, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 345, + "bb": 678, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.1", + "path": "aa.1", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": "change aa", + "bb": 678, + }, + "values": Array [ + Object { + "aa": "change aa", + "bb": 678, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 345, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.1.aa", + "path": "aa.1.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change aa", + "values": Array [ + "change aa", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 678, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.1.bb", + "path": "aa.1.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 678, + "values": Array [ + 678, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences nested dynamic remove 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Array [ + undefined, + Object { + "aa": "change aa", + }, + ], + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + undefined, + Object { + "aa": "change aa", + }, + ], + "values": Array [ + Array [ + undefined, + Object { + "aa": "change aa", + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.1", + "path": "aa.1", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": "change aa", + }, + "values": Array [ + Object { + "aa": "change aa", + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.1.aa", + "path": "aa.1.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change aa", + "values": Array [ + "change aa", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.1.bb", + "path": "aa.1.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences nested dynamic remove 2`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Array [ + Object { + "aa": "change aa", + }, + ], + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": "change aa", + }, + ], + "values": Array [ + Array [ + Object { + "aa": "change aa", + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences nested dynamic remove 3`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Array [ + undefined, + Object { + "aa": "change aa", + }, + ], + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + undefined, + Object { + "aa": "change aa", + }, + ], + "values": Array [ + Array [ + undefined, + Object { + "aa": "change aa", + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.1", + "path": "aa.1", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": "change aa", + }, + "values": Array [ + Object { + "aa": "change aa", + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.1.aa", + "path": "aa.1.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change aa", + "values": Array [ + "change aa", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.1.bb", + "path": "aa.1.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences nested visible 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Object { + "bb": 123, + "cc": 222, + }, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object {}, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "bb": 123, + "cc": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": 123, + "cc": 222, + }, + "values": Array [ + Object { + "bb": 123, + "cc": 222, + }, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "aa.cc": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 222, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.cc", + "path": "aa.cc", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 222, + "values": Array [ + 222, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences nested visible 2`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Object { + "bb": 123, + "cc": 222, + }, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object {}, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "bb": 123, + "cc": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": 123, + "cc": 222, + }, + "values": Array [ + Object { + "bb": 123, + "cc": 222, + }, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "aa.cc": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 222, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.cc", + "path": "aa.cc", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 222, + "values": Array [ + 222, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences nested visible 3`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Object { + "bb": 123, + "cc": 222, + }, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": 123, + "cc": 222, + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "bb": 123, + "cc": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": 123, + "cc": 222, + }, + "values": Array [ + Object { + "bb": 123, + "cc": 222, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.cc": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 222, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.cc", + "path": "aa.cc", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 222, + "values": Array [ + 222, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`reset array reset forceclear 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "values": Array [ + Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "values": Array [ + Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 123, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.0", + "path": "aa.bb.0", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 123, + }, + "values": Array [ + Object { + "aa": 123, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.0.aa", + "path": "aa.bb.0.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 321, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1", + "path": "aa.bb.1", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 321, + }, + "values": Array [ + Object { + "aa": 321, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 321, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1.aa", + "path": "aa.bb.1.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 321, + "values": Array [ + 321, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`reset array reset forceclear 2`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": Array [], + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Array [], + }, + "values": Array [ + Object { + "bb": Array [], + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [], + "values": Array [ + Array [], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`reset array reset no forceclear(initial values) 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "values": Array [ + Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "values": Array [ + Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 123, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.0", + "path": "aa.bb.0", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 123, + }, + "values": Array [ + Object { + "aa": 123, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.0.aa", + "path": "aa.bb.0.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 321, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1", + "path": "aa.bb.1", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 321, + }, + "values": Array [ + Object { + "aa": 321, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 321, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1.aa", + "path": "aa.bb.1.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 321, + "values": Array [ + 321, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`reset array reset no forceclear(initial values) 2`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + }, + "values": Array [ + Object { + "bb": Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + "values": Array [ + Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 123, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb.0", + "path": "aa.bb.0", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": "aa changed", + }, + "values": Array [ + Object { + "aa": "aa changed", + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb.0.aa", + "path": "aa.bb.0.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "aa changed", + "values": Array [ + "aa changed", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 321, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1", + "path": "aa.bb.1", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 321, + }, + "values": Array [ + Object { + "aa": 321, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 321, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1.aa", + "path": "aa.bb.1.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 321, + "values": Array [ + 321, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`reset array reset no forceclear(initial values) 3`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "values": Array [ + Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "values": Array [ + Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 123, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.0", + "path": "aa.bb.0", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 123, + }, + "values": Array [ + Object { + "aa": 123, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.0.aa", + "path": "aa.bb.0.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 321, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1", + "path": "aa.bb.1", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 321, + }, + "values": Array [ + Object { + "aa": 321, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 321, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1.aa", + "path": "aa.bb.1.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 321, + "values": Array [ + 321, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`reset array reset no forceclear(values) 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "values": Array [ + Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "values": Array [ + Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.0", + "path": "aa.bb.0", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 123, + }, + "values": Array [ + Object { + "aa": 123, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.0.aa", + "path": "aa.bb.0.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1", + "path": "aa.bb.1", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 321, + }, + "values": Array [ + Object { + "aa": 321, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1.aa", + "path": "aa.bb.1.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 321, + "values": Array [ + 321, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`reset array reset no forceclear(values) 2`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + }, + "values": Array [ + Object { + "bb": Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + "values": Array [ + Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb.0", + "path": "aa.bb.0", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": "aa changed", + }, + "values": Array [ + Object { + "aa": "aa changed", + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb.0.aa", + "path": "aa.bb.0.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "aa changed", + "values": Array [ + "aa changed", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1", + "path": "aa.bb.1", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 321, + }, + "values": Array [ + Object { + "aa": 321, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1.aa", + "path": "aa.bb.1.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 321, + "values": Array [ + 321, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`reset array reset no forceclear(values) 3`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": Array [], + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Array [], + }, + "values": Array [ + Object { + "bb": Array [], + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [], + "values": Array [ + Array [], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; diff --git a/packages/core/src/__tests__/field.state.spec.ts b/packages/core/src/__tests__/field.state.spec.ts new file mode 100644 index 00000000000..5c9cd2c1486 --- /dev/null +++ b/packages/core/src/__tests__/field.state.spec.ts @@ -0,0 +1,32 @@ +//import { FieldState } from '../state/field' + +test('subscribe', () => { + //todo +}) +test('unsubscribe', () => { + //todo +}) +test('batch', () => { + //todo +}) +test('getState', () => { + //todo +}) +test('setState', () => { + //todo +}) +test('getSourceState', () => { + //todo +}) +test('setSourceState', () => { + //todo +}) +test('setState', () => { + //todo +}) +test('hasChanged', () => { + //todo +}) +test('getChanged', () => { + //todo +}) diff --git a/packages/core/src/__tests__/form.state.spec.ts b/packages/core/src/__tests__/form.state.spec.ts new file mode 100644 index 00000000000..6135d4643fd --- /dev/null +++ b/packages/core/src/__tests__/form.state.spec.ts @@ -0,0 +1,32 @@ +//import { FormState } from '../state/form' + +test('subscribe', () => { + //todo +}) +test('unsubscribe', () => { + //todo +}) +test('batch', () => { + //todo +}) +test('getState', () => { + //todo +}) +test('setState', () => { + //todo +}) +test('getSourceState', () => { + //todo +}) +test('setSourceState', () => { + //todo +}) +test('setState', () => { + //todo +}) +test('hasChanged', () => { + //todo +}) +test('getChanged', () => { + //todo +}) diff --git a/packages/core/src/__tests__/graph.spec.ts b/packages/core/src/__tests__/graph.spec.ts new file mode 100644 index 00000000000..b707eb01da0 --- /dev/null +++ b/packages/core/src/__tests__/graph.spec.ts @@ -0,0 +1,49 @@ +//import { FormGraph } from '../shared/graph' + +test('constructor',()=>{ + //todo +}) + +test('select',()=>{ + //todo +}) + +test('selectParent',()=>{ + //todo +}) + +test('selectChildren',()=>{ + //todo +}) + +test('exist',()=>{ + //todo +}) + +test('eachChildren',()=>{ + //todo +}) + +test('eachParent',()=>{ + //todo +}) + +test('getLatestParent',()=>{ + //todo +}) + +test('appendNode',()=>{ + //todo +}) + +test('remove',()=>{ + //todo +}) + +test('toJSON',()=>{ + //todo +}) + +test('fromJSON',()=>{ + //todo +}) \ No newline at end of file diff --git a/packages/core/src/__tests__/index.spec.ts b/packages/core/src/__tests__/index.spec.ts new file mode 100644 index 00000000000..d8bb50152b0 --- /dev/null +++ b/packages/core/src/__tests__/index.spec.ts @@ -0,0 +1,1130 @@ +import { isEqual } from '@uform/shared' +import { createForm, LifeCycleTypes, FormLifeCycle, FormPath } from '../index' +import { ValidateDescription, ValidatePatternRules } from '@uform/validator' + +// mock datasource +const testValues = { aa: 111, bb: 222 } +const testValues2 = { aa: '123', bb: '321' } +const changeValues = { aa: 'change aa', bb: 'change bb' } +const resetInitValues = { + aa: { + bb: [{ aa: 123 }, { aa: 321 }] + } +} +const deepValues = { + a: { + b: { c: { d: { e: 1}}}, + c: { + e: 2 + } + }, + b: { + c: 3 + } +} + + +describe('createForm', () => { + test('values', () => { + const form = createForm({ + values: testValues + }) + expect(form.getFormState(state => state.values)).toEqual(testValues) + expect(form.getFormState(state => state.pristine)).toEqual(false) + expect(form.getFormState(state => state.initialized)).toEqual(true) + expect(form.getFormGraph()).toMatchSnapshot() + }) + + test('initialValues on init', () => { + const form = createForm({ + initialValues: testValues + }) + const aa = form.registerField({ path: 'aa' }) + const bb = form.registerField({ path: 'bb' }) + expect(form.getFormState(state => state.values)).toEqual(testValues) + expect(form.getFormState(state => state.initialValues)).toEqual(testValues) + expect(form.getFormState(state => state.pristine)).toEqual(true) + expect(form.getFormState(state => state.initialized)).toEqual(true) + expect(aa.getState(state => state.value)).toEqual(testValues.aa) + expect(bb.getState(state => state.value)).toEqual(testValues.bb) + expect(form.getFormGraph()).toMatchSnapshot() + }) + + test('merge values and initialValues', () => { + const form = createForm({ + values: { a: 1, b: 2 }, + initialValues: { a: 'x', b: 'y' } + }) + expect(form.getFormState(state => state.values)).toEqual({ a: 1, b: 2 }) + expect(form.getFormGraph()).toMatchSnapshot() + }) + + test('initialValues after init', () => { + const form = createForm() + const aa = form.registerField({ path: 'aa' }) + const bb = form.registerField({ path: 'bb' }) + form.setFormState(state => { + state.initialValues = testValues + }) + expect(form.getFormState(state => state.values)).toEqual(testValues) + expect(form.getFormState(state => state.initialValues)).toEqual(testValues) + expect(form.getFormState(state => state.pristine)).toEqual(true) + expect(form.getFormState(state => state.initialized)).toEqual(true) + expect(aa.getState(state => state.value)).toEqual(testValues.aa) + expect(bb.getState(state => state.value)).toEqual(testValues.bb) + expect(form.getFormGraph()).toMatchSnapshot() + }) + + test('initialValue', () => { + const form = createForm({ + initialValues: testValues + }) + expect(form.getFormState(state => state.values)).toEqual(testValues) + expect(form.getFormState(state => state.initialValues)).toEqual(testValues) + expect(form.getFormState(state => state.pristine)).toEqual(true) + expect(form.getFormState(state => state.initialized)).toEqual(true) + expect(form.getFormGraph()).toMatchSnapshot() + }) + + test('lifecycles', () => { + const onFormInit = jest.fn() + const onFieldInit = jest.fn() + const onFieldChange = jest.fn() + const form = createForm({ + initialValues: testValues, + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FORM_INIT, onFormInit), + new FormLifeCycle(LifeCycleTypes.ON_FIELD_INIT, onFieldInit), + new FormLifeCycle(LifeCycleTypes.ON_FIELD_CHANGE, onFieldChange) + ] + }) + + const aa = form.registerField({ path: 'aa', value: testValues2.aa }) + const bb = form.registerField({ path: 'bb', value: testValues2.bb }) + + // registerField will trigger ON_FIELD_CHANGE(because of initialized changed) + expect(onFormInit).toBeCalledTimes(1) + expect(onFieldInit).toBeCalledTimes(2) + expect(onFieldChange).toBeCalledTimes(2) + expect(form.getFormState(state => state.values)).toEqual(testValues2) + + // change field's value + aa.setState(state => state.value = changeValues.aa) + bb.setState(state => state.value = changeValues.bb) + expect(onFieldChange).toBeCalledTimes(4) + expect(form.getFormState(state => state.values)).toEqual(changeValues) + expect(aa.getState(state => state.value)).toEqual(changeValues.aa) + expect(bb.getState(state => state.value)).toEqual(changeValues.bb) + expect(form.getFormGraph()).toMatchSnapshot() + }) +}) + +describe('graph', () => { + test('getFormGraph', () => { + const form = createForm({ + initialValues: testValues + }) + + form.registerField({ path: 'aa', value: changeValues.aa }) + form.registerField({ path: 'bb', value: changeValues.bb }) + expect(form.getFormGraph()).toMatchSnapshot() + }) + + test('setFormGraph', () => { + const form = createForm({ + initialValues: testValues + }) + + form.registerField({ path: 'aa', value: changeValues.aa }) + form.registerField({ path: 'bb', value: changeValues.bb }) + const snapshot = form.getFormGraph() + form.setFieldState('aa', state => state.visible = false) + expect(form.getFormGraph()).toMatchSnapshot() + + // change graph or you can also call it 'time travel' + form.setFormGraph(snapshot) + expect(form.getFormGraph()).toEqual(snapshot) + }) +}) + +describe('submit', () => { + test('onSubmit', async() => { + const onSubmitContructor = jest.fn() + const onSubmitFn = jest.fn() + const changeSubmitPayload = (values) => ({ hello: 'world' }) + const form1 = createForm({ onSubmit: onSubmitContructor }) + const form2 = createForm() + + expect(onSubmitContructor).toBeCalledTimes(0) + expect(onSubmitContructor).toBeCalledTimes(0) + await form1.submit() + await form2.submit() + expect(onSubmitContructor).toBeCalledTimes(1) + expect(onSubmitFn).toBeCalledTimes(0) + + // priority: onSubmitFn > onSubmitContructor + await form1.submit(onSubmitFn) + await form2.submit(onSubmitFn) + expect(onSubmitContructor).toBeCalledTimes(1) + expect(onSubmitFn).toBeCalledTimes(2) + const result = await form2.submit(changeSubmitPayload) + expect(result).toEqual({ validated: { + errors: [], + warnings: [], + }, payload: { hello: 'world'} }) + }) + + test('submitResult', async () => { + const sunmitFailed = jest.fn() + const form = createForm() + form.registerField({ path: 'a', rules: [(value) => { + return value === undefined ? { type: 'error', message: 'a is required' } : null + }] }) + form.registerField({ path: 'b', rules: [(value) => { + return value === undefined ? { type: 'warning', message: 'b is required' } : null + }] }) + + // error failed + try { + await form.submit() + } catch (errors) { + sunmitFailed() + expect(errors).toEqual([{ path: 'a', messages: ['a is required']}]) + } + + // warning pass + form.setFieldValue('a', 1) + let validated + try { + const result = await form.submit() + validated = result.validated + } catch (errors) { + sunmitFailed() + } + + expect(validated.warnings).toEqual([{ path: 'b', messages: ['b is required'] }]) + expect(validated.errors).toEqual([]) + expect(sunmitFailed).toHaveBeenCalledTimes(1) + }) + + test('repeat submit', async () => { + const form = createForm() + const result1 = form.submit() + const result2 = form.submit() + // reuse before result + expect(result1).toEqual(result2) + }) + + test('basic', async () => { + const onSubmitStart = jest.fn() + const onSubmitEnd = jest.fn() + const form = createForm({ + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FORM_SUBMIT_START, onSubmitStart), + new FormLifeCycle(LifeCycleTypes.ON_FORM_SUBMIT_END, onSubmitEnd), + ], + onSubmit: () => { + expect(form.getFormState(state => state.submitting)).toEqual(true) + } + }) + expect(form.getFormState(state => state.submitting)).toEqual(false) + expect(onSubmitStart).toBeCalledTimes(0) + expect(onSubmitEnd).toBeCalledTimes(0) + await form.submit() + expect(form.getFormState(state => state.submitting)).toEqual(false) + expect(onSubmitStart).toBeCalledTimes(1) + expect(onSubmitEnd).toBeCalledTimes(1) + }) +}) + +describe('reset', () => { + test('array reset forceclear', async () => { + const form = createForm({ + initialValues: resetInitValues + }) + + form.registerField({ path: 'aa' }) + form.registerField({ path: 'aa.bb' }) + form.registerField({ path: 'aa.bb.0' }) + form.registerField({ path: 'aa.bb.1' }) + form.registerField({ path: 'aa.bb.0.aa' }) + form.registerField({ path: 'aa.bb.1.aa' }) + expect(form.getFormGraph()).toMatchSnapshot() + + form.setFieldState('aa.bb.0.aa', state => { + state.value = 'aa changed' + }) + await form.reset({ forceClear: true }) + expect(form.getFormGraph()).toMatchSnapshot() + expect(form.getFormState(state => state.values)).toEqual({ aa: { bb: [] } }) + expect(form.getFormState(state => state.initialValues)).toEqual(resetInitValues) + }) + + test('array reset no forceclear(initial values)', async () => { + const form = createForm({ + initialValues: resetInitValues + }) + form.registerField({ path: 'aa' }) + form.registerField({ path: 'aa.bb' }) + form.registerField({ path: 'aa.bb.0' }) + form.registerField({ path: 'aa.bb.1' }) + form.registerField({ path: 'aa.bb.0.aa' }) + form.registerField({ path: 'aa.bb.1.aa' }) + expect(form.getFormGraph()).toMatchSnapshot() + form.setFieldState('aa.bb.0.aa', state => { + state.value = 'aa changed' + }) + expect(form.getFormGraph()).toMatchSnapshot() + await form.reset() + expect(form.getFormGraph()).toMatchSnapshot() + expect(form.getFormState(state => state.values)).toEqual(resetInitValues) + expect(form.getFormState(state => state.initialValues)).toEqual(resetInitValues) + }) + + test('array reset no forceclear(values)', async () => { + const form = createForm({ + values: resetInitValues + }) + form.registerField({ path: 'aa' }) + form.registerField({ path: 'aa.bb' }) + form.registerField({ path: 'aa.bb.0' }) + form.registerField({ path: 'aa.bb.1' }) + form.registerField({ path: 'aa.bb.0.aa' }) + form.registerField({ path: 'aa.bb.1.aa' }) + expect(form.getFormGraph()).toMatchSnapshot() + form.setFieldState('aa.bb.0.aa', state => { + state.value = 'aa changed' + }) + expect(form.getFormGraph()).toMatchSnapshot() + await form.reset() + expect(form.getFormGraph()).toMatchSnapshot() + expect(form.getFormState(state => state.values)).toEqual({ aa: { bb: [] }}) + expect(form.getFormState(state => state.initialValues)).toEqual({}) + }) +}) + +describe('clearErrors', () => { + test('basic', async () => { + const form = createForm() + const warnMsg = ['warning msg'] + const errMsg = ['error msg'] + form.registerField({ path: 'b', rules: [(v) => v === undefined ? ({ type: 'warning', message: 'warning msg' }) : undefined] }) // CustomValidator warning + form.registerField({ path: 'c', rules: [(v) => v === undefined ? ({ type: 'error', message: 'error msg' }) : undefined] }) // CustomValidator error + const result1 = await form.validate() + expect(result1.warnings).toEqual([{ path: 'b', messages: warnMsg }]) + expect(result1.errors).toEqual([{ path: 'c', messages: errMsg}]) + + form.clearErrors('b') + expect(form.getFormState(state => state.warnings)).toEqual([]) + expect(form.getFormState(state => state.errors)).toEqual([{ path: 'c', messages: errMsg}]) + + form.clearErrors('c') + expect(form.getFormState(state => state.warnings)).toEqual([]) + expect(form.getFormState(state => state.errors)).toEqual([]) + + const result2 = await form.validate() + expect(result2.warnings).toEqual([{ path: 'b', messages: warnMsg }]) + expect(result2.errors).toEqual([{ path: 'c', messages: errMsg}]) + + form.clearErrors() + expect(form.getFormState(state => state.warnings)).toEqual([]) + expect(form.getFormState(state => state.errors)).toEqual([]) + }) + + test('wildcard path', async () => { + + }) + + test('effect', async () => { + + }) +}) + +describe('validate', () => { + test('empty', async () => { + const form = createForm() + const { warnings, errors } = await form.validate() + expect(warnings).toEqual([]) + expect(errors).toEqual([]) + }) + + test('onValidateFailed', async () => { + const onValidateFailedTrigger = jest.fn() + const onValidateFailed = ({ warnings, errors }) => { + expect(warnings).toEqual([{ path: 'b', messages: ['warning msg'] }]) + expect(errors).toEqual([{ path: 'c', messages: ['error msg']}]) + onValidateFailedTrigger(); + }; + const form = createForm({ + onValidateFailed + }) + form.registerField({ path: 'b', rules: [() => ({ type: 'warning', message: 'warning msg' })] }) // CustomValidator warning + form.registerField({ path: 'c', rules: [() => ({ type: 'error', message: 'error msg' })] }) // CustomValidator error + await form.validate() + expect(onValidateFailedTrigger).toBeCalledTimes(1) + }) + + test('validate basic', async () => { + const onValidateStart = jest.fn() + const onValidateEnd = jest.fn() + const form = createForm({ + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FORM_VALIDATE_START, onValidateStart), + new FormLifeCycle(LifeCycleTypes.ON_FORM_VALIDATE_END, onValidateEnd), + ], + }) + form.registerField({ path: 'a', rules: ['number'] }) // string + form.registerField({ path: 'b', rules: [() => ({ type: 'warning', message: 'warning msg' })] }) // CustomValidator warning + form.registerField({ path: 'c', rules: [() => ({ type: 'error', message: 'warning msg' })] }) // CustomValidator error + form.registerField({ path: 'd', rules: [() => 'straight error msg'] }) // CustomValidator string + form.registerField({ path: 'e', rules: [{ required: true, message: 'desc msg' }] }) // ValidateDescription + + expect(onValidateStart).toBeCalledTimes(0) + expect(onValidateEnd).toBeCalledTimes(0) + expect(form.getFormState(state => state.validating)).toEqual(false) + const validatePromise = form.validate() + expect(form.getFormState(state => state.validating)).toEqual(true) + expect(onValidateStart).toBeCalledTimes(1) + validatePromise.then((validated) => { + expect(form.getFormState(state => state.validating)).toEqual(false) + expect(onValidateEnd).toBeCalledTimes(1) + const { warnings, errors } = validated; + expect(warnings.length).toEqual(1) + expect(errors.length).toEqual(4) + }) + }) + + test('path', async () => { + const form = createForm() + form.registerField({ path: 'b', rules: [(v) => v === undefined ? ({ type: 'warning', message: 'warning msg' }) : undefined] }) // CustomValidator warning + form.registerField({ path: 'c', rules: [(v) => v === undefined ? ({ type: 'error', message: 'error msg' }) : undefined] }) // CustomValidator error + const bResult = await form.validate('b') + expect(bResult.warnings).toEqual([{ path: 'b', messages: ['warning msg'] }]) + expect(bResult.errors).toEqual([]) + expect(form.getFieldState('b', state => state.warnings)).toEqual(['warning msg']) + expect(form.getFieldState('c', state => state.errors)).toEqual([]) + expect(form.getFormState(state => state.warnings)).toEqual([{ path: 'b', messages: ['warning msg'] }]) + expect(form.getFormState(state => state.errors)).toEqual([]) + + const cResult = await form.validate('c') + expect(cResult.warnings).toEqual([]) + expect(cResult.errors).toEqual([{ path: 'c', messages: ['error msg']}]) + expect(form.getFieldState('b', state => state.warnings)).toEqual(['warning msg']) + expect(form.getFieldState('c', state => state.errors)).toEqual(['error msg']) + expect(form.getFormState(state => state.warnings)).toEqual([{ path: 'b', messages: ['warning msg'] }]) + expect(form.getFormState(state => state.errors)).toEqual([{ path: 'c', messages: ['error msg']}]) + + form.setFieldValue('b', 1) + form.setFieldValue('c', 1) + const bResult2 = await form.validate('b') + const cResult2 = await form.validate('c') + expect(bResult2.warnings).toEqual([]) + expect(bResult2.errors).toEqual([]) + expect(cResult2.warnings).toEqual([]) + expect(cResult2.errors).toEqual([]) + expect(form.getFieldState('b', state => state.warnings)).toEqual([]) + expect(form.getFieldState('c', state => state.errors)).toEqual([]) + expect(form.getFormState(state => state.warnings)).toEqual([]) + expect(form.getFormState(state => state.errors)).toEqual([]) + }) +}) + +describe('setFormState', () => { + test('no callback', async () => { + const form = createForm() + const state = form.getFormState() + form.setFormState() + expect(form.getFormState()).toEqual(state) + }) + + test('set', async() => { + const form = createForm() + // pristine 依赖 draft.values === draft.initialValues + // invalid 依赖 errors.length === 0 + // valid 是 invalid的取反 + // loading 取决于validating + // mounted 和 unmounted 互为取反,先读mounted + // errors, warnings 无法被设置,会从最新的state中获取 + form.registerField({ path: 'b', rules: [(v) => v === undefined ? ({ type: 'warning', message: 'warning msg' }) : undefined] }) // CustomValidator warning + form.registerField({ path: 'c', rules: [(v) => v === undefined ? ({ type: 'error', message: 'error msg' }) : undefined] }) // CustomValidator error + const { errors, warnings } = await form.validate() + const invalid = errors.length > 0 + + const values = { b: '2' } + const initialValues = { a: '1' } + const validating = true + form.setFormState((state) => { + state.pristine = false + state.valid = false + state.invalid = false + state.loading = false + state.validating = validating + state.submitting = true + state.initialized = false + state.editable = false + state.values = values + state.initialValues = initialValues + state.mounted = true + state.unmounted = true + state.props = { hello: 'world' } + }) + expect(form.getFormState()).toEqual({ + displayName: 'FormState', + pristine: isEqual(values, initialValues), + valid: !invalid, + invalid: invalid, + loading: validating, + validating: validating, + submitting: true, + initialized: false, + editable: false, + errors, + warnings, + values, + initialValues, + mounted: true, + unmounted: false, + props: { hello: 'world' }, + }) + }) +}) + +describe('getFormState', () => { + test('basic', async () => { + const form = createForm() + const state = form.getFormState() + expect(state).toEqual(form.getFormState((state) => state)) + expect(form.getFormState(state => state.pristine)).toEqual(state.pristine) + expect(form.getFormState(state => state.valid)).toEqual(state.valid) + expect(form.getFormState(state => state.invalid)).toEqual(state.invalid) + expect(form.getFormState(state => state.loading)).toEqual(state.loading) + expect(form.getFormState(state => state.validating)).toEqual(state.validating) + expect(form.getFormState(state => state.submitting)).toEqual(state.submitting) + expect(form.getFormState(state => state.initialized)).toEqual(state.initialized) + expect(form.getFormState(state => state.editable)).toEqual(state.editable) + expect(form.getFormState(state => state.errors)).toEqual(state.errors) + expect(form.getFormState(state => state.warnings)).toEqual(state.warnings) + expect(form.getFormState(state => state.values)).toEqual(state.values) + expect(form.getFormState(state => state.initialValues)).toEqual(state.initialValues) + expect(form.getFormState(state => state.mounted)).toEqual(state.mounted) + expect(form.getFormState(state => state.unmounted)).toEqual(state.unmounted) + expect(form.getFormState(state => state.props)).toEqual(state.props) + }) +}) + +describe('setFieldState', () => { + test('no callback', async () => { + const form = createForm() + form.registerField({ path: 'a' }) + const state = form.getFieldState('a') + form.setFieldState('a') + expect(form.getFieldState('a')).toEqual(state) + }) + + test('validating and loading', () => { + const form = createForm() + form.registerField({ path: 'a', rules: [ + (v) => v === undefined ? ({ type: 'warning', message: 'warning msg' }) : undefined, + (v) => v === undefined ? ({ type: 'error', message: 'error msg' }) : undefined + ] }) + expect(form.getFieldState('a', state => state.validating)).toEqual(false) + expect(form.getFieldState('a', state => state.loading)).toEqual(false) + form.setFieldState('a', state => state.validating = true) + expect(form.getFieldState('a', state => state.loading)).toEqual(true) + form.setFieldState('a', state => state.validating = false) + expect(form.getFieldState('a', state => state.loading)).toEqual(false) + }); + + test('value, values and parseValues', () => { + const form = createForm() + form.registerField({ path: 'a' }) + expect(form.getFieldState('a', state => state.modified)).toEqual(false) + expect(form.getFieldState('a', state => state.value)).toEqual(undefined) + expect(form.getFieldState('a', state => state.values)).toEqual([undefined]) + const arr = [1,2,3] + form.setFieldState('a', state => state.value = arr) + expect(form.getFieldState('a', state => state.modified)).toEqual(true) + expect(form.getFieldState('a', state => state.value)).toEqual(arr) + expect(form.getFieldState('a', state => state.values)).toEqual([arr]) + form.setFieldState('a', state => state.values = ['e', 'context']) + // values 第一个参数会是value, 处理onChange的多参数一般会和values[0]同步,这里value测试极端情况 + expect(form.getFieldState('a', state => state.value)).toEqual(arr) + expect(form.getFieldState('a', state => state.values)).toEqual([arr, 'context']) + // visible为false或者已卸载的组件无法修改value + form.setFieldState('a', state => state.visible = false) + form.setFieldState('a', state => state.value = [4,5,6]) + expect(form.getFieldState('a', state => state.value)).toEqual(arr) + form.setFieldState('a', state => { + state.visible = true; + state.unmounted = true; + }) + form.setFieldState('a', state => state.value = [4,5,6]) + expect(form.getFieldState('a', state => state.value)).toEqual(arr) + }); + + test('mount and unmount', () => { + const form = createForm() + form.registerField({ path: 'a' }) + expect(form.getFieldState('a', state => state.mounted)).toEqual(false) + expect(form.getFieldState('a', state => state.unmounted)).toEqual(false) + form.setFieldState('a', state => state.mounted = true) + expect(form.getFieldState('a', state => state.mounted)).toEqual(true) + expect(form.getFieldState('a', state => state.unmounted)).toEqual(false) + }); + + test('rules, required and parseRules', () => { + const form = createForm() + form.registerField({ path: 'a' }) + expect(form.getFieldState('a', state => state.required)).toEqual(false) + expect(form.getFieldState('a', state => state.rules)).toEqual([]) + form.setFieldState('a', state => state.required = true) + expect(form.getFieldState('a', state => state.required)).toEqual(true) + const customValidator: ValidatePatternRules[] = [(v, _: ValidateDescription) => v === undefined ? ({ type: 'warning', message: 'warning msg' }) : null]; + form.setFieldState('a', state => state.rules = customValidator) + const rules = form.getFieldState('a', state => state.rules); + expect(rules).toEqual([...customValidator, { required: true }]) + }); + + test('pristine', () => { // 无法被修改,依赖value和initialValue的差别 + const form = createForm() + form.registerField({ path: 'a' }) + expect(form.getFieldState('a', state => state.pristine)).toEqual(true) + form.setFieldState('a', state => state.pristine = false) + expect(form.getFieldState('a', state => state.pristine)).toEqual(true) + form.setFieldState('a', state => state.value = '1') + expect(form.getFieldState('a', state => state.pristine)).toEqual(false) + form.setFieldState('a', state => state.initialValue = '1') + expect(form.getFieldState('a', state => state.pristine)).toEqual(true) + }); + + test('invalid 和 valid', () => { // 无法被修改,依赖错误信息和告警信息 + const form = createForm() + form.registerField({ path: 'a' }) + expect(form.getFieldState('a', state => state.invalid)).toEqual(false) + expect(form.getFieldState('a', state => state.valid)).toEqual(true) + form.setFieldState('a', state => state.invalid = true) + form.setFieldState('a', state => state.valid = false) + expect(form.getFieldState('a', state => state.invalid)).toEqual(false) + expect(form.getFieldState('a', state => state.valid)).toEqual(true) + }); + + test('set errors and warnings', async() => { + const form = createForm() + const a = form.registerField({ path: 'a', rules: [ + (v) => v === undefined ? ({ type: 'warning', message: 'warning msg' }) : undefined, + (v) => v === undefined ? ({ type: 'error', message: 'error msg' }) : undefined + ] }) + const state = form.getFieldState('a') + expect(state.effectErrors).toEqual([]) + expect(state.effectWarnings).toEqual([]) + expect(state.ruleErrors).toEqual([]) + expect(state.ruleWarnings).toEqual([]) + + const mutators = form.createMutators(a) + const result = await mutators.validate() + expect(result.errors).toEqual([{ path: 'a', messages: ['error msg'] }]) + expect(result.warnings).toEqual([{ path: 'a', messages: ['warning msg'] }]) + // 校验后影响的是ruleErrors, ruleWarnings + const state2 = form.getFieldState('a') + expect(state2.effectErrors).toEqual([]) + expect(state2.effectWarnings).toEqual([]) + expect(state2.ruleErrors).toEqual(['error msg']) + expect(state2.ruleWarnings).toEqual(['warning msg']) + expect(state2.errors).toEqual(['error msg']) + expect(state2.warnings).toEqual(['warning msg']) + + // effectError 和 effectWarnings 需要通过设置 errors 和 warning 进行设置 + // 看起来有问题,但是对于开发者并不透出effectErrors的概念,只让他感知这是errors + // errors = effectErrors + ruleErrors + // warnings = effectWarnings + ruleWarnings + form.setFieldState('a', state => state.errors = ['effect errors msg']) + form.setFieldState('a', state => state.warnings = ['effect warning msg']) + const state3 = form.getFieldState('a') + expect(state3.effectErrors).toEqual(['effect errors msg']) + expect(state3.effectWarnings).toEqual(['effect warning msg']) + expect(state3.errors).toEqual(['error msg', 'effect errors msg']) + expect(state3.warnings).toEqual(['warning msg', 'effect warning msg']) + + // 不可编辑,清空所有错误和警告信息 + form.setFieldState('a', state => state.editable = false) + const state4 = form.getFieldState('a') + expect(state4.effectErrors).toEqual([]) + expect(state4.effectWarnings).toEqual([]) + expect(state4.errors).toEqual([]) + expect(state4.warnings).toEqual([]) + form.setFieldState('a', state => state.editable = true) + + // 隐藏,清空所有错误和警告信息 + await mutators.validate() + form.setFieldState('a', state => state.errors = ['effect errors msg']) + form.setFieldState('a', state => state.warnings = ['effect warning msg']) + form.setFieldState('a', state => state.visible = false) + const state6 = form.getFieldState('a') + expect(state6.effectErrors).toEqual([]) + expect(state6.effectWarnings).toEqual([]) + expect(state6.errors).toEqual([]) + expect(state6.warnings).toEqual([]) + + // 卸载组件,清空所有错误和警告信息 + await mutators.validate() + form.setFieldState('a', state => state.errors = ['effect errors msg']) + form.setFieldState('a', state => state.warnings = ['effect warning msg']) + const state7 = form.getFieldState('a') + expect(state7.effectErrors).toEqual([]) + expect(state7.effectWarnings).toEqual([]) + expect(state7.errors).toEqual([]) + expect(state7.warnings).toEqual([]) + }); + + test('set editable', async() => { + const form = createForm() + form.registerField({ path: 'a' }) + const state = form.getFieldState('a') + + // 初始化 + expect(state.editable).toEqual(true) + expect(state.selfEditable).toEqual(undefined) + expect(state.formEditable).toEqual(undefined) + expect(form.getFieldState('a', state => state.editable)).toEqual(true) + // 简单设置 (editable会影响selfEditable) + form.setFieldState('a', state => state.editable = false) + expect(form.getFieldState('a', state => state.editable)).toEqual(false) + expect(form.getFieldState('a', state => state.selfEditable)).toEqual(false) + // 设置影响计算的值selfEditable + form.setFieldState('a', state => state.selfEditable = true) + expect(form.getFieldState('a', state => state.editable)).toEqual(true) + // 设置影响计算的值formEditable(selfEditable优先级高于formEditable) + form.setFieldState('a', state => state.selfEditable = undefined) + form.setFieldState('a', state => state.formEditable = false) + expect(form.getFieldState('a', state => state.editable)).toEqual(false) + // 支持函数(UI层传入) + form.setFieldState('a', state => state.formEditable = () => true) + expect(form.getFieldState('a', state => state.editable)).toEqual(true) + // editable会影响selfEditable, 设置顺序又因为 editable > selfEditable > formEditable + // 前两者都无效时,最终返回formEditable的值 + form.setFieldState('a', state => state.editable = undefined) + form.setFieldState('a', state => state.formEditable = () => false) + expect(form.getFieldState('a', state => state.selfEditable)).toEqual(undefined) + expect(form.getFieldState('a', state => state.editable)).toEqual(false) + }); +}) + +describe('getFieldState', () => { + const form = createForm() + form.registerField({ path: 'a' }) + const state = form.getFieldState('a') + expect(state).toEqual(form.getFieldState('a', (state) => state)) + expect(form.getFieldState('a', state => state.name)).toEqual(state.name) + expect(form.getFieldState('a', state => state.initialized)).toEqual(state.initialized) + expect(form.getFieldState('a', state => state.pristine)).toEqual(state.pristine) + expect(form.getFieldState('a', state => state.valid)).toEqual(state.valid) + expect(form.getFieldState('a', state => state.touched)).toEqual(state.touched) + expect(form.getFieldState('a', state => state.invalid)).toEqual(state.invalid) + expect(form.getFieldState('a', state => state.visible)).toEqual(state.visible) + expect(form.getFieldState('a', state => state.display)).toEqual(state.display) + expect(form.getFieldState('a', state => state.editable)).toEqual(state.editable) + expect(form.getFieldState('a', state => state.formEditable)).toEqual(state.formEditable) + expect(form.getFieldState('a', state => state.loading)).toEqual(state.loading) + expect(form.getFieldState('a', state => state.modified)).toEqual(state.modified) + expect(form.getFieldState('a', state => state.active)).toEqual(state.active) + expect(form.getFieldState('a', state => state.visited)).toEqual(state.visited) + expect(form.getFieldState('a', state => state.validating)).toEqual(state.validating) + expect(form.getFieldState('a', state => state.errors)).toEqual(state.errors) + expect(form.getFieldState('a', state => state.values)).toEqual(state.values) + expect(form.getFieldState('a', state => state.effectErrors)).toEqual(state.effectErrors) + expect(form.getFieldState('a', state => state.warnings)).toEqual(state.warnings) + expect(form.getFieldState('a', state => state.effectWarnings)).toEqual(state.effectWarnings) + expect(form.getFieldState('a', state => state.value)).toEqual(state.value) + expect(form.getFieldState('a', state => state.initialValue)).toEqual(state.initialValue) + expect(form.getFieldState('a', state => state.rules)).toEqual(state.rules) + expect(form.getFieldState('a', state => state.required)).toEqual(state.required) + expect(form.getFieldState('a', state => state.mounted)).toEqual(state.mounted) + expect(form.getFieldState('a', state => state.unmounted)).toEqual(state.unmounted) + expect(form.getFieldState('a', state => state.props)).toEqual(state.props) +}) + +describe('setFieldValue', () => { + const form = createForm() + form.registerField({ path: 'a' }) + form.setFieldValue('a') + expect(form.getFieldValue('a')).toEqual(undefined) + expect(form.getFieldState('a', state => state.value)).toEqual(undefined) + expect(form.getFormState(state => state.values)).toEqual({ a: undefined }) + form.setFieldValue('a', 1) + expect(form.getFieldValue('a')).toEqual(1) + expect(form.getFieldState('a', state => state.value)).toEqual(1) + expect(form.getFormState(state => state.values)).toEqual({ a: 1 }) +}) + +describe('getFieldValue', () => { + test('normal path', async () => { + const form = createForm({ values: deepValues }) + form.registerField({ path: 'a' }) + form.registerField({ path: 'a.b' }) + form.registerField({ path: 'a.b.c' }) + form.registerField({ path: 'a.b.c.d' }) + form.registerField({ path: 'a.b.c.d.e' }) + + expect(form.getFieldValue('a')).toEqual(deepValues.a) + expect(form.getFieldValue('a.b')).toEqual(deepValues.a.b) + expect(form.getFieldValue('a.b.c')).toEqual(deepValues.a.b.c) + expect(form.getFieldValue('a.b.c.d')).toEqual(deepValues.a.b.c.d) + expect(form.getFieldValue('a.b.c.d.e')).toEqual(deepValues.a.b.c.d.e) + }) + + test('virtual path', async () => { + const form = createForm({ values: deepValues }) + form.registerField({ path: 'a' }) + form.registerVirtualField({ path: 'a.b' }) + form.registerField({ path: 'a.b.c' }) + form.registerVirtualField({ path: 'a.b.c.d' }) + form.registerField({ path: 'a.b.c.d.e' }) + + expect(form.getFieldValue('a')).toEqual(deepValues.a) + expect(form.getFieldValue('a.b')).toEqual(deepValues.a) + expect(form.getFieldValue('a.b.c')).toEqual(deepValues.a.c) + expect(form.getFieldValue('a.b.c.d')).toEqual(deepValues.a.c) + expect(form.getFieldValue('a.b.c.d.e')).toEqual(deepValues.a.c.e) + }) + + test('virtual path(head)', async () => { + const form = createForm({ values: deepValues }) + form.registerVirtualField({ path: 'a' }) + form.registerField({ path: 'a.b' }) + form.registerField({ path: 'a.b.c' }) + + expect(form.getFieldValue('a')).toEqual(deepValues) + expect(form.getFieldValue('a.b')).toEqual(deepValues.b) + expect(form.getFieldValue('a.b.c')).toEqual(deepValues.b.c) + }) +}) + +describe('registerField', () => { + test('basic', async () => { + const form = createForm({ values: { a: 1 } }) + form.registerField({ path: 'a' }) + form.registerField({ path: 'b' }) + form.registerField({ path: 'c' }) + form.registerField({ path: 'd', editable: false }) + form.registerField({ path: 'e', editable: true }) + expect(form.getFieldValue('a')).toEqual(1) + expect(form.getFieldValue('b')).toEqual(undefined) + expect(form.getFieldState('a', state => state.values)).toEqual([1]) + expect(form.getFieldState('b', state => state.values)).toEqual([undefined]) + expect(form.getFieldState('c', state => state.editable)).toEqual(true) + expect(form.getFieldState('d', state => state.editable)).toEqual(false) + expect(form.getFieldState('e', state => state.editable)).toEqual(true) + }) + + test('merge', async () => { + const form = createForm({ values: { a: 1, b: 2, c: 3, d: 4 }}) + form.registerField({ path: 'a' }) + form.registerField({ path: 'b', value: 'x' }) + form.registerField({ path: 'c', initialValue: 'y' }) + form.registerField({ path: 'd', initialValue: 'z', value: 's' }) + expect(form.getFieldValue('a')).toEqual(1) + expect(form.getFieldValue('b')).toEqual('x') + expect(form.getFieldValue('c')).toEqual(3) // false, 得到y + expect(form.getFieldValue('d')).toEqual('s') + }) +}) + +describe('registerVirtualField', () => { + test('basic', async () => { + const onChange = jest.fn() + const vprops = { hello: 'world' }; + const form = createForm({ values: { a: 1 } }) + form.registerVirtualField({ path: 'a' }) + form.registerVirtualField({ path: 'b', onChange }) + expect(onChange).toBeCalledTimes(1) // initialized + form.registerVirtualField({ path: 'c', props: vprops }) + expect(form.getFieldValue('a')).toEqual({ a: 1 }) // 根据dataPath法则,会拿到根路径的value + expect(form.getFieldState('a', state => state.values)).toEqual(undefined) // 不存在这个属性 + expect(form.getFieldState('c', state => state.props)).toEqual(vprops) + expect(form.getFieldState('b', state => state.display)).toEqual(true) + expect(form.getFieldState('b', state => state.visible)).toEqual(true) + form.setFieldState('b', state => state.display = false) + expect(form.getFieldState('b', state => state.display)).toEqual(false) + form.setFieldState('b', state => state.visible = false) + expect(form.getFieldState('b', state => state.visible)).toEqual(false) + expect(onChange).toBeCalledTimes(3) + }) +}) + +describe('createMutators', () => { + const arr = ['a', 'b'] + test('change', async () => { + const form = createForm() + const a = form.registerField({ path: 'a' }) + const mutators = form.createMutators(a) + expect(form.getFieldState('a', (state => ({ values: state.values, value: state.value })))).toEqual({ + value: undefined, + values: [undefined], + }) + mutators.change(1,2,3,4) + expect(form.getFieldState('a', (state => ({ values: state.values, value: state.value })))).toEqual({ + value: 1, + values: [1,2,3,4], + }) + }) + + test('focus', async () => { + const form = createForm() + const a = form.registerField({ path: 'a' }) + const mutators = form.createMutators(a) + expect(form.getFormGraph()).toMatchSnapshot() + expect(form.getFieldState('a', (state => ({ active: state.active, visited: state.visited })))).toEqual({ + active: false, + visited: false, + }) + mutators.focus() + expect(form.getFormGraph()).toMatchSnapshot() + expect(form.getFieldState('a', (state => ({ active: state.active, visited: state.visited })))).toEqual({ + active: true, + visited: true, + }) + }) + + test('blur', async () => { + const form = createForm() + const a = form.registerField({ path: 'a' }) + const mutators = form.createMutators(a) + expect(form.getFormGraph()).toMatchSnapshot() + expect(form.getFieldState('a', (state => ({ active: state.active, visited: state.visited })))).toEqual({ + active: false, + visited: false, + }) + mutators.focus() + mutators.blur() + expect(form.getFormGraph()).toMatchSnapshot() + expect(form.getFieldState('a', (state => ({ active: state.active, visited: state.visited })))).toEqual({ + active: false, + visited: true, + }) + }) + + test('push', async () => { + const form = createForm() + const mm = form.registerField({ path: 'mm', value: [] }) + const mutators = form.createMutators(mm) + expect(form.getFieldValue('mm')).toEqual([]) + mutators.push({}) + expect(form.getFieldValue('mm')).toEqual([{}]) + }) + + test('pop', async () => { + const form = createForm() + const mm = form.registerField({ path: 'mm', value: [{}] }) + const mutators = form.createMutators(mm) + expect(form.getFieldValue('mm')).toEqual([{}]) + mutators.pop() + expect(form.getFieldValue('mm')).toEqual([]) + }) + + test('insert', async () => { + const form = createForm() + const mm = form.registerField({ path: 'mm', value: arr }) + const mutators = form.createMutators(mm) + expect(form.getFieldValue('mm')).toEqual(arr) + mutators.insert(1, 'x') + expect(form.getFieldValue('mm')).toEqual(['a','x','b']) + }) + + test('remove', async () => { + const form = createForm() + const mm = form.registerField({ path: 'mm', value: arr }) + const mutators = form.createMutators(mm) + expect(form.getFieldValue('mm')).toEqual(arr) + mutators.remove(1) + expect(form.getFieldValue('mm')).toEqual(arr.slice(0, 1)) + }) + + test('exist', async () => { + const form = createForm() + const mm = form.registerField({ path: 'mm', value: arr }) + const mutators = form.createMutators(mm) + expect(mutators.exist(1)).toEqual(true) + }) + + test('shift', async () => { + const form = createForm() + const mm = form.registerField({ path: 'mm', value: arr }) + const mutators = form.createMutators(mm) + expect(form.getFieldValue('mm')).toEqual(arr) + mutators.shift() + expect(form.getFieldValue('mm')).toEqual(arr.slice(1)) + }) + + test('unshift', async () => { + const form = createForm() + const mm = form.registerField({ path: 'mm', value: arr }) + const mutators = form.createMutators(mm) + expect(form.getFieldValue('mm')).toEqual(arr) + mutators.unshift('x') + expect(form.getFieldValue('mm')).toEqual(['x', ...arr]) + }) + + test('move', async () => { + const form = createForm() + const mm = form.registerField({ path: 'mm', value: arr }) + const mutators = form.createMutators(mm) + expect(form.getFieldValue('mm')).toEqual(arr) + mutators.move(0, 1) + expect(form.getFieldValue('mm')).toEqual(arr.reverse()) + }) + + test('validate', async () => { + const form = createForm() + const mm = form.registerField({ path: 'mm', rules: [(v) => v === undefined ? ({ type: 'warning', message: 'warning msg' }) : undefined] }) + const mutators = form.createMutators(mm) + const result = await mutators.validate() + expect(result.errors).toEqual([]) + expect(result.warnings).toEqual([{ path: 'mm', messages: ['warning msg'] }]) + mutators.change(1) + const result2 = await mutators.validate() + expect(result2.errors).toEqual([]) + expect(result2.warnings).toEqual([]) + }) +}) + +describe('transformDataPath', () => { + test('normal path', async () => { + const form = createForm() + form.registerField({ path: 'a' }) + form.registerField({ path: 'a.b' }) + form.registerField({ path: 'a.b.c' }) + form.registerField({ path: 'a.b.c.d' }) + form.registerField({ path: 'a.b.c.d.e' }) + + expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a')).toString()).toEqual('a') + expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b')).toString()).toEqual('a.b') + expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c')).toString()).toEqual('a.b.c') + expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d')).toString()).toEqual('a.b.c.d') + expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d.e')).toString()).toEqual('a.b.c.d.e') + }) + + test('virtual path', async () => { + const form = createForm() + form.registerField({ path: 'a' }) + form.registerVirtualField({ path: 'a.b' }) + form.registerField({ path: 'a.b.c' }) + form.registerVirtualField({ path: 'a.b.c.d' }) + form.registerField({ path: 'a.b.c.d.e' }) + + expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a')).toString()).toEqual('a') + expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b')).toString()).toEqual('a') + expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c')).toString()).toEqual('a.c') + expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d')).toString()).toEqual('a.c') + expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d.e')).toString()).toEqual('a.c.e') + }) + + test('virtual path(head)', async () => { + const form = createForm() + form.registerVirtualField({ path: 'a' }) + form.registerField({ path: 'a.b' }) + form.registerField({ path: 'a.b.c' }) + + expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a')).toString()).toEqual('') + expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b')).toString()).toEqual('b') + expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c')).toString()).toEqual('b.c') + }) +}) + +describe('major sences', () => { + test('dynamic remove with intialValues', async () => { + const form = createForm({ + initialValues: { + aa: [{ aa: 123, bb: 321 }, { aa: 345, bb: 678 }] + } + }) + const aa = form.registerField({ path: 'aa' }) + form.registerField({ path: 'aa.0' }) + form.registerField({ path: 'aa.0.aa' }) + form.registerField({ path: 'aa.0.bb' }) + form.registerField({ path: 'aa.1' }) + form.registerField({ path: 'aa.1.aa' }) + form.registerField({ path: 'aa.1.bb' }) + form.setFieldState('aa.1.aa', state => { + state.value = 'change aa' + }) + const mutators = form.createMutators(aa) + const snapshot = form.getFormGraph() + expect(snapshot).toMatchSnapshot() + mutators.remove(0) + expect(form.getFormGraph()).toMatchSnapshot() + form.setFormGraph(snapshot) + expect(form.getFormGraph()).toMatchSnapshot() + }) + + test('nested dynamic remove', () => { + const form = createForm({ + useDirty: true + }) + const aa = form.registerField({ path: 'aa', value: [] }) + form.registerField({ path: 'aa.0' }) + form.registerField({ path: 'aa.0.aa' }) + form.registerField({ path: 'aa.0.bb' }) + form.registerField({ path: 'aa.1' }) + form.registerField({ path: 'aa.1.aa' }) + form.registerField({ path: 'aa.1.bb' }) + form.setFieldState('aa.1.aa', state => { + state.value = 'change aa' + }) + + const mutators = form.createMutators(aa) + const snapshot = form.getFormGraph() + expect(snapshot).toMatchSnapshot() + mutators.remove(0) + expect(form.getFormGraph()).toMatchSnapshot() + form.setFormGraph(snapshot) + expect(form.getFormGraph()).toMatchSnapshot() + expect(form.getFormGraph()).toEqual(snapshot) + }) + + test('nested visible', () => { + const form = createForm() + form.registerField({ path: 'aa', value: {} }) + form.registerField({ path: 'aa.bb', initialValue: 123 }) + form.registerField({ path: 'aa.cc', initialValue: 222 }) + form.setFieldState('aa', state => state.visible = false) + expect(form.getFormState(state => state.values)).toEqual({}) + + expect(form.getFormGraph()).toMatchSnapshot() + form.setFieldState('aa.bb', state => state.value = '123') + expect(form.getFormGraph()).toMatchSnapshot() + + form.setFieldState('aa', state => state.visible = true) + expect(form.getFormGraph()).toMatchSnapshot() + }) + + test('deep nested visible(root)', () => { + const form = createForm() + form.registerField({ path: 'aa', value: {} }) + form.registerField({ path: 'aa.bb' }) + form.registerField({ path: 'aa.bb.cc', value: 123 }) + form.setFieldState('aa', state => state.visible = false) + expect(form.getFormGraph()).toMatchSnapshot() + expect(form.getFormState(state => state.values)).toEqual({}) + }) + + test('deep nested visible(middle part)', () => { + const form = createForm() + form.registerField({ path: 'aa', value: {} }) + form.registerField({ path: 'aa.bb' }) + form.registerField({ path: 'aa.bb.cc', value: 123 }) + form.setFieldState('aa.bb', state => state.visible = false) + expect(form.getFormGraph()).toMatchSnapshot() + expect(form.getFormState(state => state.values)).toEqual({ aa: {} }) + }) + + test('deep nested visible with VirtualField', () => { + const form = createForm() + form.registerField({ path: 'aa', value: {} }) + form.registerVirtualField({ path: 'aa.bb' }) + form.registerField({ path: 'aa.bb.cc', value: 123 }) + form.setFieldState('aa', state => { + state.visible = false + }) + expect(form.getFormGraph()).toMatchSnapshot() + }) +}) diff --git a/packages/core/src/__tests__/lifecycle.spec.ts b/packages/core/src/__tests__/lifecycle.spec.ts new file mode 100644 index 00000000000..d93d33fc19d --- /dev/null +++ b/packages/core/src/__tests__/lifecycle.spec.ts @@ -0,0 +1,13 @@ +//import { FormHeart, FormLifeCycle, LifeCycleTypes } from '../shared/lifecycle' + +test('create lifecycle',()=>{ + //todo +}) + +test('create form heart',()=>{ + //todo +}) + +test('heart with lifecycle',()=>{ + //todo +}) \ No newline at end of file diff --git a/packages/core/src/__tests__/model.spec.ts b/packages/core/src/__tests__/model.spec.ts new file mode 100644 index 00000000000..bfd1a6dea3e --- /dev/null +++ b/packages/core/src/__tests__/model.spec.ts @@ -0,0 +1,71 @@ +//import { createStateModel } from '../shared/model' + +test('createStateModel', () => { + //todo +}) + +describe('proxy model', () => { + test('subscribe', () => { + //todo + }) + test('unsubscribe', () => { + //todo + }) + test('batch', () => { + //todo + }) + test('getState', () => { + //todo + }) + test('setState', () => { + //todo + }) + test('getSourceState', () => { + //todo + }) + test('setSourceState', () => { + //todo + }) + test('setState', () => { + //todo + }) + test('hasChanged', () => { + //todo + }) + test('getChanged', () => { + //todo + }) +}) + +describe('dirty model', () => { + test('subscribe', () => { + //todo + }) + test('unsubscribe', () => { + //todo + }) + test('batch', () => { + //todo + }) + test('getState', () => { + //todo + }) + test('setState', () => { + //todo + }) + test('getSourceState', () => { + //todo + }) + test('setSourceState', () => { + //todo + }) + test('setState', () => { + //todo + }) + test('hasChanged', () => { + //todo + }) + test('getChanged', () => { + //todo + }) +}) diff --git a/packages/core/src/__tests__/vfield.state.spec.ts b/packages/core/src/__tests__/vfield.state.spec.ts new file mode 100644 index 00000000000..4c1f7679617 --- /dev/null +++ b/packages/core/src/__tests__/vfield.state.spec.ts @@ -0,0 +1,32 @@ +//import { VirtualFieldState } from '../state/VirtualField' + +test('subscribe', () => { + //todo +}) +test('unsubscribe', () => { + //todo +}) +test('batch', () => { + //todo +}) +test('getState', () => { + //todo +}) +test('setState', () => { + //todo +}) +test('getSourceState', () => { + //todo +}) +test('setSourceState', () => { + //todo +}) +test('setState', () => { + //todo +}) +test('hasChanged', () => { + //todo +}) +test('getChanged', () => { + //todo +}) diff --git a/packages/core/src/field.ts b/packages/core/src/field.ts deleted file mode 100644 index 5a5b8dc8237..00000000000 --- a/packages/core/src/field.ts +++ /dev/null @@ -1,586 +0,0 @@ -import { - Broadcast, - publishFieldState, - isEqual, - clone, - isFn, - isBool, - toArr, - isStr, - hasRequired, - resolveFieldPath, - isEmpty -} from './utils' -import { - IFieldOptions, - IRuleDescription, - Path, - IField, - IFormPathMatcher, - IFieldState -} from '@uform/types' -import { Form } from './form' - -const filterSchema = (value: any, key: string): boolean => - key !== 'properties' && key !== 'items' - -export class Field implements IField { - public dirty: boolean - - public dirtyType: string - - public pristine: boolean - - public valid: boolean - - public invalid: boolean - - public visible: boolean - - public display: boolean - - public editable: boolean - - public loading: boolean - - public errors: string[] - - public effectErrors: string[] - - public name: string - - public value: any - - public hiddenFromParent: boolean - - public shownFromParent: boolean - - public initialValue: any - - public namePath: string[] - - public path: string[] - - public rules: IRuleDescription[] - - public required: boolean - - public props: any - - public lastValidateValue: any - - private context: Form - - private removed: boolean - - private destructed: boolean - - private alreadyHiddenBeforeUnmount: boolean - - private fieldbrd: Broadcast - - private unSubscribeOnChange: () => void - - constructor(context: Form, options: IFieldOptions) { - this.fieldbrd = new Broadcast() - this.context = context - this.dirty = false - this.pristine = true - this.valid = true - this.removed = false - this.invalid = false - this.visible = true - this.display = true - this.editable = true - this.destructed = false - this.loading = false - this.errors = [] - this.props = {} - this.effectErrors = [] - this.initialize(options) - } - - public initialize(options: IFieldOptions) { - const rules = this.getRulesFromProps(options.props) - this.value = !isEqual(this.value, options.value) - ? clone(options.value) - : this.value - this.name = !isEmpty(options.name) ? options.name : this.name || '' - this.namePath = resolveFieldPath(this.name) - - this.path = resolveFieldPath( - !isEmpty(options.path) ? options.path : this.path || [] - ) - this.rules = !isEmpty(rules) ? rules : this.rules - this.required = hasRequired(this.rules) - - if (isEmpty(options.props)) { - this.initialValue = !isEmpty(options.initialValue) - ? options.initialValue - : this.initialValue - } else { - this.initialValue = !isEqual(this.initialValue, options.initialValue) - ? options.initialValue - : !isEmpty(this.initialValue) - ? this.initialValue - : this.getInitialValueFromProps(options.props) - this.props = !isEmpty(this.props) - ? { ...this.props, ...clone(options.props) } - : clone(options.props) - const editable = this.getEditableFromProps(options.props) - this.editable = !isEmpty(editable) ? editable : this.getContextEditable() - } - - if (options.initialValue) { - this.lastValidateValue = options.initialValue - } - - if ( - this.pristine && - !isEmpty(this.initialValue) && - ((isEmpty(this.value) && this.visible) || - (this.removed && !this.shownFromParent)) - ) { - this.value = clone(this.initialValue) - this.context.setIn(this.name, this.value) - } - - this.mount() - - if (isFn(options.onChange)) { - this.onChange(options.onChange) - } - - this.context.syncUpdate(() => { - this.context.dispatchEffect('onFieldInit', this.publishState()) - }) - } - - public getInitialValueFromProps(props: any) { - if (props) { - if (!isEqual(this.initialValue, props.default)) { - return props.default - } - } - } - - public getContextEditable() { - return this.getEditable(this.context.editable) - } - - public getEditableFromProps(props: any) { - if (props) { - if (!isEmpty(props.editable)) { - return this.getEditable(props.editable) - } else { - if (props['x-props'] && !isEmpty(props['x-props'].editable)) { - return this.getEditable(props['x-props'].editable) - } - } - } - } - - public getRulesFromProps(props: any) { - if (props) { - const rules = toArr(props['x-rules']) - if (props.required && !rules.some(rule => rule.required)) { - rules.push({ required: true }) - } - return clone(rules) - } - return this.rules - } - - public getRequiredFromProps(props: any) { - if (!isEmpty(props.required)) { - return props.required - } - } - - public getEditable(editable: boolean | ((name: string) => boolean)): boolean { - if (isFn(editable)) { - return editable(this.name) - } - if (isBool(editable)) { - return editable - } - return this.editable - } - - public onChange(fn: (payload: any) => void) { - if (isFn(fn)) { - if (this.unSubscribeOnChange) { - this.unSubscribeOnChange() - } - fn(this.publishState()) - this.unSubscribeOnChange = this.subscribe(fn) - } - } - - public pathEqual(path: Path | IFormPathMatcher): boolean { - if (isStr(path)) { - if (path === this.name) { - return true - } - } - - path = resolveFieldPath(path) - - if (path.length === this.path.length) { - for (let i = 0; i < path.length; i++) { - if (path[i] !== this.path[i]) { - return false - } - } - return true - } else if (path.length === this.namePath.length) { - for (let i = 0; i < path.length; i++) { - if (path[i] !== this.namePath[i]) { - return false - } - } - return true - } - - return false - } - - public match(path: Path | IFormPathMatcher) { - if (isFn(path)) { - return path(this) - } - if (isStr(path)) { - if (path === this.name) { - return true - } - } - - path = resolveFieldPath(path) - - if (path.length === this.path.length) { - for (let i = 0; i < path.length; i++) { - if (path[i] !== this.path[i]) { - return false - } - } - return true - } else if (path.length === this.namePath.length) { - for (let i = 0; i < path.length; i++) { - if (path[i] !== this.namePath[i]) { - return false - } - } - return true - } - - return false - } - - public publishState() { - return publishFieldState(this) - } - - public syncContextValue() { - if (this.visible) { - const contextValue = this.context.getValue(this.name, true) - const contextInitialValue = this.context.getInitialValue( - this.name, - this.path - ) - if (!isEqual(this.value, contextValue)) { - this.value = contextValue - } - if (!isEqual(this.initialValue, contextInitialValue)) { - this.initialValue = contextInitialValue - } - } - } - - public subscribe(callback) { - return this.fieldbrd.subscribe(callback) - } - - public notify(force?: boolean) { - if (!this.dirty && !force) { - return - } - this.fieldbrd.notify(this.publishState()) - this.dirty = false - this.dirtyType = '' - } - - public unsubscribe() { - this.fieldbrd.unsubscribe() - } - - public changeProps(props: any, force?: boolean) { - const lastProps = this.props - if (isEmpty(props)) { - return - } - if (force || !isEqual(lastProps, props, filterSchema)) { - this.props = clone(props, filterSchema) - const editable = this.getEditableFromProps(this.props) - if (!isEmpty(editable)) { - this.editable = this.getEditableFromProps(this.props) - } - const rules = this.getRulesFromProps(this.props) - if (!isEmpty(rules)) { - this.rules = rules - } - this.dirty = true - this.notify() - } - } - - public changeEditable(editable: boolean | ((name: string) => boolean)): void { - if (!this.props || !isEmpty(this.props.editable)) { - return - } - if (this.props['x-props'] && !isEmpty(this.props['x-props'].editable)) { - return - } - this.editable = this.getEditable(editable) - this.dirty = true - this.notify() - } - - public mount() { - if (this.removed) { - if (!this.alreadyHiddenBeforeUnmount && !this.visible) { - this.visible = true - } - this.removed = false - this.context.dispatchEffect('onFieldChange', this.publishState()) - } - } - - public unmount() { - if (!this.visible) { - this.alreadyHiddenBeforeUnmount = true - } else { - this.alreadyHiddenBeforeUnmount = false - } - this.visible = false - this.removed = true - if (!this.context) { - return - } - if (!this.hiddenFromParent) { - this.context.deleteIn(this.name) - } - } - - public checkState(published = this.publishState()) { - if (!isEqual(published.value, this.value)) { - this.value = published.value - this.pristine = false - this.context.setIn(this.name, this.value) - this.context.updateChildrenValue(this) - this.dirtyType = 'value' - this.dirty = true - } - - if (!isEqual(published.initialValue, this.initialValue)) { - this.initialValue = published.initialValue - this.context.setInitialValueIn(this.name, this.value) - this.context.updateChildrenInitalValue(this) - this.dirtyType = 'initialValue' - this.dirty = true - } - - const editable = this.getEditable(published.editable) - if (!isEqual(editable, this.editable)) { - this.editable = editable - this.dirtyType = 'editable' - this.dirty = true - } else { - const prevEditable = this.getEditableFromProps(this.props) - const propsEditable = this.getEditableFromProps(published.props) - if ( - !isEmpty(propsEditable) && - !isEqual(propsEditable, this.editable) && - !isEqual(prevEditable, propsEditable) - ) { - this.editable = propsEditable - this.dirtyType = 'editable' - this.dirty = true - } - } - - published.errors = toArr(published.errors).filter(v => !!v) - - if (!isEqual(published.errors, this.effectErrors)) { - this.effectErrors = published.errors - this.valid = this.effectErrors.length > 0 && this.errors.length > 0 - this.invalid = !this.valid - this.dirtyType = 'errors' - this.dirty = true - } - if (!isEqual(published.rules, this.rules)) { - this.rules = published.rules - this.errors = [] - this.valid = true - if (hasRequired(this.rules)) { - this.required = true - published.required = true - } - this.invalid = false - this.dirtyType = 'rules' - this.dirty = true - } else { - const prePropsRules = this.getRulesFromProps(this.props) - const propsRules = this.getRulesFromProps(published.props) - if ( - !isEmpty(propsRules) && - !isEqual(prePropsRules, propsRules) && - !isEqual(propsRules, this.rules) - ) { - this.rules = propsRules - this.errors = [] - if (hasRequired(this.rules)) { - this.required = true - published.required = true - } - this.valid = true - this.invalid = false - this.dirtyType = 'rules' - this.dirty = true - } - } - if (!isEqual(published.required, this.required)) { - this.required = !!published.required - if (this.required) { - if (!hasRequired(this.rules)) { - this.rules = toArr(this.rules).concat({ - required: true - }) - this.errors = [] - this.valid = true - this.invalid = false - } - } else { - this.rules = toArr(this.rules).filter(rule => { - if (rule && rule.required) { - return false - } - return true - }) - this.errors = [] - this.valid = true - this.invalid = false - } - this.dirty = true - } else { - const propsRequired = this.getRequiredFromProps(published.props) - if (!isEmpty(propsRequired) && !isEqual(propsRequired, this.required)) { - this.required = !!propsRequired - this.errors = [] - if (this.required) { - if (!hasRequired(this.rules)) { - this.rules = toArr(this.rules).concat({ - required: true - }) - this.errors = [] - this.valid = true - this.invalid = false - } - } else { - this.rules = toArr(this.rules).filter(rule => { - if (rule && rule.required) { - return false - } - return true - }) - this.errors = [] - this.valid = true - this.invalid = false - } - this.dirty = true - } - } - - if (published.loading !== this.loading) { - this.loading = published.loading - this.dirtyType = 'loading' - this.dirty = true - } - - if (!isEqual(published.visible, this.visible)) { - this.visible = !!published.visible - if (this.visible) { - this.value = - this.value !== undefined ? this.value : clone(this.initialValue) - if (this.value !== undefined) { - this.context.setIn(this.name, this.value) - } - this.context.updateChildrenVisible(this, true) - } else { - this.context.deleteIn(this.name) - this.context.updateChildrenVisible(this, false) - } - this.dirtyType = 'visible' - this.dirty = true - } - - if (!isEqual(published.display, this.display)) { - this.display = !!published.display - this.context.updateChildrenDisplay(this, this.display) - this.dirtyType = 'display' - this.dirty = true - } - - if (!isEqual(published.props, this.props, filterSchema)) { - this.props = clone(published.props, filterSchema) - this.dirtyType = 'props' - this.dirty = true - } - if (this.editable === false) { - this.errors = [] - this.effectErrors = [] - } - } - - public updateState(reducer: (fieldStte: IFieldState) => void) { - if (!isFn(reducer)) { - return - } - if (this.removed) { - return - } - const published = { - name: this.name, - path: this.path, - props: clone(this.props, filterSchema), - value: clone(this.value), - initialValue: clone(this.initialValue), - valid: this.valid, - loading: this.loading, - editable: this.editable, - invalid: this.invalid, - pristine: this.pristine, - rules: clone(this.rules), - errors: clone(this.effectErrors), - visible: this.visible, - display: this.display, - required: this.required - } - reducer(published) - this.checkState(published) - } - - public destructor() { - if (this.destructed) { - return - } - this.destructed = true - if (this.value !== undefined) { - this.value = undefined - this.context.deleteIn(this.name) - } - this.context.updateChildrenVisible(this, false) - delete this.context - this.unsubscribe() - delete this.fieldbrd - } -} diff --git a/packages/core/src/form.ts b/packages/core/src/form.ts deleted file mode 100644 index 69928865b3c..00000000000 --- a/packages/core/src/form.ts +++ /dev/null @@ -1,847 +0,0 @@ -import { - Broadcast, - each, - reduce, - isEqual, - isFn, - isStr, - isArr, - setIn, - getIn, - deleteIn, - clone, - isEmpty, - toArr, - publishFormState, - raf, - caf, - isChildField, - getSchemaNodeFromPath, - BufferList, - defer -} from './utils' -import { Field } from './field' -import { runValidation, format } from '@uform/validator' -import { Subject } from 'rxjs/internal/Subject' -import { filter } from 'rxjs/internal/operators/filter' -import { FormPath } from './path' -import { - IFormOptions, - IFieldOptions, - IFieldState, - IField, - IFormPathMatcher, - IFormState, - ISchema, - Path, - IFieldMap, - ISubscribers -} from '@uform/types' - -type Editable = boolean | ((name: string) => boolean) - -const defaults = (opts: T): T => - ({ - initialValues: {}, - values: {}, - onSubmit: () => {}, - effects: () => {}, - ...opts - } as T) - -export class Form { - public editable: Editable - - private options: IFormOptions - - private publisher: Broadcast - - private state: IFormState - - private fields: IFieldMap - - private subscribes: ISubscribers - - private updateQueue: any[] - - private updateBuffer: BufferList - - private schema: ISchema - - private initialized: boolean - - private destructed: boolean - - private fieldSize: number - - private syncUpdateMode: boolean - - private updateRafId: any - - private rafValidateId: any - - private batchUpdateField: boolean - - private validating: boolean - - private traverse: (schema: ISchema) => ISchema - - constructor(opts: IFormOptions) { - this.getFieldState = this.getFieldState.bind(this) - this.getFormState = this.getFormState.bind(this) - this.options = defaults(opts) - this.publisher = new Broadcast() - this.initialized = false - this.state = {} as IFormState - this.fields = {} - this.subscribes = opts.subscribes || {} - this.updateQueue = [] - this.updateBuffer = new BufferList() - this.editable = opts.editable - this.schema = opts.schema || {} - this.traverse = opts.traverse - this.initialize({ - values: this.options.values, - initialValues: this.options.initialValues - }) - this.initializeEffects() - this.initialized = true - this.destructed = false - this.fieldSize = 0 - } - - public changeValues(values: any) { - const lastValues = this.state.values - const lastDirty = this.state.dirty - this.state.values = values || {} - this.state.dirty = - lastDirty || (this.initialized ? !isEqual(values, lastValues) : false) - this.updateFieldsValue() - } - - public changeEditable(editable: Editable) { - this.editable = editable - each(this.fields, field => { - field.changeEditable(editable) - }) - } - - public isDirtyValues(values: any) { - return !isEmpty(values) && !isEqual(this.state.values, values) - } - - public setFieldState = ( - path: Path | IFormPathMatcher, - callback: (fieldState: IFieldState) => void - ): Promise => { - if (this.destructed) { - return - } - return new Promise(resolve => { - if (isStr(path) || isArr(path) || isFn(path)) { - this.updateQueue.push({ path, callback, resolve }) - } - if (this.syncUpdateMode) { - this.updateFieldStateFromQueue() - return resolve() - } else if (this.updateQueue.length > 0) { - if (this.updateRafId !== undefined) { - caf(this.updateRafId) - } - this.updateRafId = raf(() => { - if (this.destructed) { - return - } - this.updateFieldStateFromQueue() - }) - } else { - return resolve() - } - }) - } - - public getFieldState( - path: Path | IFormPathMatcher, - callback: (fieldState: IFieldState) => void - ): void - public getFieldState(path: Path | IFormPathMatcher): IFieldState - - public getFieldState( - path: Path | IFormPathMatcher, - callback?: (fieldState: IFieldState) => void - ): any { - let field: IField - each(this.fields, innerField => { - if (innerField.match(path)) { - field = innerField - return false - } - }) - if (field) { - field.syncContextValue() - return isFn(callback) - ? callback(field.publishState()) - : field.publishState() - } - } - - public getFormState(callback: (formState: IFormState) => void): void - public getFormState(): IFormState - public getFormState(callback?: any): any { - return isFn(callback) ? callback(this.publishState()) : this.publishState() - } - - public setFormState = (callback: (formState: IFormState) => void) => { - if (this.destructed) { - return - } - if (!isFn(callback)) { - return - } - const published = this.publishState() - callback(published) - return Promise.resolve(this.checkState(published)) - } - - public registerField(name: string, options: IFieldOptions) { - const value = this.getValue(name) - const initialValue = this.getInitialValue(name, options.path) - const field = this.fields[name] - if (field) { - field.initialize({ - path: options.path, - onChange: options.onChange, - value, - initialValue - } as IFieldOptions) - this.asyncUpdate(() => { - this.updateFieldStateFromBuffer(field) - }) - } else { - this.fields[name] = new Field(this, { - name, - value, - path: options.path, - initialValue, - props: this.traverse ? this.traverse(options.props) : options.props - }) - const field = this.fields[name] - if (options.onChange) { - this.asyncUpdate(() => { - this.updateFieldStateFromBuffer(field) - field.onChange(options.onChange) - }) - this.dispatchEffect('onFieldChange', field.publishState()) - } - this.fieldSize++ - } - return this.fields[name] - } - - public setIn(name: string, value: any) { - setIn(this.state.values, name, value, path => { - return getSchemaNodeFromPath(this.schema, path) - }) - } - - public setInitialValueIn(name: string, value: any) { - setIn(this.state.initialValues, name, value) - } - - public setValue(name: string, value: any) { - const field = this.fields[name] - if (field) { - field.updateState(state => { - state.value = value - }) - field.pristine = false - if (field.dirty) { - field.notify() - this.dispatchEffect('onFieldInputChange', field.publishState()) - this.internalValidate(this.state.values).then(() => { - this.formNotify(field.publishState()) - }) - } - } - } - - public setErrors(name: string, errors: string[] | string, ...args: string[]) { - errors = toArr(errors) - const field = this.fields[name] - if (field) { - const lastErrors = field.errors - if (!isEqual(lastErrors, errors)) { - field.errors = errors.map(msg => format(msg, ...args)) - if (errors.length) { - field.invalid = true - field.valid = false - } else { - field.invalid = false - field.valid = true - } - field.dirty = true - field.notify() - } - } - } - - public updateChildrenValue(parent: Field) { - if (!parent.path || this.batchUpdateField) { - return - } - each(this.fields, (field, $name) => { - if (isChildField(field, parent)) { - const newValue = this.getValue($name) - if (!isEqual(field.value, newValue)) { - field.dirty = true - field.value = newValue - field.notify() - this.dispatchEffect('onFieldChange', field.publishState()) - } - } - }) - } - - public updateChildrenInitalValue(parent: Field) { - if (!parent.path) { - return - } - each(this.fields, (field, $name) => { - if (isChildField(field, parent)) { - const newValue = this.getInitialValue($name) - if (!isEqual(field.initialValue, newValue)) { - field.dirty = true - field.initialValue = newValue - } - } - }) - } - - public updateFieldInitialValue(): Promise { - if (this.state.dirty && this.initialized) { - each(this.fields, (field, name) => { - const newValue = this.getInitialValue(name) - field.initialValue = newValue - }) - } - return Promise.resolve() - } - - public updateFieldsValue(validate = true): Promise { - const { promise, resolve } = defer() - const update = () => { - const updateList = [] - this.batchUpdateField = true - each(this.fields, (field, name) => { - const newValue = this.getValue(name) - field.updateState(state => { - state.value = newValue - }) - if (field.dirty) { - updateList.push( - new Promise(resolve => { - raf(() => { - if (this.destructed) { - return - } - field.notify() - this.dispatchEffect('onFieldChange', field.publishState()) - resolve() - }) - }) - ) - } - }) - this.batchUpdateField = false - resolve(Promise.all(updateList)) - } - if (this.state.dirty && this.initialized) { - if (validate) { - this.internalValidate(this.state.values, true).then(() => { - this.formNotify() - update() - }) - } else { - update() - } - } - - return promise - } - - public updateChildrenVisible(parent: Field, visible?: boolean) { - if (!parent.path) { - return - } - each(this.fields, (field, $name) => { - if ($name === parent.name) { - return - } - if (isChildField(field, parent)) { - if (!visible) { - this.deleteIn($name) - } else { - const value = - field.value !== undefined ? field.value : clone(field.initialValue) - if (field.value !== undefined) { - this.setIn($name, value) - } - } - if (field.visible !== visible) { - if (visible) { - if (field.hiddenFromParent) { - field.visible = visible - field.hiddenFromParent = false - field.shownFromParent = true - field.dirty = true - } - } else { - field.visible = visible - field.hiddenFromParent = true - field.shownFromParent = false - field.dirty = true - } - } - } - }) - } - - public updateChildrenDisplay(parent: Field, display?: boolean) { - if (!parent.path) { - return - } - each(this.fields, (field, $name) => { - if ($name === parent.name) { - return - } - if (isChildField(field, parent)) { - if (field.display !== display) { - if (display) { - if (field.hiddenFromParent) { - field.display = display - field.hiddenFromParent = false - field.shownFromParent = true - field.dirty = true - } - } else { - field.display = display - field.hiddenFromParent = true - field.shownFromParent = false - field.dirty = true - } - } - } - }) - } - - public getInitialValue(name: string, path?: Path) { - const initialValue = getIn(this.state.initialValues, name) - let schema: ISchema - let schemaDefault: any - if (initialValue === undefined) { - schema = path ? getSchemaNodeFromPath(this.schema, path) : undefined - schemaDefault = schema && schema.default - if (schemaDefault !== undefined) { - this.setIn(name, schemaDefault) - } - } - return initialValue !== undefined ? initialValue : schemaDefault - } - - public getValue(name?: string, copy?: boolean) { - return copy - ? clone(getIn(this.state.values, name)) - : getIn(this.state.values, name) - } - - public deleteIn(name: string) { - deleteIn(this.state.values, name) - } - - public deleteInitialValues(name: string) { - deleteIn(this.state.initialValues, name) - } - - public reset(forceClear?: boolean, validate: boolean = true) { - each(this.fields, (field, name) => { - const value = this.getValue(name) - const initialValue = this.getInitialValue(name, field.path) - if (!validate) { - if (field.errors.length > 0) { - field.errors = [] - field.dirty = true - } - if (field.effectErrors.length > 0) { - field.effectErrors = [] - field.dirty = true - } - } - if (!isEmpty(value) || !isEmpty(initialValue)) { - field.updateState(state => { - state.value = forceClear ? undefined : initialValue - }) - field.pristine = true - } - if (field.dirty) { - field.notify() - this.formNotify(field.publishState()) - } - }) - if (!validate) { - const formState = this.publishState() - this.dispatchEffect('onFormReset', formState) - if (isFn(this.options.onReset)) { - this.options.onReset({ formState }) - } - } else { - this.internalValidate(this.state.values, true).then(() => { - this.formNotify() - raf(() => { - if (this.destructed) { - return - } - const formState = this.publishState() - this.dispatchEffect('onFormReset', formState) - if (isFn(this.options.onReset)) { - this.options.onReset({ formState }) - } - }) - }) - } - } - - public publishState() { - return publishFormState(this.state) - } - - public formNotify(fieldState?: IFieldState) { - const formState = this.publishState() - if (isFn(this.options.onFieldChange)) { - this.options.onFieldChange({ formState, fieldState }) - } - if (fieldState) { - this.dispatchEffect('onFieldChange', fieldState) - } - if (this.state.dirty) { - this.publisher.notify({ formState, fieldState }) - } - this.state.dirty = false - return formState - } - - public validate(): Promise { - this.validating = true - return this.internalValidate(this.state.values, true).then(() => { - return new Promise((resolve, reject) => { - this.formNotify() - raf(() => { - this.validating = false - if (this.destructed) { - return - } - if (this.state.valid) { - resolve(this.publishState()) - } else { - if (this.options.onValidateFailed) { - this.options.onValidateFailed(this.state.errors) - } - reject(this.state.errors) - } - }) - }) - }) - } - - public submit() { - if (this.validating) - return new Promise(resolve => { - resolve(this.publishState()) - }) - return this.validate().then((formState: IFormState) => { - this.dispatchEffect('onFormSubmit', formState) - if (isFn(this.options.onSubmit)) { - this.options.onSubmit({ formState }) - } - return formState - }) - } - - public subscribe(callback: (payload: any) => void) { - return this.publisher.subscribe(callback) - } - - public destructor() { - if (this.destructed) { - return - } - this.destructed = true - this.publisher.unsubscribe() - each(this.subscribes, effect => { - effect.unsubscribe() - }) - each(this.fields, (field, key) => { - field.destructor() - delete this.fields[key] - }) - this.fieldSize = 0 - delete this.fields - delete this.publisher - } - - public dispatchEffect = (eventName: string, ...args: any[]) => { - if (this.subscribes[eventName]) { - this.subscribes[eventName].next(...args) - } - } - - public syncUpdate(fn: () => void) { - if (isFn(fn)) { - this.syncUpdateMode = true - fn() - this.syncUpdateMode = false - } - } - - public initialize({ - initialValues = this.state.initialValues, - values = this.state.values - }) { - const lastValues = this.state.values - const lastDirty = this.state.dirty - const currentInitialValues = clone(initialValues) || {} - const currentValues = isEmpty(values) - ? clone(currentInitialValues) - : clone(values) || {} - this.state = { - valid: true, - invalid: false, - errors: [], - pristine: true, - initialValues: currentInitialValues, - values: currentValues, - dirty: - lastDirty || - (this.initialized ? !isEqual(currentValues, lastValues) : false) - } - if (this.options.onFormChange && !this.initialized) { - this.subscribe(this.options.onFormChange) - this.options.onFormChange({ - formState: this.publishState() - }) - } - this.updateFieldsValue(false) - } - - public selectEffect = ( - eventName: string, - eventFilter: string | IFormPathMatcher - ) => { - if (!this.subscribes[eventName]) { - this.subscribes[eventName] = new Subject() - } - if (isStr(eventFilter) || isFn(eventFilter)) { - const predicate = isStr(eventFilter) - ? FormPath.match(eventFilter as string) - : (eventFilter as IFormPathMatcher) - return this.subscribes[eventName].pipe(filter(predicate)) as Subject - } - return this.subscribes[eventName] - } - - private initializeEffects() { - const { effects } = this.options - if (isFn(effects)) { - effects(this.selectEffect, { - setFieldState: this.setFieldState, - getFieldState: this.getFieldState, - getFormState: this.getFormState, - setFormState: this.setFormState - }) - } - } - - private checkState(published: any): Promise { - if (!isEqual(this.state.values, published.values)) { - this.state.values = published.values - this.state.dirty = true - return this.updateFieldsValue() - } - if (!isEqual(this.state.initialValues, published.initialValues)) { - this.state.initialValues = published.initialValues - this.state.dirty = true - return this.updateFieldInitialValue() - } - - return Promise.resolve() - } - - private asyncUpdate(fn: () => void) { - if (isFn(fn)) { - if (this.syncUpdateMode) { - this.syncUpdateMode = false - fn() - this.syncUpdateMode = true - } else { - fn() - } - } - } - - private updateFieldStateFromQueue() { - const failed = {} - const rafIdMap = {} - each(this.updateQueue, ({ path, callback, resolve }, i) => { - each(this.fields, field => { - if (path && (isFn(path) || isArr(path) || isStr(path))) { - if (isFn(path) ? path(field) : field.pathEqual(path)) { - field.updateState(callback) - if (this.syncUpdateMode) { - field.dirty = false - } - if ((path as IFormPathMatcher).hasWildcard) { - this.updateBuffer.push( - (path as IFormPathMatcher).pattern, - callback, - { path, resolve } - ) - } - if (field.dirty) { - const dirtyType = field.dirtyType - field.notify() - if (rafIdMap[field.name]) { - caf(rafIdMap[field.name]) - } - rafIdMap[field.name] = raf(() => { - if (this.destructed) { - return - } - if (dirtyType === 'value') { - this.internalValidate().then(() => { - this.formNotify(field.publishState()) - }) - } else { - this.formNotify(field.publishState()) - } - }) - } - } else { - failed[i] = failed[i] || 0 - failed[i]++ - if (this.fieldSize <= failed[i]) { - if (isArr(path)) { - this.updateBuffer.push(path.join('.'), callback, { - path, - resolve - }) - } else if (isStr(path)) { - this.updateBuffer.push(path, callback, { path, resolve }) - } else if (isFn(path) && (path as IFormPathMatcher).pattern) { - this.updateBuffer.push( - (path as IFormPathMatcher).pattern, - callback, - { - path, - resolve - } - ) - } - } - } - } - }) - if (resolve && isFn(resolve)) { - resolve() - } - }) - this.updateQueue = [] - } - - private updateFieldStateFromBuffer(field: IField) { - const rafIdMap = {} - this.updateBuffer.forEach(({ path, values, key }) => { - if (isFn(path) ? path(field) : field.pathEqual(path)) { - values.forEach(callback => field.updateState(callback)) - if (this.syncUpdateMode) { - field.dirty = false - } - if (field.dirty) { - const dirtyType = field.dirtyType - field.notify() - if (rafIdMap[field.name]) { - caf(rafIdMap[field.name]) - } - rafIdMap[field.name] = raf(() => { - if (this.destructed) { - return - } - if (dirtyType === 'value') { - this.internalValidate().then(() => { - this.formNotify(field.publishState()) - }) - } else { - this.formNotify(field.publishState()) - } - }) - } - if (!path.hasWildcard) { - this.updateBuffer.remove(key) - } - } - }) - } - - private internalValidate( - values: any = this.state.values, - forceUpdate?: boolean - ) { - if (this.destructed) { - return - } - return new Promise(resolve => { - if (this.rafValidateId) { - caf(this.rafValidateId) - } - this.rafValidateId = raf(() => { - if (this.destructed) { - return resolve() - } - return runValidation( - values || this.state.values, - this.fields, - forceUpdate - ) - .then(response => { - const lastValid = this.state.valid - const newErrors = reduce( - response, - (buf, { name, errors }) => { - if (!errors.length) { - return buf - } else { - return buf.concat({ name, errors }) - } - }, - [] - ) - this.state.valid = newErrors.length === 0 - this.state.invalid = !this.state.valid - this.state.errors = newErrors - if (this.state.valid !== lastValid) { - this.state.dirty = true - } - const lastPristine = this.state.pristine - if (!isEqual(this.state.values, this.state.initialValues)) { - this.state.pristine = false - } else { - this.state.pristine = true - } - if (lastPristine !== this.state.pristine) { - this.state.dirty = true - } - return response - }) - .then(resolve) - }) - }) - } -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 64ed2905d4a..cd51939b6ae 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,94 +1,990 @@ -import { IFormOptions, ISchema } from '@uform/types' import { - setLocale as setValidationLocale, - setLanguage as setValidationLanguage -} from '@uform/validator' - -import { Form } from './form' -import { - calculateSchemaInitialValues, isFn, + isEqual, + toArr, + isNum, + isArr, + clone, + isValid, + FormPath, + FormPathPattern, each, - isEmpty, - clone -} from './utils' - -export * from './path' - -export const createForm = ({ - initialValues, - values, - onSubmit, - onReset, - schema, - onFormChange, - onFieldChange, - onFormWillInit, - subscribes, - editable, - effects, - onValidateFailed, - traverse -}: IFormOptions) => { - let fields = [] - let calculatedValues = calculateSchemaInitialValues( - schema, - isEmpty(values) ? clone(initialValues) : clone(values), - ({ name, path, schemaPath }, schema: ISchema, value: any) => { - fields.push({ name, path, schemaPath, schema, value }) - } - ) - - if (isEmpty(values)) { - initialValues = calculatedValues - } else { - values = calculatedValues - } - - const form = new Form({ - initialValues, - values, - onSubmit, - onReset, - subscribes, - onFormChange, - onFieldChange, - editable, - effects, - onValidateFailed, - schema, - traverse - }) + deprecate, + isObj +} from '@uform/shared' +import { + FormValidator, + setValidationLanguage, + setValidationLocale +} from '@uform/validator' +import { FormHeart } from './shared/lifecycle' +import { FormGraph } from './shared/graph' +import { FormState } from './state/form' +import { VirtualFieldState } from './state/virtual-field' +import { FieldState } from './state/field' +import { + IFormState, + IFieldState, + IVirtualFieldState, + IFormCreatorOptions, + IFieldStateProps, + IVirtualFieldStateProps, + IForm, + IFormSubmitResult, + IFormValidateResult, + IFormResetOptions, + IField, + IVirtualField, + isField, + FormHeartSubscriber, + LifeCycleTypes, + isVirtualField +} from './types' +export * from './shared/lifecycle' +export * from './types' - if (isFn(onFormWillInit)) { - onFormWillInit(form) +export const createForm = (options: IFormCreatorOptions = {}): IForm => { + function onGraphChange({ type, payload }) { + heart.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE, graph) + if (type === 'GRAPH_NODE_WILL_UNMOUNT') { + validator.unregister(payload.path.toString()) + } + } + + function onFormChange(published: IFormState) { + heart.notify(LifeCycleTypes.ON_FORM_CHANGE, state) + const valuesChanged = state.hasChanged('values') + const initialValuesChanged = state.hasChanged('initialValues') + const unmountedChanged = state.hasChanged('unmounted') + const mountedChanged = state.hasChanged('mounted') + const initializedChanged = state.hasChanged('initialized') + const editableChanged = state.hasChanged('editable') + if (valuesChanged || initialValuesChanged) { + /** + * 影子更新:不会触发具体字段的onChange,如果不这样处理,会导致任何值变化都会导致整树rerender + */ + shadowUpdate(() => { + graph.eachChildren((field: IField | IVirtualField) => { + if (isField(field)) { + field.setState(state => { + if (state.visible) { + if (valuesChanged) { + const path = FormPath.parse(state.name) + const parent = graph.getLatestParent(path) + const parentValue = getFormValuesIn(parent.path) + const value = getFormValuesIn(state.name) + /** + * https://github.com/alibaba/uform/issues/267 dynamic remove node + */ + let removed = false + if ( + isArr(parentValue) && + !path.existIn(parentValue, parent.path) + ) { + if ( + !parent.path + .getNearestChildPathBy(path) + .existIn(parentValue, parent.path) + ) { + graph.remove(state.name) + removed = true + } + } else { + each(env.removeNodes, (_, name) => { + if (path.includes(name)) { + graph.remove(path) + delete env.removeNodes[name] + removed = true + } + }) + } + if (removed) return + if (!isEqual(value, state.value)) { + state.value = isValid(value) ? value : state.initialValue + } + } + if (initialValuesChanged) { + const initialValue = getFormInitialValuesIn(state.name) + if (!isEqual(initialValue, state.initialValue)) { + state.initialValue = initialValue + if (!isValid(state.value)) { + state.value = initialValue + } + } + } + } + }) + } + }) + }) + if (valuesChanged) { + heart.notify(LifeCycleTypes.ON_FORM_VALUES_CHANGE, state) + } + if (initialValuesChanged) { + heart.notify(LifeCycleTypes.ON_FORM_INITIAL_VALUES_CHANGE, state) + } + } + + if (editableChanged) { + graph.eachChildren((field: IField | IVirtualField) => { + if (isField(field)) { + field.setState(state => { + state.formEditable = published.editable + }) + } + }) + } + + if (unmountedChanged && published.unmounted) { + heart.notify(LifeCycleTypes.ON_FORM_UNMOUNT, state) + } + if (mountedChanged && published.mounted) { + heart.notify(LifeCycleTypes.ON_FORM_MOUNT, state) + } + if (initializedChanged) { + heart.notify(LifeCycleTypes.ON_FORM_INIT, state) + } } - fields = fields.map(({ name, schemaPath, schema }) => { - return form.registerField(name || schemaPath.join('.'), { - path: schemaPath, - props: schema + function onFieldChange({ onChange, field, path }) { + return (published: IFieldState) => { + const valueChanged = field.hasChanged('value') + const initialValueChanged = field.hasChanged('initialValue') + const visibleChanged = field.hasChanged('visible') + const displayChanged = field.hasChanged('display') + const unmountedChanged = field.hasChanged('unmounted') + const mountedChanged = field.hasChanged('mounted') + const initializedChanged = field.hasChanged('initialized') + const warningsChanged = field.hasChanged('warnings') + const errorsChanges = field.hasChanged('errors') + if (initializedChanged) { + heart.notify(LifeCycleTypes.ON_FIELD_INIT, field) + const isEmptyValue = !isValid(published.value) + const isEmptyInitialValue = !isValid(published.initialValue) + if (isEmptyValue || isEmptyInitialValue) { + field.setState((state: IFieldState) => { + if (isEmptyValue) state.value = getFormValuesIn(state.name) + if (isEmptyInitialValue) + state.initialValue = getFormInitialValuesIn(state.name) + }, true) + } + } + + if (visibleChanged) { + if (!published.visible) { + deleteFormValuesIn(path, true) + } else { + setFormValuesIn(path, published.value) + } + graph.eachChildren( + path, + childState => { + childState.setState((state: IFieldState) => { + state.visible = published.visible + }) + }, + false + ) + } + if (displayChanged) { + graph.eachChildren( + path, + childState => { + childState.setState((state: IFieldState) => { + state.display = published.display + }) + }, + false + ) + } + + if (unmountedChanged) { + if (published.unmounted) { + deleteFormValuesIn(path, true) + } else { + setFormValuesIn(path, published.value) + } + graph.eachChildren( + path, + childState => { + childState.setState((state: IFieldState) => { + state.unmounted = published.unmounted + }) + }, + false + ) + } + if (mountedChanged && published.mounted) { + heart.notify(LifeCycleTypes.ON_FIELD_MOUNT, field) + } + + if (valueChanged) { + setFormValuesIn(path, published.value) + heart.notify(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, field) + } + if (initialValueChanged) { + setFormInitialValuesIn(path, published.initialValue) + heart.notify(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, field) + } + + if (errorsChanges) { + syncFormMessages('errors', published.name, published.errors) + } + + if (warningsChanged) { + syncFormMessages('warnings', published.name, published.warnings) + } + + if (isFn(onChange) && (!env.shadowStage || env.leadingStage)) { + onChange(field) + } + heart.notify(LifeCycleTypes.ON_FIELD_CHANGE, field) + } + } + + //实时同步Form Messages + function syncFormMessages(type: string, path: string, messages: string[]) { + state.unsafe_setSourceState(state => { + let foundField = false + state[type] = state[type] || [] + state[type] = state[type].reduce((buf: any, item: any) => { + if (item.path === path) { + foundField = true + return messages.length ? buf.concat({ path, messages }) : buf + } else { + return buf.concat(item) + } + }, []) + if (!foundField && messages.length) { + state[type].push({ + path, + messages + }) + } + if (state.errors.length) { + state.invalid = true + state.valid = false + } else { + state.invalid = false + state.valid = true + } }) - }) + } + + function onVirtualFieldChange({ onChange, field, path }) { + return (published: IVirtualFieldState) => { + const visibleChanged = field.hasChanged('visible') + const displayChanged = field.hasChanged('display') + const unmountedChanged = field.hasChanged('unmounted') + const mountedChanged = field.hasChanged('mounted') + const initializedChnaged = field.hasChanged('initialized') + + if (initializedChnaged) { + heart.notify(LifeCycleTypes.ON_FIELD_INIT, field) + } + + if (visibleChanged) { + graph.eachChildren(path, childState => { + childState.setState((state: IVirtualFieldState) => { + state.visible = published.visible + }) + }) + } + + if (displayChanged) { + graph.eachChildren(path, childState => { + childState.setState((state: IVirtualFieldState) => { + state.display = published.display + }) + }) + } + + if (unmountedChanged) { + graph.eachChildren(path, childState => { + childState.setState((state: IVirtualFieldState) => { + state.unmounted = published.unmounted + }) + }) + } + if (mountedChanged && published.mounted) { + heart.notify(LifeCycleTypes.ON_FIELD_MOUNT, field) + } + if (isFn(onChange)) onChange(field) + heart.notify(LifeCycleTypes.ON_FIELD_CHANGE, field) + } + } - form.syncUpdate(() => { - form.dispatchEffect('onFormInit', form.publishState()) - each( - fields, - field => { - form.dispatchEffect('onFieldChange', field.publishState()) + function registerVirtualField({ + name, + path, + props, + onChange + }: IVirtualFieldStateProps): IVirtualField { + let nodePath = FormPath.parse(path || name) + let field: IVirtualField + const createField = () => { + let field: IVirtualField + field = new VirtualFieldState({ + nodePath, + useDirty: options.useDirty + }) + field.subscribe(onVirtualFieldChange({ onChange, field, path: nodePath })) + field.batch(() => { + batchRunTaskQueue(field, nodePath) + field.setState((state: IVirtualFieldState) => { + state.initialized = true + state.props = props + }) + }) + return field + } + if (graph.exist(nodePath)) { + field = graph.get(nodePath) + if (isField(field)) { + field = createField() + graph.replace(nodePath, field) + } + } else { + field = createField() + graph.appendNode(nodePath, field) + } + return field + } + + function registerField({ + path, + name, + value, + initialValue, + required, + rules, + editable, + onChange, + props + }: Exclude): IField { + let field: IField + let nodePath = FormPath.parse(path || name) + let dataPath = transformDataPath(path) + const createField = () => { + let field: IField + field = new FieldState({ + nodePath, + dataPath, + useDirty: options.useDirty + }) + heart.notify(LifeCycleTypes.ON_FIELD_WILL_INIT, field) + field.subscribe(onFieldChange({ onChange, field, path: nodePath })) + field.batch(() => { + batchRunTaskQueue(field, nodePath) + field.setState((state: IFieldState) => { + const formValue = getFormValuesIn(dataPath) + const formInitialValue = getFormInitialValuesIn(dataPath) + state.initialized = true + if (isValid(value)) { + // value > formValue > initialValue + state.value = value + } else { + state.value = isValid(formValue) ? formValue : initialValue + } + // initialValue > formInitialValue + state.initialValue = isValid(initialValue) + ? initialValue + : formInitialValue + + state.props = props + state.required = required + state.rules = rules as any + state.editable = editable + state.formEditable = options.editable + }) + }) + validator.register(nodePath, validate => { + const { value, rules, editable, visible, unmounted } = field.getState() + // 不需要校验的情况有: 非编辑态(editable),已销毁(unmounted), 逻辑上不可见(visible) + if (editable === false || visible === false || unmounted === true) + return validate(value, []) + clearTimeout((field as any).validateTimer) + ;(field as any).validateTimer = setTimeout(() => { + field.setState(state => { + state.validating = true + }) + }, 60) + validate(value, rules).then(({ errors, warnings }) => { + clearTimeout((field as any).validateTimer) + field.setState((state: IFieldState) => { + state.validating = false + state.ruleErrors = errors + state.ruleWarnings = warnings + }) + }) + }) + return field + } + if (graph.exist(nodePath)) { + field = graph.get(nodePath) + if (isVirtualField(nodePath)) { + field = createField() + graph.replace(nodePath, field) + } + } else { + field = createField() + graph.appendNode(nodePath, field) + } + return field + } + + function transformDataPath(path: FormPathPattern) { + const newPath = FormPath.getPath(path) + return newPath.reduce((path: FormPath, key: string, index: number) => { + const realPath = newPath.slice(0, index + 1) + const dataPath = path.concat(key) + const selected = graph.get(realPath) + if (isVirtualField(selected)) { + return path + } + return dataPath + }, FormPath.getPath('')) + } + + function setFormIn( + path: FormPathPattern, + key: string, + value: any, + silent?: boolean + ) { + state.setState(state => { + FormPath.setIn(state[key], path, value) + }, silent) + } + + function deleteFormIn(path: FormPathPattern, key: string, silent?: boolean) { + state.setState(state => { + FormPath.deleteIn(state[key], path) + }, silent) + } + + function deleteFormValuesIn(path: FormPathPattern, silent?: boolean) { + deleteFormIn(transformDataPath(path), 'values', silent) + } + + function setFormValuesIn( + path: FormPathPattern, + value?: any, + silent?: boolean + ) { + return setFormIn(transformDataPath(path), 'values', value, silent) + } + + function setFormInitialValuesIn( + path: FormPathPattern, + value?: any, + silent?: boolean + ) { + return setFormIn(transformDataPath(path), 'initialValues', value, silent) + } + + function getFormIn(path: FormPathPattern, key?: string) { + return state.getState(state => FormPath.getIn(state[key], path)) + } + + function getFormValuesIn(path: FormPathPattern) { + return getFormIn(transformDataPath(path), 'values') + } + + function getFormInitialValuesIn(path: FormPathPattern) { + return getFormIn(transformDataPath(path), 'initialValues') + } + + function createMutators(field: IField) { + if (!(field instanceof FieldState)) { + throw new Error( + 'The `createMutators` can only accept FieldState instance.' + ) + } + + function setValue(...values: any[]) { + field.setState((state: IFieldState) => { + state.value = values[0] + state.values = values + }) + heart.notify(LifeCycleTypes.ON_FIELD_INPUT_CHANGE, field) + heart.notify(LifeCycleTypes.ON_FORM_INPUT_CHANGE, state) + } + + function removeValue(key: string | number) { + const name = field.unsafe_getSourceState(state => state.name) + env.removeNodes[name] = true + field.setState((state: IFieldState) => { + state.value = undefined + state.values = [] + }, true) + deleteFormValuesIn(key ? FormPath.parse(name).concat(key) : name) + heart.notify(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, field) + heart.notify(LifeCycleTypes.ON_FIELD_INPUT_CHANGE, field) + heart.notify(LifeCycleTypes.ON_FORM_INPUT_CHANGE, state) + heart.notify(LifeCycleTypes.ON_FIELD_CHANGE, field) + } + + function getValue() { + return field.unsafe_getSourceState(state => state.value) + } + return { + change(...values: any[]) { + setValue(...values) + return values[0] + }, + focus() { + field.setState((state: IFieldState) => { + state.active = true + state.visited = true + }) }, - true - ) + blur() { + field.setState((state: IFieldState) => { + state.active = false + }) + }, + push(value: any) { + const arr = toArr(getValue()).slice() + arr.push(value) + setValue(arr) + return arr + }, + pop() { + const arr = toArr(getValue()).slice() + arr.pop() + setValue(arr) + return arr + }, + insert(index: number, value: any) { + const arr = toArr(getValue()).slice() + arr.splice(index, 0, value) + setValue(arr) + return arr + }, + remove(index?: number | string) { + let val = getValue() + if (isNum(index) && isArr(val)) { + val = [].concat(val) + val.splice(index, 1) + setValue(val) + } else { + removeValue(index) + } + }, + exist(index?: number | string) { + const newPath = field.unsafe_getSourceState(state => + FormPath.parse(state.path) + ) + let val = getValue() + return (index !== undefined ? newPath.concat(index) : newPath).existIn( + val, + newPath + ) + }, + unshift(value: any) { + const arr = toArr(getValue()).slice() + arr.unshift(value) + setValue(arr) + return arr + }, + shift() { + const arr = toArr(getValue()).slice() + arr.shift() + setValue(arr) + return arr + }, + move($from: number, $to: number) { + const arr = toArr(getValue()).slice() + const item = arr[$from] + arr.splice($from, 1) + arr.splice($to, 0, item) + setValue(arr) + return arr + }, + moveUp(index: number) { + const arr = toArr(getValue()).slice() + const item = arr[index] + const len = arr.length + arr.splice(index, 1) + arr.splice(index - 1 < 0 ? len - 1 : index - 1, 0, item) + setValue(arr) + return arr + }, + moveDown(index: number) { + const arr = toArr(getValue()).slice() + const item = arr[index] + const len = arr.length + arr.splice(index, 1) + arr.splice(index + 1 > len ? 0 : index + 1, 0, item) + setValue(arr) + return arr + }, + validate() { + return validate(field.unsafe_getSourceState(state => state.path)) + } + } + } + + function clearErrors(pattern: FormPathPattern = '*') { + // 1. 指定路径或全部子路径清理 + graph.eachChildren('', pattern, field => { + if (isField(field)) { + field.setState(state => { + state.ruleErrors = [] + state.ruleWarnings = [] + state.effectErrors = [] + state.effectWarnings = [] + }) + } + }) + } + + async function reset({ + forceClear = false, + validate = true + }: IFormResetOptions = {}): Promise { + let result: Promise + leadingUpdate(() => { + graph.eachChildren(field => { + field.setState((state: IFieldState) => { + state.modified = false + state.ruleErrors = [] + state.ruleWarnings = [] + state.effectErrors = [] + state.effectWarnings = [] + // forceClear仅对设置initialValues的情况下有意义 + if (forceClear || !isValid(state.initialValue)) { + if (isArr(state.value)) { + state.value = [] + } else if (!isObj(state.value)) { + state.value = undefined + } + } else { + const value = clone(state.initialValue) + if (isArr(state.value)) { + if (isArr(value)) { + state.value = value + } else { + state.value = [] + } + } else if (isObj(state.value)) { + if (isObj(value)) { + state.value = value + } else { + state.value = {} + } + } else { + state.value = value + } + } + }) + }) + if (isFn(options.onReset)) { + options.onReset() + } + if (validate) { + result = formApi.validate() + } + }) + + return result + } + + async function submit( + onSubmit?: (values: IFormState['values']) => any | Promise + ): Promise { + // 重复提交,返回前一次的promise + if (state.getState(state => state.submitting)) return env.submittingTask + onSubmit = onSubmit || options.onSubmit + state.setState(state => { + state.submitting = true + }) + heart.notify(LifeCycleTypes.ON_FORM_SUBMIT_START, state) + env.submittingTask = validate() + .then(validated => { + const { errors } = validated + if (errors.length) { + state.setState(state => { + state.submitting = false + }) + heart.notify(LifeCycleTypes.ON_FORM_SUBMIT_END, state) + return Promise.reject(errors) + } + if (isFn(onSubmit)) { + return Promise.resolve( + onSubmit(state.getState(state => state.values)) + ).then(payload => ({ validated, payload })) + } + return { validated, payload: undefined } + }) + .then(response => { + const { + validated: { errors, warnings } + } = response + state.setState(state => { + state.submitting = false + }) + heart.notify(LifeCycleTypes.ON_FORM_SUBMIT_END, state) + if (errors.length) { + return Promise.reject(errors) + } + if (warnings.length) { + console.warn(warnings) + } + return response + }) + return env.submittingTask + } + + async function validate( + path?: FormPathPattern, + opts?: {} + ): Promise { + if (!state.getState(state => state.validating)) { + state.unsafe_setSourceState(state => { + state.validating = true + }) + // 渲染优化 + clearTimeout(env.validateTimer) + env.validateTimer = setTimeout(() => { + state.notify() + }, 60) + } + + heart.notify(LifeCycleTypes.ON_FORM_VALIDATE_START, state) + return validator.validate(path, opts).then(payload => { + clearTimeout(env.validateTimer) + state.setState(state => { + state.validating = false + }) + if (isFn(options.onValidateFailed)) { + options.onValidateFailed(payload) + } + heart.notify(LifeCycleTypes.ON_FORM_VALIDATE_END, state) + return payload + }) + } + + function setFormState(callback?: (state: IFormState) => any) { + leadingUpdate(() => { + state.setState(callback) + }) + } + + function getFormState(callback?: (state: IFormState) => any) { + return state.getState(callback) + } + + function batchRunTaskQueue(field: IField | IVirtualField, path: FormPath) { + env.taskQueue.forEach((task, index) => { + const { pattern, callbacks } = task + if (pattern.match(path)) { + callbacks.forEach(callback => { + field.setState(callback) + }) + if (!path.isWildMatchPattern && !path.isMatchPattern) { + env.taskQueue.splice(index, 1) + env.taskQueue.forEach(({ pattern }, index) => { + if (pattern.toString() === path.toString()) { + env.taskIndexes[path.toString()] = index + } + }) + } + } + }) + } + + function setFieldState( + path: FormPathPattern, + callback?: (state: IFieldState) => void + ) { + if (!isFn(callback)) return + let matchCount = 0 + let pattern = FormPath.getPath(path) + graph.select(pattern, field => { + field.setState(callback) + matchCount++ + }) + if (matchCount === 0 || pattern.isWildMatchPattern) { + let taskIndex = env.taskIndexes[pattern.toString()] + if (isValid(taskIndex)) { + if ( + !env.taskQueue[taskIndex].callbacks.some(fn => isEqual(fn, callback)) + ) { + env.taskQueue[taskIndex].callbacks.push(callback) + } + } else { + env.taskIndexes[pattern.toString()] = env.taskQueue.length + env.taskQueue.push({ + pattern, + callbacks: [callback] + }) + } + } + } + + function setFieldValue(path: FormPathPattern, value?: any) { + setFieldState(path, state => { + state.value = value + }) + } + + function getFieldValue(path?: FormPathPattern) { + return getFormValuesIn(path) + } + + function setFieldInitialValue(path?: FormPathPattern, value?: any) { + setFieldState(path, state => { + state.initialValue = value + }) + } + + function getFieldInitialValue(path?: FormPathPattern) { + return getFormInitialValuesIn(path) + } + + function getFieldState( + path: FormPathPattern, + callback?: (state: IFieldState) => any + ) { + const field = graph.select(path) + return field && field.getState(callback) + } + + function getFormGraph() { + return graph.map(node => { + return node.getState() + }) + } + + function setFormGraph(nodes: {}) { + each(nodes, (node: IFieldState | IVirtualFieldState, key) => { + let nodeState: any + if (graph.exist(key)) { + nodeState = graph.get(key) + nodeState.unsafe_setSourceState(state => { + Object.assign(state, node) + }) + } else { + if (node.displayName === 'VirtualFieldState') { + nodeState = registerVirtualField({ + path: key + }) + nodeState.unsafe_setSourceState(state => { + Object.assign(state, node) + }) + } else if (node.displayName === 'FieldState') { + nodeState = registerField({ + path: key + }) + nodeState.unsafe_setSourceState(state => { + Object.assign(state, node) + }) + } + } + if (nodeState) { + nodeState.notify(state.getState()) + } + }) + } + + function shadowUpdate(callback: () => void) { + env.shadowStage = true + if (isFn(callback)) { + callback() + } + env.shadowStage = false + } + + function leadingUpdate(callback: () => void) { + env.leadingStage = true + if (isFn(callback)) { + callback() + } + env.leadingStage = false + } + + const state = new FormState(options) + const validator = new FormValidator(options) + const graph = new FormGraph() + const formApi = { + submit, + reset, + clearErrors, + validate, + setFormState, + getFormState, + setFieldState, + getFieldState, + registerField, + registerVirtualField, + createMutators, + getFormGraph, + setFormGraph, + setFieldValue, + unsafe_do_not_use_transform_data_path: transformDataPath, //eslint-disable-line + setValue: deprecate( + setFieldValue, + 'setValue', + 'Please use the setFieldValue.' + ), + getFieldValue, + getValue: deprecate( + getFieldValue, + 'getValue', + 'Please use the getFieldValue.' + ), + setFieldInitialValue, + getFieldInitialValue, + getInitialValue: deprecate( + getFieldInitialValue, + 'getInitialValue', + 'Please use the getFieldInitialValue.' + ), + subscribe: (callback?: FormHeartSubscriber) => { + heart.subscribe(callback) + }, + unsubscribe: (callback?: FormHeartSubscriber) => { + heart.unsubscribe(callback) + }, + notify: (type: string, payload: T) => { + heart.notify(type, payload) + } + } + const heart = new FormHeart({ ...options, context: formApi }) + const env = { + validateTimer: null, + graphChangeTimer: null, + shadowStage: false, + leadingStage: false, + taskQueue: [], + taskIndexes: {}, + removeNodes: {}, + submittingTask: undefined + } + heart.notify(LifeCycleTypes.ON_FORM_WILL_INIT, state) + state.subscribe(onFormChange) + graph.appendNode('', state) + state.setState((state: IFormState) => { + state.initialized = true }) - return form + graph.subscribe(onGraphChange) + return formApi } +export const registerValidationFormats = FormValidator.registerFormats + +export const registerValidationRules = FormValidator.registerRules + +export const registerValidationMTEngine = FormValidator.registerMTEngine + export { - setValidationLocale, setValidationLanguage, - Form, - calculateSchemaInitialValues + setValidationLocale, + FormPath, + FormPathPattern, + FormGraph } export default createForm diff --git a/packages/core/src/path.ts b/packages/core/src/path.ts deleted file mode 100644 index 8a60d3dcd9f..00000000000 --- a/packages/core/src/path.ts +++ /dev/null @@ -1,81 +0,0 @@ -import createMatcher from 'dot-match' -import { resolveFieldPath, isStr, isFn, isArr, reduce } from './utils' -import { IFormPathMatcher } from '@uform/types' -type Filter = (payload: any) => boolean - -const matchWithFilter = (result: boolean, filter: Filter, payload: any) => { - if (isFn(filter)) { - return result && filter(payload) - } - return result -} - -const wildcardRE = /\*/ - -export const FormPath = { - match( - pattern: string, - isRealPath?: boolean | Filter, - filter?: Filter - ): IFormPathMatcher { - pattern = pattern + '' - const match = createMatcher(pattern) - if (isFn(isRealPath)) { - filter = isRealPath as Filter - isRealPath = false - } - const matcher = (payload: any) => { - if (payload && payload.fieldState) { - return matchWithFilter( - match( - resolveFieldPath( - isRealPath ? payload.fieldState.path : payload.fieldState.name - ) - ), - filter, - payload.fieldState - ) - } else if (payload && payload.name && payload.path) { - return matchWithFilter( - match(resolveFieldPath(isRealPath ? payload.path : payload.name)), - filter, - payload - ) - } else if (isStr(payload)) { - return matchWithFilter(match(resolveFieldPath(payload)), filter, { - name: payload - }) - } else if (isArr(payload)) { - return matchWithFilter(match(payload), filter, { path: payload }) - } - return false - } - - matcher.hasWildcard = wildcardRE.test(pattern) - matcher.pattern = pattern - - return matcher - }, - exclude(matcher: IFormPathMatcher) { - return (path: any): boolean => - isFn(matcher) - ? !matcher(path) - : isStr(matcher) - ? !FormPath.match(matcher)(path) - : false - }, - transform( - path: string, - regexp: RegExp, - calllback: (...args: string[]) => string - ) { - const args = reduce( - resolveFieldPath(path), - (buf: string[], key: string) => { - return new RegExp(regexp).test(key) ? buf.concat(key) : buf - }, - [] - ) - return calllback(...args) - } -} diff --git a/packages/core/src/shared/graph.ts b/packages/core/src/shared/graph.ts new file mode 100644 index 00000000000..246bec5ad89 --- /dev/null +++ b/packages/core/src/shared/graph.ts @@ -0,0 +1,270 @@ +import { + each, + reduce, + map, + isFn, + FormPath, + FormPathPattern +} from '@uform/shared' +import { Subscrible } from './subscrible' +import { + FormGraphNodeRef, + FormGraphMatcher, + FormGraphEacher, +} from '../types' + +export class FormGraph extends Subscrible<{ + type: string + payload: FormGraphNodeRef +}> { + private refrences: { + [key in string]: FormGraphNodeRef + } + + private nodes: { + [key in string]: NodeType + } + + private buffer: { + path: FormPath + ref: FormGraphNodeRef + latestParent?: { + ref: FormGraphNodeRef + path: FormPath + } + }[] + + constructor() { + super() + this.refrences = {} + this.nodes = {} + this.buffer = [] + } + + /** + * 模糊匹配API + * @param path + * @param matcher + */ + select(path: FormPathPattern, matcher?: FormGraphMatcher) { + const pattern = FormPath.parse(path) + if (!matcher) { + const node = this.get(pattern) + if (node) { + return node + } + } + for (let name in this.nodes) { + const node = this.nodes[name] + if (pattern.match(name)) { + if (isFn(matcher)) { + const result = matcher(node, FormPath.parse(name)) + if (result === false) { + return node + } + } else { + return node + } + } + } + } + + get(path: FormPathPattern) { + return this.nodes[FormPath.getPath(path).toString()] + } + + selectParent(path: FormPathPattern) { + return this.get(FormPath.getPath(path).parent()) + } + + selectChildren(path: FormPathPattern) { + const ref = this.refrences[FormPath.getPath(path).toString()] + if (ref && ref.children) { + return reduce( + ref.children, + (buf, path) => { + return buf.concat(this.get(path)).concat(this.selectChildren(path)) + }, + [] + ) + } + return [] + } + + exist(path: FormPathPattern) { + return !!this.get(FormPath.getPath(path)) + } + + /** + * 递归遍历所有children + * 支持模糊匹配 + */ + eachChildren(eacher: FormGraphEacher, recursion?: boolean): void + eachChildren( + path: FormPathPattern, + eacher: FormGraphEacher, + recursion?: boolean + ): void + eachChildren( + path: FormPathPattern, + selector: FormPathPattern, + eacher: FormGraphEacher, + recursion?: boolean + ): void + eachChildren( + path: any, + selector: any = true, + eacher: any = true, + recursion: any = true + ) { + if (isFn(path)) { + recursion = selector + eacher = path + path = '' + selector = '*' + } + if (isFn(selector)) { + recursion = eacher + eacher = selector + selector = '*' + } + const ref = this.refrences[FormPath.getPath(path).toString()] + if (ref && ref.children) { + return each(ref.children, path => { + if (isFn(eacher)) { + const node = this.get(path) + if (node && FormPath.parse(path).match(selector)) { + eacher(node, path) + if (recursion) { + this.eachChildren(path, selector, eacher, recursion) + } + } + } + }) + } + } + + /** + * 递归遍历所有parent + */ + eachParent(path: FormPathPattern, eacher: FormGraphEacher) { + const selfPath = FormPath.getPath(path) + const ref = this.refrences[selfPath.toString()] + if (isFn(eacher)) { + eacher(this.get(selfPath), selfPath) + if (ref.parent) { + this.eachParent(ref.parent.path, eacher) + } + } + } + + getLatestParent(path: FormPathPattern) { + const selfPath = FormPath.getPath(path) + const parentPath = FormPath.getPath(path).parent() + if (selfPath.toString() === parentPath.toString()) return undefined + if (this.refrences[parentPath.toString()]) + return { ref: this.refrences[parentPath.toString()], path: parentPath } + return this.getLatestParent(parentPath) + } + + map(mapper: (node: NodeType) => any) { + return map(this.nodes, mapper) + } + + reduce( + reducer: (buffer: T, node: NodeType, key: string) => T, + initial: T + ) { + return reduce(this.nodes, reducer, initial) + } + + appendNode(path: FormPathPattern, node: NodeType) { + const selfPath = FormPath.getPath(path) + const parentPath = selfPath.parent() + const parentRef = this.refrences[parentPath.toString()] + const selfRef: FormGraphNodeRef = { + path: selfPath, + children: [] + } + if (this.get(selfPath)) return + this.nodes[selfPath.toString()] = node + this.refrences[selfPath.toString()] = selfRef + if (parentRef) { + parentRef.children.push(selfPath) + selfRef.parent = parentRef + } else { + const latestParent = this.getLatestParent(selfPath) + if (latestParent) { + latestParent.ref.children.push(selfPath) + selfRef.parent = latestParent.ref + this.buffer.push({ + path: selfPath, + ref: selfRef, + latestParent: latestParent + }) + } + } + this.buffer.forEach(({ path, ref, latestParent }, index) => { + if ( + path.parent().match(selfPath) || + (selfPath.includes(latestParent.path) && + path.includes(selfPath) && + selfPath.toString() !== path.toString()) + ) { + selfRef.children.push(path) + ref.parent = selfRef + latestParent.ref.children.splice( + latestParent.ref.children.indexOf(path), + 1 + ) + this.buffer.splice(index, 1) + } + }) + this.notify({ + type: 'GRAPH_NODE_DID_MOUNT', + payload: selfRef + }) + } + + remove(path: FormPathPattern) { + const selfPath = FormPath.getPath(path) + const selfRef = this.refrences[selfPath.toString()] + if (!selfRef) return + this.notify({ + type: 'GRAPH_NODE_WILL_UNMOUNT', + payload: selfRef + }) + if (selfRef.children) { + selfRef.children.forEach(path => { + this.remove(path) + }) + } + this.buffer = this.buffer.filter(({ ref }) => { + return selfRef !== ref + }) + delete this.nodes[selfPath.toString()] + delete this.refrences[selfPath.toString()] + if (selfRef.parent) { + selfRef.parent.children.forEach((path, index) => { + if (path.match(selfPath)) { + selfRef.parent.children.splice(index, 0) + } + }) + } + } + + replace(path: FormPathPattern, node: NodeType) { + const selfPath = FormPath.getPath(path) + const selfRef = this.refrences[selfPath.toString()] + if (!selfRef) return + this.notify({ + type: 'GRAPH_NODE_WILL_UNMOUNT', + payload: selfRef + }) + this.nodes[selfPath.toString()] = node + this.notify({ + type: 'GRAPH_NODE_DID_MOUNT', + payload: selfRef + }) + } +} diff --git a/packages/core/src/shared/lifecycle.ts b/packages/core/src/shared/lifecycle.ts new file mode 100644 index 00000000000..0f899bd0d9a --- /dev/null +++ b/packages/core/src/shared/lifecycle.ts @@ -0,0 +1,113 @@ +import { isFn, isStr, isArr, isObj, each } from '@uform/shared' +import { + FormLifeCyclePayload, + FormLifeCycleHandler, + FormHeartSubscriber +} from '../types' + +export class FormLifeCycle { + private listener: FormLifeCyclePayload + + constructor(handler: FormLifeCycleHandler) + constructor(type: string, handler: FormLifeCycleHandler) + constructor(handlerMap: { [key: string]: FormLifeCycleHandler }) + constructor(...params: any[]) { + this.listener = this.buildListener(params) + } + buildListener(params: any[]) { + return function(payload: { type: string; payload: Payload }, ctx: any) { + for (let index = 0; index < params.length; index++) { + let item = params[index] + if (isFn(item)) { + item.call(this, payload, ctx) + } else if (isStr(item) && isFn(params[index + 1])) { + if (item === payload.type) { + params[index + 1].call(this, payload.payload, ctx) + } + index++ + } else if (isObj(item)) { + each(item, (handler, type) => { + if (isFn(handler) && isStr(type)) { + if (type === payload.type) { + handler.call(this, payload.payload, ctx) + return false + } + } + }) + } + } + } + } + + notify = (type: any, payload: Payload, ctx?: any) => { + if (isStr(type)) { + this.listener.call(ctx, { type, payload }, ctx) + } + } +} + +export class FormHeart { + private lifecycles: FormLifeCycle[] + + private context: Context + + private subscribers: FormHeartSubscriber[] + + constructor({ + lifecycles, + context + }: { + lifecycles?: FormLifeCycle[] + context?: Context + }) { + this.lifecycles = this.buildLifeCycles(lifecycles || []) + this.subscribers = [] + this.context = context + } + + buildLifeCycles(lifecycles: FormLifeCycle[]) { + return lifecycles.reduce((buf, item) => { + if (item instanceof FormLifeCycle) { + return buf.concat(item) + } else { + if (typeof item === 'object') { + this.context = item + return buf + } else if (isArr(item)) { + return this.buildLifeCycles(item) + } + return buf + } + }, []) + } + + unsubscribe = (callback?: FormHeartSubscriber) => { + if (isFn(callback)) { + this.subscribers = this.subscribers.filter( + fn => fn.toString() !== callback.toString() + ) + } else { + this.subscribers = [] + } + } + + subscribe = (callback?: FormHeartSubscriber) => { + if ( + isFn(callback) && + !this.subscribers.some(fn => fn.toString() === callback.toString()) + ) { + this.subscribers.push(callback) + } + } + + notify = (type: any, payload: P, context?: C) => { + if (isStr(type)) { + this.lifecycles.forEach(lifecycle => { + lifecycle.notify(type, payload, context || this.context) + }) + this.subscribers.forEach(callback => { + callback({ type, payload }) + }) + } + } +} diff --git a/packages/core/src/shared/model.ts b/packages/core/src/shared/model.ts new file mode 100644 index 00000000000..532fd5d8f90 --- /dev/null +++ b/packages/core/src/shared/model.ts @@ -0,0 +1,180 @@ +import { clone, isEqual, isFn, each, globalThisPolyfill } from '@uform/shared' +import produce, { Draft } from 'immer' +import { Subscrible } from './subscrible' +import { IStateModelFactory, StateDirtyMap, IModel, StateModel } from '../types' +const hasProxy = !!globalThisPolyfill.Proxy + +export const createStateModel = ( + Factory: IStateModelFactory +) => { + return class Model extends Subscrible + implements IModel { + public state: State & { displayName?: string } + public props: Props & DefaultProps & { useDirty?: boolean } + public displayName?: string + public dirtyNum: number + public dirtyMap: StateDirtyMap + public batching: boolean + public controller: StateModel + constructor(defaultProps: DefaultProps) { + super() + this.state = { ...Factory.defaultState } + this.props = { + ...Factory.defaultProps, + ...defaultProps + } + this.dirtyMap = {} + this.dirtyNum = 0 + this.batching = false + this.controller = new Factory(this.state, this.props) + this.displayName = Factory.displayName + this.state.displayName = this.displayName + } + + batch = (callback?: () => void) => { + this.batching = true + if (isFn(callback)) { + callback() + } + if (this.dirtyNum > 0) { + this.notify(this.getState()) + } + this.dirtyMap = {} + this.dirtyNum = 0 + this.batching = false + } + + getState = (callback?: (state: State) => any) => { + if (isFn(callback)) { + return callback(this.getState()) + } else { + if (!hasProxy || this.props.useDirty) { + if (isFn(this.controller.publishState)) { + return this.controller.publishState(this.state) + } + return clone(this.state) + } else { + return produce(this.state, () => {}) + } + } + } + + unsafe_getSourceState = (callback?: (state: State) => any) => { + if (isFn(callback)) { + return callback(this.state) + } else { + return this.state + } + } + + unsafe_setSourceState = (callback: (state: State) => void) => { + if (isFn(callback)) { + if (!hasProxy || this.props.useDirty) { + callback(this.state) + } else { + this.state = produce(this.state, callback) + } + } + } + + setState = ( + callback: (state: State | Draft) => State | void, + silent = false + ) => { + if (isFn(callback)) { + if (!hasProxy || this.props.useDirty) { + const draft = this.getState() + this.dirtyNum = 0 + this.dirtyMap = {} + callback(draft) + if (isFn(this.controller.computeState)) { + this.controller.computeState(draft, this.state) + } + const draftKeys = Object.keys(draft || {}) + const stateKeys = Object.keys(this.state || {}) + each( + draftKeys.length > stateKeys.length ? draft : this.state, + (value, key) => { + if (!isEqual(value, draft[key])) { + this.state[key] = draft[key] + this.dirtyMap[key] = true + this.dirtyNum++ + } + } + ) + if (isFn(this.controller.dirtyCheck)) { + const result = this.controller.dirtyCheck(this.dirtyMap) + if (result !== undefined) { + Object.assign(this.dirtyMap, result) + } + } + if (this.dirtyNum > 0 && !silent) { + if (this.batching) return + this.notify(this.getState()) + this.dirtyMap = {} + this.dirtyNum = 0 + } + } else { + this.dirtyNum = 0 + this.dirtyMap = {} + //用proxy解决脏检查计算属性问题 + this.state = produce( + this.state, + draft => { + callback(draft) + if (isFn(this.controller.computeState)) { + this.controller.computeState(draft, this.state) + } + }, + patches => { + patches.forEach(({ path, op, value }) => { + if (!this.dirtyMap[path[0]]) { + if (op === 'replace') { + if (!isEqual(this.state[path[0]], value)) { + this.dirtyMap[path[0]] = true + this.dirtyNum++ + } + } else { + this.dirtyMap[path[0]] = true + this.dirtyNum++ + } + } + }) + } + ) + if (isFn(this.controller.dirtyCheck)) { + const result = this.controller.dirtyCheck(this.dirtyMap) + if (result !== undefined) { + Object.assign(this.dirtyMap, result) + } + } + if (this.dirtyNum > 0 && !silent) { + if (this.batching) return + this.notify(this.getState()) + this.dirtyMap = {} + this.dirtyNum = 0 + } + } + } + } + + hasChanged = (key?: string) => + key ? this.dirtyMap[key] === true : this.dirtyNum > 0 + + getChanged = () => { + if (!hasProxy || this.props.useDirty) { + return clone(this.dirtyMap) + } else { + return this.dirtyMap + } + } + + watch = (key: string, callback?: (dirtys: StateDirtyMap) => any) => { + if (this.hasChanged(key)) { + if (isFn(callback)) { + callback(this.getChanged()) + } + } + } + } +} diff --git a/packages/core/src/shared/subscrible.ts b/packages/core/src/shared/subscrible.ts new file mode 100644 index 00000000000..d1acdde036a --- /dev/null +++ b/packages/core/src/shared/subscrible.ts @@ -0,0 +1,29 @@ +import { isFn, each } from '@uform/shared' +import { Subscriber } from '../types' + +export class Subscrible { + subscribers: Subscriber[] = [] + + subscribe = (callback?: Subscriber) => { + if ( + isFn(callback) && + !this.subscribers.some(fn => fn.toString() === callback.toString()) + ) { + this.subscribers.push(callback) + } + } + + unsubscribe = (callback?: Subscriber) => { + if (isFn(callback)) { + this.subscribers = this.subscribers.filter(fn => { + return fn.toString() !== callback.toString() + }) + } else { + this.subscribers.length = 0 + } + } + + notify = (payload?: Payload) => { + each(this.subscribers, callback => callback(payload)) + } +} diff --git a/packages/core/src/state/field.ts b/packages/core/src/state/field.ts new file mode 100644 index 00000000000..2e4f73ccf64 --- /dev/null +++ b/packages/core/src/state/field.ts @@ -0,0 +1,187 @@ +import { createStateModel } from '../shared/model' +import { clone, toArr, isValid, isEqual, FormPath, isFn } from '@uform/shared' +import { IFieldState, IFieldStateProps } from '../types' +/** + * 核心数据结构,描述表单字段的所有状态 + */ +export const FieldState = createStateModel( + class FieldState { + static displayName = 'FieldState' + static defaultState = { + name: '', + path: '', + initialized: false, + pristine: true, + valid: true, + modified: false, + touched: false, + active: false, + visited: false, + invalid: false, + visible: true, + display: true, + loading: false, + validating: false, + errors: [], + values: [], + ruleErrors: [], + ruleWarnings: [], + effectErrors: [], + warnings: [], + effectWarnings: [], + editable: true, + selfEditable: undefined, + formEditable: undefined, + value: undefined, + initialValue: undefined, + rules: [], + required: false, + mounted: false, + unmounted: false, + props: {} + } + + static defaultProps = { + path: '' + } + + private state: IFieldState + + private nodePath: FormPath + + private dataPath: FormPath + + constructor(state: IFieldState, props: IFieldStateProps) { + this.state = state + this.nodePath = FormPath.getPath(props.nodePath) + this.dataPath = FormPath.getPath(props.dataPath) + this.state.name = this.dataPath.entire + this.state.path = this.nodePath.entire + } + + readValues({ value, values }: IFieldStateProps) { + if (isValid(values)) { + values = toArr(values) + values[0] = value + } else { + if (isValid(value)) { + values = toArr(value) + } + } + return { + value, + values: toArr(values) + } + } + + readRules({ rules, required }: IFieldStateProps) { + let newRules = isValid(rules) ? clone(toArr(rules)) : this.state.rules + if (isValid(required)) { + if (required) { + if (!newRules.some(rule => rule && rule.required)) { + newRules.push({ required: true }) + } + } else { + newRules = newRules.filter(rule => rule && !rule.required) + } + } else { + required = newRules.some(rule => rule && rule.required) + } + return { + rules: newRules, + required + } + } + + computeState(draft: IFieldState, prevState: IFieldState) { + //如果是隐藏状态,则禁止修改值 + if (!draft.visible || draft.unmounted) { + draft.value = prevState.value + draft.initialValue = prevState.initialValue + } + //操作重定向 + if (!isEqual(draft.errors, prevState.errors)) { + draft.effectErrors = draft.errors + } + if (!isEqual(draft.warnings, prevState.warnings)) { + draft.effectWarnings = draft.warnings + } + //容错逻辑 + draft.rules = toArr(draft.rules).filter(v => !!v) + draft.effectWarnings = toArr(draft.effectWarnings).filter(v => !!v) + draft.effectErrors = toArr(draft.effectErrors).filter(v => !!v) + draft.ruleWarnings = toArr(draft.ruleWarnings).filter(v => !!v) + draft.ruleErrors = toArr(draft.ruleErrors).filter(v => !!v) + + draft.errors = draft.ruleErrors.concat(draft.effectErrors) + draft.warnings = draft.ruleWarnings.concat(draft.effectWarnings) + + if (!isEqual(draft.editable, prevState.editable)) { + draft.selfEditable = draft.editable + } + draft.editable = isValid(draft.selfEditable) + ? draft.selfEditable + : isValid(draft.formEditable) + ? isFn(draft.formEditable) + ? draft.formEditable(draft.name) + : draft.formEditable + : true + + const { value, values } = this.readValues(draft) + draft.value = value + draft.values = values + if ( + draft.initialized && + prevState.initialized && + !isEqual(draft.value, prevState.value) + ) { + draft.modified = true + } + if (isEqual(draft.value, draft.initialValue)) { + draft.pristine = true + } else { + draft.pristine = false + } + if (!isValid(draft.props)) { + draft.props = prevState.props + } + + if (draft.validating === true) { + draft.loading = true + } else if (draft.validating === false) { + draft.loading = false + } + // 以下几种情况清理错误和警告信息 + // 1. 字段设置为不可编辑 + // 2. 字段隐藏 + // 3. 字段被卸载 + if ( + (draft.selfEditable !== prevState.selfEditable && + !draft.selfEditable) || + draft.visible === false || + draft.unmounted === true + ) { + draft.errors = [] + draft.effectErrors = [] + draft.warnings = [] + draft.effectWarnings = [] + } + if (draft.mounted === true) { + draft.unmounted = false + } + if (draft.unmounted === true) { + draft.mounted = false + } + if (draft.errors.length) { + draft.invalid = true + draft.valid = false + } else { + draft.invalid = false + draft.valid = true + } + const { rules, required } = this.readRules(draft) + draft.rules = rules + draft.required = required + } + } +) diff --git a/packages/core/src/state/form.ts b/packages/core/src/state/form.ts new file mode 100644 index 00000000000..cfc05ade446 --- /dev/null +++ b/packages/core/src/state/form.ts @@ -0,0 +1,68 @@ +import { createStateModel } from '../shared/model' +import { toArr, clone, isEqual } from '@uform/shared' +import { IFormState, IFormStateProps } from '../types' +/** + * 核心数据结构,描述Form级别状态 + */ +export const FormState = createStateModel( + class FormState { + static displayName = 'FormState' + static defaultState = { + pristine: true, + valid: true, + invalid: false, + loading: false, + validating: false, + initialized: false, + submitting: false, + editable: true, + errors: [], + warnings: [], + values: {}, + initialValues: {}, + mounted: false, + unmounted: false, + props: {} + } + + static defaultProps = { + lifecycles: [] + } + + private state: IFormState + constructor(state: IFormState, props: IFormStateProps) { + this.state = state + this.state.initialValues = clone(props.initialValues || {}) + this.state.values = clone(props.values || props.initialValues || {}) + this.state.editable = props.editable + } + + computeState(draft: IFormState, prevState: IFormState) { + draft.errors = toArr(draft.errors).filter(v => !!v) + draft.warnings = toArr(draft.warnings).filter(v => !!v) + if (draft.errors.length) { + draft.invalid = true + draft.valid = false + } else { + draft.invalid = false + draft.valid = true + } + if (isEqual(draft.values, draft.initialValues)) { + draft.pristine = true + } else { + draft.pristine = false + } + if (draft.validating === true) { + draft.loading = true + } else if (draft.validating === false) { + draft.loading = false + } + if (draft.mounted === true) { + draft.unmounted = false + } + if (draft.unmounted === true) { + draft.mounted = false + } + } + } +) diff --git a/packages/core/src/state/virtual-field.ts b/packages/core/src/state/virtual-field.ts new file mode 100644 index 00000000000..0f7028bc52b --- /dev/null +++ b/packages/core/src/state/virtual-field.ts @@ -0,0 +1,56 @@ +import { createStateModel } from '../shared/model' +import { FormPath, isValid } from '@uform/shared' +import { IVirtualFieldState, IVirtualFieldStateProps } from '../types' + +/** + * UForm特有,描述一个虚拟字段, + * 它不占用数据空间,但是它拥有状态, + * 可以联动控制Field或者VirtualField的状态 + * 类似于现在UForm的Card之类的容器布局组件 + */ +export const VirtualFieldState = createStateModel< + IVirtualFieldState, + IVirtualFieldStateProps +>( + class VirtualFieldState { + static displayName = 'VirtualFieldState' + static defaultState = { + name: '', + path: '', + initialized: false, + visible: true, + display: true, + mounted: false, + unmounted: false, + props: {} + } + + static defaultProps = { + path: '', + props: {} + } + + private state: IVirtualFieldState + + private path: FormPath + + constructor(state: IVirtualFieldState, props: IVirtualFieldStateProps) { + this.state = state + this.path = FormPath.getPath(props.nodePath) + this.state.name = this.path.entire + this.state.path = this.path.entire + } + + computeState(draft: IVirtualFieldState, prevState: IVirtualFieldState) { + if (draft.mounted === true) { + draft.unmounted = false + } + if (!isValid(draft.props)) { + draft.props = prevState.props + } + if (draft.unmounted === true) { + draft.mounted = false + } + } + } +) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts new file mode 100644 index 00000000000..c1d00804800 --- /dev/null +++ b/packages/core/src/types.ts @@ -0,0 +1,319 @@ +import { FormPath, FormPathPattern, isFn } from '@uform/shared' +import { ValidatePatternRules, ValidateNodeResult } from '@uform/validator' +import { FormLifeCycle } from './shared/lifecycle' +import { Draft } from 'immer' +import { Subscrible } from './shared/subscrible' + +export type FormLifeCycleHandler = (payload: T, context: any) => void + +export type FormHeartSubscriber = ({ + type, + payload +}: { + type: string + payload: any +}) => void + +export enum LifeCycleTypes { + /** + * Form LifeCycle + **/ + + ON_FORM_WILL_INIT = 'onFormWillInit', + ON_FORM_INIT = 'onFormInit', + ON_FORM_CHANGE = 'onFormChange', //ChangeType精准控制响应 + ON_FORM_MOUNT = 'onFormMount', + ON_FORM_UNMOUNT = 'onFormUnmount', + ON_FORM_SUBMIT = 'onFormSubmit', + ON_FORM_RESET = 'onFormReset', + ON_FORM_SUBMIT_START = 'onFormSubmitStart', + ON_FORM_SUBMIT_END = 'onFormSubmitEnd', + ON_FORM_VALUES_CHANGE = 'onFormValuesChange', + ON_FORM_INITIAL_VALUES_CHANGE = 'onFormInitialValueChange', + ON_FORM_VALIDATE_START = 'onFormValidateStart', + ON_FORM_VALIDATE_END = 'onFormValidateEnd', + ON_FORM_INPUT_CHANGE = 'onFormInputChange', + /** + * FormGraph LifeCycle + **/ + ON_FORM_GRAPH_CHANGE = 'onFormGraphChange', + + /** + * Field LifeCycle + **/ + + ON_FIELD_WILL_INIT = 'onFieldWillInit', + ON_FIELD_INIT = 'onFieldInit', + ON_FIELD_CHANGE = 'onFieldChange', + ON_FIELD_INPUT_CHANGE = 'onFieldInputChange', + ON_FIELD_VALUE_CHANGE = 'onFieldValueChange', + ON_FIELD_INITIAL_VALUE_CHANGE = 'onFieldInitialValueChange', + ON_FIELD_MOUNT = 'onFieldMount', + ON_FIELD_UNMOUNT = 'onFieldUnmount' +} + +export type FormGraphNodeMap = { + [key in string]: T +} + +export interface FormGraphVisitorOptions { + path: FormPath + exsist: boolean + append: (node: T) => void +} + +export type FormGraph = ( + node: T, + options: FormGraphVisitorOptions +) => void + +export interface FormGraphNodeRef { + parent?: FormGraphNodeRef + path: FormPath + children: FormPath[] +} + +export type FormGraphMatcher = (node: T, path: FormPath) => void | boolean + +export type FormGraphEacher = (node: T, path: FormPath) => void + +export type FormLifeCyclePayload = ( + params: { + type: string + payload: T + }, + context: any +) => void + +export type StateDirtyMap

    = { + [key in keyof P]?: boolean +} + +export interface StateModel

    { + publishState?: (state: P) => P + dirtyCheck?: (dirtys: StateDirtyMap

    ) => StateDirtyMap

    | void + computeState?: (state: Draft

    , preState?: P) => Draft

    | void +} + +export interface IStateModelFactory { + new (state: S, props: P): StateModel + defaultState?: S + defaultProps?: P + displayName?: string +} + +export interface IFieldState { + displayName?: string + name: string + path: string + initialized: boolean + pristine: boolean + valid: boolean + touched: boolean + invalid: boolean + visible: boolean + display: boolean + editable: boolean + selfEditable: boolean + formEditable: boolean | ((name: string) => boolean) + loading: boolean + modified: boolean + active: boolean + visited: boolean + validating: boolean + values: any[] + errors: string[] + effectErrors: string[] + ruleErrors: string[] + warnings: string[] + effectWarnings: string[] + ruleWarnings: string[] + value: any + initialValue: any + rules: ValidatePatternRules[] + required: boolean + mounted: boolean + unmounted: boolean + props: {} +} +export type FieldStateDirtyMap = StateDirtyMap + +export interface IFieldStateProps { + path?: FormPathPattern + nodePath?: FormPathPattern + dataPath?: FormPathPattern + name?: string + value?: any + values?: any[] + initialValue?: any + props?: {} + rules?: ValidatePatternRules[] + required?: boolean + editable?: boolean + onChange?: (fieldState: IField) => void +} + +export const isField = (target: any): target is IField => + target && target.displayName === 'FieldState' + +export const isFieldState = (target: any): target is IFieldState => + target && target.displayName === 'FieldState' + +export const isVirtualField = (target: any): target is IVirtualField => + target && target.displayName === 'VirtualFieldState' + +export const isVirtualFieldState = ( + target: any +): target is IVirtualFieldState => + target && target.displayName === 'VirtualFieldState' + +export const isStateModel = (target: any): target is IModel => + target && isFn(target.getState) + +export interface IFormState { + pristine: boolean + valid: boolean + invalid: boolean + loading: boolean + validating: boolean + submitting: boolean + initialized: boolean + editable: boolean | ((name: string) => boolean) + errors: string[] + warnings: string[] + values: {} + initialValues: {} + mounted: boolean + unmounted: boolean + props: {} +} + +export type FormStateDirtyMap = StateDirtyMap + +export interface IFormStateProps { + initialValues?: {} + values?: {} + lifecycles?: FormLifeCycle[] + editable?: boolean | ((name: string) => boolean) +} + +export interface IFormCreatorOptions extends IFormStateProps { + useDirty?: boolean + validateFirst?: boolean + editable?: boolean + onSubmit?: (values: IFormState['values']) => any | Promise + onReset?: () => void + onValidateFailed?: (validated: IFormValidateResult) => void +} + +export interface IVirtualFieldState { + name: string + path: string + displayName?: string + initialized: boolean + visible: boolean + display: boolean + mounted: boolean + unmounted: boolean + props: {} +} +export type VirtualFieldStateDirtyMap = StateDirtyMap + +export interface IVirtualFieldStateProps { + path?: FormPathPattern + nodePath?: FormPathPattern + name?: string + props?: {} + onChange?: (fieldState: IVirtualField) => void +} + +export type IFormValidateResult = ValidateNodeResult + +export interface IFormSubmitResult { + validated: IFormValidateResult + payload: any +} + +export interface IFormResetOptions { + forceClear?: boolean + validate?: boolean +} + +export interface IFormGraph { + [path: string]: IFormState | IFieldState | IVirtualFieldState +} + +export interface IMutators { + change(...values: any[]): any + focus(): void + blur(): void + push(value: any): any[] + pop(): any[] + insert(index: number, value: any): any[] + remove(index: number | string): any + unshift(value: any): any[] + shift(): any[] + move($from: number, $to: number): any + moveDown(index: number): any + moveUp(index: number): any + validate(): Promise + exist(index?: number | string): boolean +} + +export type Subscriber = (payload: S) => void + +export interface IModel extends Subscrible { + state: S + props: P + displayName?: string + dirtyNum: number + dirtyMap: StateDirtyMap + subscribers: Subscriber[] + batching: boolean + controller: StateModel + batch: (callback?: () => void) => void + getState: (callback?: (state: S) => any) => any + setState: (callback?: (state: S | Draft) => void, silent?: boolean) => void + unsafe_getSourceState: (callback?: (state: S) => any) => any + unsafe_setSourceState: (callback?: (state: S) => void) => void + hasChanged: (key?: string) => boolean + getChanged: () => StateDirtyMap +} + +export type IField = IModel + +export type IVirtualField = IModel + +export type IFormInternal = IModel + +export interface IForm { + submit( + onSubmit?: (values: IFormState['values']) => any | Promise + ): Promise + clearErrors: (pattern?: FormPathPattern) => void + reset(options?: IFormResetOptions): Promise + validate(path?: FormPathPattern, options?: {}): Promise + setFormState(callback?: (state: IFormState) => any): void + getFormState(callback?: (state: IFormState) => any): any + setFieldState( + path: FormPathPattern, + callback?: (state: IFieldState) => void + ): void + getFieldState( + path: FormPathPattern, + callback?: (state: IFieldState) => any + ): any + unsafe_do_not_use_transform_data_path(path: FormPathPattern): FormPathPattern //eslint-disable-line + registerField(props: IFieldStateProps): IField + registerVirtualField(props: IVirtualFieldStateProps): IVirtualField + createMutators(field: IField): IMutators + getFormGraph(): IFormGraph + setFormGraph(graph: IFormGraph): void + subscribe(callback?: FormHeartSubscriber): void + unsubscribe(callback?: FormHeartSubscriber): void + notify: (type: string, payload?: T) => void + setFieldValue(path?: FormPathPattern, value?: any): void + getFieldValue(path?: FormPathPattern): any + setFieldInitialValue(path?: FormPathPattern, value?: any): void + getFieldInitialValue(path?: FormPathPattern): any +} diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts deleted file mode 100644 index 837927628d7..00000000000 --- a/packages/core/src/utils.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { Path, IFormPathMatcher } from '@uform/types' -import { - isArr, - isStr, - getPathSegments, - isEqual, - toArr, - clone, - isFn, - globalThisPolyfill -} from '@uform/utils' - -export * from '@uform/utils' - -const self = globalThisPolyfill - -const compactScheduler = ([raf, caf, priority], fresh: boolean) => { - return [fresh ? callback => raf(priority, callback) : raf, caf] -} - -const getScheduler = () => { - if (!self.requestAnimationFrame) { - return [self.setTimeout, self.clearTimeout] - } - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const scheduler = require('scheduler') as any - return compactScheduler( - [ - scheduler.scheduleCallback || scheduler.unstable_scheduleCallback, - scheduler.cancelCallback || scheduler.unstable_cancelCallback, - scheduler.NormalPriority || scheduler.unstable_NormalPriority - ], - !!scheduler.unstable_requestPaint - ) - } catch (err) { - return [self.requestAnimationFrame, self.cancelAnimationFrame] - } -} - -export const [raf, caf] = getScheduler() - -export const resolveFieldPath = (path: Path | IFormPathMatcher): string[] => { - if (!isArr(path)) { - return isStr(path) ? resolveFieldPath(getPathSegments(path)) : undefined - } - return path.reduce((buf, key) => { - return buf.concat(getPathSegments(key)) - }, []) -} - -export const isChildField = (field, parent) => { - if (field && parent && field.path && parent.path) { - for (let i = 0; i < parent.path.length; i++) { - if (field.path[i] !== parent.path[i]) { - return false - } - } - return parent.path.length < field.path.length - } - return false -} - -export const hasRequired = rules => { - return toArr(rules).some(rule => { - return rule && rule.required - }) -} - -export const publishFormState = state => { - const { - values, - valid, - invalid, - initialValues, - errors, - pristine, - dirty - } = state - return { - values: clone(values), - valid, - invalid, - errors, - pristine, - dirty, - initialValues: clone(initialValues) - } -} - -export const publishFieldState = state => { - const { - value, - valid, - invalid, - errors, - visible, - display, - editable, - initialValue, - name, - path, - props, - effectErrors, - loading, - pristine, - required, - rules - } = state - return { - value: clone(value), - valid, - invalid, - editable, - visible, - display, - loading, - errors: errors.concat(effectErrors), - pristine, - initialValue: clone(initialValue), - name, - path, - props, - required, - rules - } -} - -export class BufferList { - public data = [] - public indexes = {} - public push(key: string, value: any, extra: any) { - if (!this.indexes[key]) { - const index = this.data.length - this.data.push({ ...extra, key, values: [value] }) - this.indexes[key] = index - } else { - const item = this.data[this.indexes[key]] - if (!item.values.some(callback => isEqual(callback, value))) { - item.values.push(value) - } - } - } - - public forEach(callback) { - for (let i = 0; i < this.data.length; i++) { - if (isFn(callback)) { - callback(this.data[i], this.data[i].key) - } - } - } - - public remove(key: string, value?: any) { - this.data = this.data.reduce((buf, item, index) => { - if (item.key === key) { - delete this.indexes[key] - return buf - } else { - this.indexes[key] = buf.length - return buf.concat(item) - } - }, []) - } -} diff --git a/packages/next/README.md b/packages/next/README.md index 3eb267bdc52..f7b7d78e396 100644 --- a/packages/next/README.md +++ b/packages/next/README.md @@ -1,2 +1,77 @@ # @uform/next -> UForm Fusion Next组件插件包 \ No newline at end of file + +> UForm Fusion Next 组件插件包 + +```jsx +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + FormEffectHooks, + createFormActions, + FormGridRow, + FormItemGrid, + FormGridCol, + FormPath, + FormLayout, + FormBlock, + FormCard, + FormTextBox, + FormStep +} from './src/index' +import { Button } from '@alifd/next' +import '@alifd/next/dist/next.css' + +const { onFormInit$ } = FormEffectHooks + +const actions = createFormActions() + +export default () => ( + { + console.log('提交') + console.log(values) + }} + actions={actions} + labelCol={{ span: 8 }} + wrapperCol={{ span: 6 }} + validateFirst + effects={({ setFieldState, getFormGraph }) => { + onFormInit$().subscribe(() => { + setFieldState('col1', state => { + state.visible = false + }) + }) + FormStep.effects(['step-1', 'step-2', 'step-3']) + }} + > + + + + + + + + + + + + 提交 + + + + +) +``` diff --git a/packages/next/build.ts b/packages/next/build.ts index 9ee3d0d8514..4fa9dc8329f 100644 --- a/packages/next/build.ts +++ b/packages/next/build.ts @@ -1,19 +1,19 @@ import { compile, getCompileConfig } from '../../scripts/build' import ts from 'typescript' -import tsImportPluginFactory from 'ts-import-plugin' +//import tsImportPluginFactory from 'ts-import-plugin' import glob from 'glob' -const transformer = tsImportPluginFactory({ - libraryName: '@alifd/next', - //style: importPath => `${importPath}/style`, -}) +// const transformer = tsImportPluginFactory({ +// libraryName: '@alifd/next', +// style: importPath => `${importPath}/style`, +// }) function buildESM() { const { fileNames, options } = getCompileConfig(require.resolve('./tsconfig.json'), { outDir: './esm', module: ts.ModuleKind.ESNext }) - compile(fileNames, options, { before: [transformer] }) + compile(fileNames, options) console.log('esm build successfully') } diff --git a/packages/next/package.json b/packages/next/package.json index 1309dc647e8..e40e33414ae 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@uform/next", - "version": "0.4.3", + "version": "0.4.0", "license": "MIT", "main": "lib", "module": "esm", @@ -21,24 +21,31 @@ }, "peerDependencies": { "@alifd/next": "^1.13.1", + "@types/classnames": "^2.2.9", + "@types/styled-components": "^4.1.19", "@babel/runtime": "^7.4.4", "react": ">=16.8.0", "react-dom": ">=16.8.0" }, "dependencies": { - "@uform/react": "^0.4.3", - "@uform/types": "^0.4.3", - "@uform/utils": "^0.4.3", + "@uform/react-schema-renderer": "^0.4.0", + "@uform/react-shared-components":"^0.4.0", + "@uform/types": "^0.4.0", + "@uform/shared": "^0.4.0", "classnames": "^2.2.6", "moveto": "^1.7.4", "react-stikky": "^0.1.15", - "styled-components": "^4.1.1" + "styled-components": "^4.1.1", + "react-eva": "^1.0.0", + "rxjs": "^6.5.1" }, "devDependencies": { - "@alifd/next": "^1.13.1" + "@alifd/next": "^1.13.1", + "@types/classnames": "^2.2.9", + "@types/styled-components": "^4.1.19" }, "publishConfig": { "access": "public" }, "gitHead": "4d068dad6183e8da294a4c899a158326c0b0b050" -} +} \ No newline at end of file diff --git a/packages/next/src/compat/Form.tsx b/packages/next/src/compat/Form.tsx new file mode 100644 index 00000000000..3ea7255f778 --- /dev/null +++ b/packages/next/src/compat/Form.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { Form } from '@alifd/next' +import { FormProps } from '@alifd/next/types/form' +import { IFormItemTopProps } from '../types' +import { FormItemProvider } from './context' +import { normalizeCol } from '../shared' + +export const CompatNextForm: React.FC< + FormProps & IFormItemTopProps +> = props => { + return ( + + + + ) +} diff --git a/packages/next/src/compat/FormItem.tsx b/packages/next/src/compat/FormItem.tsx new file mode 100644 index 00000000000..db71dc8d82a --- /dev/null +++ b/packages/next/src/compat/FormItem.tsx @@ -0,0 +1,109 @@ +import React, { createContext } from 'react' +import { Form } from '@alifd/next' +import { useFormItem } from './context' +import { IFormItemTopProps, ICompatItemProps } from '../types' +import { normalizeCol } from '../shared' +import { useContext } from 'react' + +const computeStatus = (props: ICompatItemProps) => { + if (props.loading) { + return 'loading' + } + if (props.invalid) { + return 'error' + } + //todo:暂时不支持 + // if (props.warnings.length) { + // return 'warning' + // } +} + +const computeHelp = (props: ICompatItemProps) => { + if (props.help) return props.help + const messages = [].concat(props.errors || [], props.warnings || []) + return messages.length ? messages : props.schema && props.schema.description +} + +const computeLabel = (props: ICompatItemProps) => { + if (props.label) return props.label + if (props.schema && props.schema.title) { + return props.schema.title + } +} + +const computeExtra = (props: ICompatItemProps) => { + if (props.extra) return props.extra +} + +function pickProps(obj: T, ...keys: (keyof T)[]): Pick { + const result: Pick = {} as any + for (let i = 0; i < keys.length; i++) { + if (obj[keys[i]] !== undefined) { + result[keys[i]] = obj[keys[i]] + } + } + return result +} + +const computeSchemaExtendProps = ( + props: ICompatItemProps +): IFormItemTopProps => { + if (props.schema) { + return pickProps( + { + ...props.schema.getExtendsItemProps(), + ...props.schema.getExtendsProps() + }, + 'prefix', + 'labelAlign', + 'labelTextAlign', + 'size', + 'labelCol', + 'wrapperCol' + ) + } +} + +const FormItemPropsContext = createContext({}) + +export const FormItemProps = ({ children, ...props }) => ( + + {children} + +) + +export const CompatNextFormItem: React.FC = props => { + const { + prefix, + labelAlign, + labelCol, + labelTextAlign, + wrapperCol, + size + } = useFormItem() + const formItemProps = useContext(FormItemPropsContext) + const help = computeHelp(props) + const label = computeLabel(props) + const status = computeStatus(props) + const extra = computeExtra(props) + const itemProps = computeSchemaExtendProps(props) + return ( + {extra}

    } + {...itemProps} + {...formItemProps} + > + {props.children} + + ) +} diff --git a/packages/next/src/compat/context.tsx b/packages/next/src/compat/context.tsx new file mode 100644 index 00000000000..e3f38383c73 --- /dev/null +++ b/packages/next/src/compat/context.tsx @@ -0,0 +1,35 @@ +import React, { createContext, useContext } from 'react' +import { IFormItemTopProps } from '../types' + +const FormItemContext = createContext({}) + +export const FormItemProvider: React.FC = ({ + children, + prefix, + size, + labelAlign, + labelCol, + inline, + labelTextAlign, + wrapperCol +}) => ( + + {children} + +) + +FormItemProvider.displayName = 'FormItemProvider' + +export const useFormItem = () => { + return useContext(FormItemContext) +} diff --git a/packages/next/src/compat/index.ts b/packages/next/src/compat/index.ts new file mode 100644 index 00000000000..1eca2fb65ed --- /dev/null +++ b/packages/next/src/compat/index.ts @@ -0,0 +1,10 @@ +import { + registerFormComponent, + registerFormItemComponent +} from '@uform/react-schema-renderer' +import { CompatNextForm } from './Form' +import { CompatNextFormItem } from './FormItem' + +registerFormComponent(CompatNextForm) + +registerFormItemComponent(CompatNextFormItem) diff --git a/packages/next/src/components/FormBlock.tsx b/packages/next/src/components/FormBlock.tsx new file mode 100644 index 00000000000..6468e5b03c6 --- /dev/null +++ b/packages/next/src/components/FormBlock.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import { createVirtualBox } from '@uform/react-schema-renderer' +import { Card } from '@alifd/next' +import { CardProps } from '@alifd/next/types/card' +import styled from 'styled-components' + +export const FormBlock = createVirtualBox( + 'block', + styled(({ children, className, ...props }) => { + return ( + + {children} + + ) + })` + margin-bottom: 0px; + .next-card-body { + padding-top: 20px; + padding-bottom: 0 !important; + } + &.next-card { + border: none; + padding-bottom: 15px; + } + ` +) diff --git a/packages/next/src/components/FormCard.tsx b/packages/next/src/components/FormCard.tsx new file mode 100644 index 00000000000..71cb0fbe68d --- /dev/null +++ b/packages/next/src/components/FormCard.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import { createVirtualBox } from '@uform/react-schema-renderer' +import { Card } from '@alifd/next' +import { CardProps } from '@alifd/next/types/card' +import styled from 'styled-components' + +export const FormCard = createVirtualBox( + 'card', + styled(({ children, className, ...props }) => { + return ( + + {children} + + ) + })` + margin-bottom: 30px; + .next-card-body { + padding-top: 30px; + padding-bottom: 0 !important; + } + ` +) diff --git a/packages/next/src/components/FormItemGrid.tsx b/packages/next/src/components/FormItemGrid.tsx new file mode 100644 index 00000000000..fbe30387150 --- /dev/null +++ b/packages/next/src/components/FormItemGrid.tsx @@ -0,0 +1,89 @@ +import React, { Fragment } from 'react' +import { CompatNextFormItem } from '../compat/FormItem' +import { createVirtualBox } from '@uform/react-schema-renderer' +import { toArr } from '@uform/shared' +import { Grid } from '@alifd/next' +import { RowProps, ColProps } from '@alifd/next/types/grid' +import { ItemProps } from '@alifd/next/types/form' +import { IFormItemGridProps, IItemProps } from '../types' +import { normalizeCol } from '../shared' +const { Row, Col } = Grid + +export const FormItemGrid = createVirtualBox( + 'grid', + props => { + const { + cols: rawCols, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + title, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + description, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + help, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + extra, + ...selfProps + } = props + const children = toArr(props.children) + const cols = toArr(rawCols).map(col => normalizeCol(col)) + const childNum = children.length + + if (cols.length < childNum) { + let offset: number = childNum - cols.length + let lastSpan: number = + 24 - + cols.reduce((buf, col) => { + return ( + buf + + Number(col.span ? col.span : 0) + + Number(col.offset ? col.offset : 0) + ) + }, 0) + for (let i = 0; i < offset; i++) { + cols.push({ span: Math.floor(lastSpan / offset) }) + } + } + const grids = ( + + {children.reduce((buf, child, key) => { + return child + ? buf.concat( + + {child} + + ) + : buf + }, [])} + + ) + + if (title) { + return ( + + {grids} + + ) + } + return {grids} + } +) + +export const FormGridRow = createVirtualBox( + 'grid-row', + props => { + const { title, description, extra } = props + const grids = {props.children} + if (title) { + return ( + + {grids} + + ) + } + return grids + } +) + +export const FormGridCol = createVirtualBox('grid-col', props => { + return {props.children} +}) diff --git a/packages/next/src/components/FormLayout.tsx b/packages/next/src/components/FormLayout.tsx new file mode 100644 index 00000000000..eb89eab6c43 --- /dev/null +++ b/packages/next/src/components/FormLayout.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { FormItemProvider, useFormItem } from '../compat/context' +import { createVirtualBox } from '@uform/react-schema-renderer' +import cls from 'classnames' +import { IFormItemTopProps } from '../types' + +export const FormLayout = createVirtualBox( + 'layout', + props => { + const { inline } = useFormItem() + const isInline = props.inline || inline + const children = + isInline || props.className || props.style ? ( +
    + {props.children} +
    + ) : ( + props.children + ) + return {children} + } +) diff --git a/packages/next/src/components/FormStep.tsx b/packages/next/src/components/FormStep.tsx new file mode 100644 index 00000000000..1de88681f35 --- /dev/null +++ b/packages/next/src/components/FormStep.tsx @@ -0,0 +1,98 @@ +import React, { useState, useMemo, useRef } from 'react' +import { + createControllerBox, + ISchemaVirtualFieldComponentProps, + FormPathPattern, + createEffectHook, + createFormActions +} from '@uform/react-schema-renderer' +import { toArr } from '@uform/shared' +import { Observable } from 'rxjs/internal/Observable' +import { Step } from '@alifd/next' +import { IFormStep } from '../types' + +enum StateMap { + ON_FORM_STEP_NEXT = 'onFormStepNext', + ON_FORM_STEP_PREVIOUS = 'onFormStepPrevious', + ON_FORM_STEP_GO_TO = 'onFormStepGoto', + ON_FORM_STEP_CURRENT_CHANGE = 'onFormStepCurrentChange' +} +const EffectHooks = { + onStepNext$: createEffectHook(StateMap.ON_FORM_STEP_NEXT), + onStepPrevious$: createEffectHook(StateMap.ON_FORM_STEP_PREVIOUS), + onStepGoto$: createEffectHook(StateMap.ON_FORM_STEP_GO_TO), + onStepCurrentChange$: createEffectHook<{ + value: number + preValue: number + }>(StateMap.ON_FORM_STEP_CURRENT_CHANGE) +} + +const effects = (relations: FormPathPattern[]) => { + const actions = createFormActions() + return EffectHooks.onStepCurrentChange$().subscribe(({ value }) => { + relations.forEach((pattern, index) => { + actions.setFieldState(pattern, (state: any) => { + state.display = index === value + }) + }) + }) +} + +type StepComponentExtendsProps = StateMap & { + getEffects: ( + relations: FormPathPattern[] + ) => Observable<{ + value: number + preValue: number + }> +} + +export const FormStep: React.FC & + StepComponentExtendsProps = createControllerBox( + 'step', + ({ props, form }: ISchemaVirtualFieldComponentProps) => { + const [current, setCurrent] = useState(0) + const ref = useRef(current) + const { dataSource, ...stepProps } = props['x-component-props'] || {} + const items = toArr(dataSource) + const update = (cur: number) => { + form.notify(StateMap.ON_FORM_STEP_CURRENT_CHANGE, { + value: cur, + preValue: current + }) + setCurrent(cur) + } + useMemo(() => { + update(ref.current) + form.subscribe(({ type, payload }) => { + switch (type) { + case StateMap.ON_FORM_STEP_NEXT: + update( + ref.current + 1 > items.length - 1 ? ref.current : ref.current + 1 + ) + break + case StateMap.ON_FORM_STEP_PREVIOUS: + update(ref.current - 1 < 0 ? ref.current : ref.current - 1) + break + case StateMap.ON_FORM_STEP_GO_TO: + if (!(payload < 0 || payload > items.length)) { + update(payload) + } + break + } + }) + }, []) + ref.current = current + return ( + + {items.map((props, key) => { + return + })} + + ) + } +) as any + +Object.assign(FormStep, StateMap, EffectHooks, { + effects +}) diff --git a/packages/next/src/components/FormTextBox.tsx b/packages/next/src/components/FormTextBox.tsx new file mode 100644 index 00000000000..45cf18233ee --- /dev/null +++ b/packages/next/src/components/FormTextBox.tsx @@ -0,0 +1,104 @@ +import React, { useRef, useEffect } from 'react' +import { createControllerBox } from '@uform/react-schema-renderer' +import { IFormTextBox } from '../types' +import { toArr } from '@uform/shared' +import { CompatNextFormItem } from '../compat/FormItem' +import styled from 'styled-components' + +export const FormTextBox = createControllerBox( + 'text-box', + styled(({ props, className, children }) => { + const { + title, + help, + text, + name, + extra, + gutter, + style, + ...componentProps + } = Object.assign( + { + gutter: 5 + }, + props['x-component-props'] + ) + const ref: React.RefObject = useRef() + const arrChildren = toArr(children) + const split = text.split('%s') + let index = 0 + useEffect(() => { + if (ref.current) { + const eles = ref.current.querySelectorAll('.text-box-field') + eles.forEach((el: HTMLElement) => { + const ctrl = el.querySelector('.next-form-item-control:first-child') + if (ctrl) { + el.style.width = getComputedStyle(ctrl).width + } + }) + } + }, []) + const newChildren = split.reduce((buf, item, key) => { + return buf.concat( + item ? ( +

    + {item} +

    + ) : null, + arrChildren[key] ? ( +
    + {arrChildren[key]} +
    + ) : null + ) + }, []) + + const textChildren = ( +
    + {newChildren} +
    + ) + + if (!title) return textChildren + + return ( + + {textChildren} + + ) + })` + display: flex; + .text-box-words:nth-child(1) { + margin-left: 0; + } + .text-box-field { + display: inline-block; + } + .next-form-item { + margin-bottom: 0 !important; + } + .preview-text { + text-align: center !important; + } + ` +) diff --git a/packages/next/src/components/button.tsx b/packages/next/src/components/button.tsx index 7cbc4eb1d69..9a3b67750b4 100644 --- a/packages/next/src/components/button.tsx +++ b/packages/next/src/components/button.tsx @@ -1,25 +1,77 @@ import React from 'react' -import { FormConsumer } from '@uform/react' +import { FormSpy, LifeCycleTypes } from '@uform/react-schema-renderer' import { Button } from '@alifd/next' -import { ISubmitProps } from '../type' +import { ButtonProps } from '@alifd/next/types/button' +import { ISubmitProps, IResetProps } from '../types' +import styled from 'styled-components' -export const Submit = ({ showLoading, ...props }: ISubmitProps) => { +export const TextButton: React.FC = props => ( + + ) +})` + border-radius: 50% !important; + padding: 0 !important; + min-width: 28px; + &.next-large { + min-width: 40px; + } + &.next-small { + min-width: 20px; + } + &.has-text { + .next-icon { + margin-right: 5px; + } + background: none !important; + border: none !important; + } +` + +export const Submit = ({ showLoading, onSubmit, ...props }: ISubmitProps) => { return ( - - {({ status }) => { + { + switch (action.type) { + case LifeCycleTypes.ON_FORM_SUBMIT_START: + return { + ...state, + submitting: true + } + case LifeCycleTypes.ON_FORM_SUBMIT_END: + return { + ...state, + submitting: false + } + default: + return state + } + }} + > + {({ state, form }) => { return ( ) }} - + ) } @@ -27,16 +79,24 @@ Submit.defaultProps = { showLoading: true } -export const Reset: React.FC> = props => { +export const Reset: React.FC = ({ + children, + forceClear, + validate, + ...props +}) => { return ( - - {({ reset }) => { + + {({ form }) => { return ( - ) }} - + ) } diff --git a/packages/next/src/components/formButtonGroup.tsx b/packages/next/src/components/formButtonGroup.tsx index 0af76eead94..2faea71c2a4 100644 --- a/packages/next/src/components/formButtonGroup.tsx +++ b/packages/next/src/components/formButtonGroup.tsx @@ -1,12 +1,10 @@ -import React, { Component } from 'react' -import ReactDOM from 'react-dom' +import React, { useRef } from 'react' import { Grid } from '@alifd/next' import Sticky from 'react-stikky' import cls from 'classnames' import styled from 'styled-components' - -import { FormLayoutConsumer } from '../form' -import { IFormButtonGroupProps } from '../type' +import { useFormItem } from '../compat/context' +import { IFormButtonGroupProps } from '../types' const { Row, Col } = Grid @@ -59,17 +57,22 @@ const isElementInViewport = ( ) } -export const FormButtonGroup: React.FC = styled( - class FormButtonGroup extends Component { - static defaultProps = { - span: 24, - zIndex: 100 - } - - private formNode: HTMLElement - - private renderChildren() { - const { children, itemStyle, offset, span } = this.props +export const FormButtonGroup = styled( + (props: React.PropsWithChildren) => { + const { + span, + zIndex, + sticky, + style, + offset, + className, + children, + triggerDistance, + itemStyle + } = props + const { inline } = useFormItem() + const selfRef = useRef() + const renderChildren = () => { return (
    @@ -84,69 +87,53 @@ export const FormButtonGroup: React.FC = styled(
    ) } - - getStickyBoundaryHandler(ref) { + const getStickyBoundaryHandler = () => { return () => { - this.formNode = this.formNode || ReactDOM.findDOMNode(ref.current) - if (this.formNode) { - return isElementInViewport(this.formNode.getBoundingClientRect()) + if (selfRef.current && selfRef.current.parentElement) { + const container = selfRef.current.parentElement + return isElementInViewport(container.getBoundingClientRect()) } return true } } - render() { - const { sticky, style, className } = this.props + const content = ( +
    + {renderChildren()} +
    + ) - const content = ( - - {({ inline } = {}) => ( -
    - {this.renderChildren()} + if (sticky) { + return ( +
    + +
    + {content}
    - )} - +
    +
    ) - - if (sticky) { - return ( -
    - - {({ FormRef } = {}) => { - if (!FormRef) return - return ( - -
    - {content} -
    -
    - ) - }} -
    -
    - ) - } - - return content } + + return content } -)` - ${props => +)` + ${(props: IFormButtonGroupProps) => props.align ? `display:flex;justify-content: ${getAlign(props.align)}` : ''} &.is-inline { display: inline-block; diff --git a/packages/next/src/components/index.ts b/packages/next/src/components/index.ts new file mode 100644 index 00000000000..1015143d9c7 --- /dev/null +++ b/packages/next/src/components/index.ts @@ -0,0 +1,8 @@ +export * from './Button' +export * from './FormButtonGroup' +export * from './FormLayout' +export * from './FormItemGrid' +export * from './FormCard' +export * from './FormBlock' +export * from './FormTextBox' +export * from './FormStep' diff --git a/packages/next/src/components/layout.tsx b/packages/next/src/components/layout.tsx deleted file mode 100644 index 08c19608d79..00000000000 --- a/packages/next/src/components/layout.tsx +++ /dev/null @@ -1,312 +0,0 @@ -import React, { Component, useEffect, useRef } from 'react' -import { createVirtualBox, createControllerBox } from '@uform/react' -import { toArr } from '@uform/utils' -import { Grid } from '@alifd/next' -import Card from '@alifd/next/lib/card' -import styled from 'styled-components' -import cls from 'classnames' -import { IFormItemGridProps, IFormItemProps } from '@uform/types' - -import { FormLayoutConsumer, FormItem, FormLayoutProvider } from '../form' -import { - IFormTextBox, - IFormCardProps, - IFormBlockProps, - IFormLayoutProps, - TFormCardOrFormBlockProps, - IFormItemGridProps as IFormItemGridPropsAlias -} from '../type' - -const { Row, Col } = Grid - -const normalizeCol = ( - col: { span: number; offset?: number } | number, - defaultValue: { span: number } = { span: 0 } -): { span: number; offset?: number } => { - if (!col) { - return defaultValue - } else { - return typeof col === 'object' ? col : { span: col } - } -} - -export const FormLayout = createVirtualBox( - 'layout', - ({ children, ...props }) => { - return ( - - {value => { - let newValue = { ...value, ...props } - let child = - newValue.inline || newValue.className || newValue.style ? ( -
    - {children} -
    - ) : ( - children - ) - return ( - {child} - ) - }} -
    - ) - } -) - -export const FormLayoutItem: React.FC = props => - React.createElement( - FormLayoutConsumer, - {}, - ({ - labelAlign, - labelTextAlign, - labelCol, - wrapperCol, - size, - autoAddColon - }) => { - return React.createElement( - FormItem, - { - labelAlign, - labelTextAlign, - labelCol, - wrapperCol, - autoAddColon, - size, - ...props - }, - props.children - ) - } - ) - -export const FormItemGrid = createVirtualBox( - 'grid', - class extends Component { - renderFormItem(children) { - const { title, help, name, extra, ...props } = this.props - return React.createElement( - FormLayoutItem, - { - label: title, - noMinHeight: true, - id: name, - extra, - help, - ...props - } as IFormItemGridProps, - children - ) - } - - renderGrid() { - const { - children: rawChildren, - cols: rawCols, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - title, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - description, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - help, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - extra, - ...props - } = this.props - - const children = toArr(rawChildren) - const cols = toArr(rawCols).map(col => normalizeCol(col)) - const childNum = children.length - - if (cols.length < childNum) { - let offset: number = childNum - cols.length - let lastSpan: number = - 24 - - cols.reduce((buf, col) => { - return ( - buf + - Number(col.span ? col.span : 0) + - Number(col.offset ? col.offset : 0) - ) - }, 0) - for (let i = 0; i < offset; i++) { - cols.push({ span: Math.floor(lastSpan / offset) }) - } - } - - return ( - - {children.reduce((buf, child, key) => { - return child - ? buf.concat( - - {child} - - ) - : buf - }, [])} - - ) - } - - render() { - const { title } = this.props - if (title) { - return this.renderFormItem(this.renderGrid()) - } else { - return this.renderGrid() - } - } - } -) - -export const FormCard = createVirtualBox( - 'card', - styled( - class extends Component { - static defaultProps = { - contentHeight: 'auto' - } - render() { - const { children, className, ...props } = this.props - return ( - - {children} - - ) - } - } - )` - margin-bottom: 30px; - .next-card-body { - padding-top: 30px; - padding-bottom: 0 !important; - } - ` -) - -export const FormBlock = createVirtualBox( - 'block', - styled( - class extends Component { - static defaultProps = { - contentHeight: 'auto' - } - render() { - const { children, className, ...props } = this.props - return ( - - {children} - - ) - } - } - )` - margin-bottom: 0px; - .next-card-body { - padding-top: 20px; - padding-bottom: 0 !important; - } - &.next-card { - border: none; - padding: 0 15px; - padding-bottom: 15px; - } - ` -) - -export const FormTextBox = createControllerBox( - 'text-box', - styled(({ children, schema, className }) => { - const { title, help, text, name, extra, ...props } = schema['x-props'] - const ref: React.RefObject = useRef() - const arrChildren = toArr(children) - const split = String(text).split('%s') - let index = 0 - useEffect(() => { - if (ref.current) { - const eles = ref.current.querySelectorAll('.text-box-field') - eles.forEach((el: HTMLElement) => { - const ctrl = el.querySelector( - '.next-form-item-control>*:not(.next-form-item-space)' - ) - if (ctrl) { - el.style.width = getComputedStyle(ctrl).width - } - }) - } - }, []) - const newChildren = split.reduce((buf, item, key) => { - return buf.concat( - item ? ( - - {item} - - ) : ( - undefined - ), - arrChildren[key] ? ( -
    - {arrChildren[key]} -
    - ) : ( - undefined - ) - ) - }, []) - - if (!title) - return ( -
    - {newChildren} -
    - ) - - return React.createElement( - FormLayoutItem, - { - label: title, - noMinHeight: true, - id: name, - extra, - help, - ...props - }, -
    - {newChildren} -
    - ) - })` - display: flex; - .text-box-words { - font-size: 12px; - line-height: 28px; - color: #333; - ${props => { - const { editable, schema } = props - const { gutter } = schema['x-props'] - if (!editable) { - return { - margin: 0 - } - } - return { - margin: `0 ${gutter === 0 || gutter ? gutter : 10}px` - } - }} - } - .text-box-words:nth-child(1) { - margin-left: 0; - } - .text-box-field { - display: inline-block; - } - ` -) diff --git a/packages/next/src/fields/array.tsx b/packages/next/src/fields/array.tsx deleted file mode 100644 index 3221814b335..00000000000 --- a/packages/next/src/fields/array.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import React from 'react' -import { registerFormField, createArrayField } from '@uform/react' -import { Button, Icon } from '@alifd/next' -import styled from 'styled-components' - -export const CircleButton = styled['div'].attrs({ className: 'cricle-btn' })` - ${props => - !props.hasText - ? `width:30px; - height:30px;` - : ''} - margin-right:10px; - border-radius: ${props => (!props.hasText ? '100px' : 'none')}; - border: ${props => (!props.hasText ? '1px solid #eee' : 'none')}; - margin-bottom:20px; - cursor:pointer; - display: flex; - align-items: center; - justify-content: center; - line-height: 1.3; - ${props => - !props.hasText - ? `&:hover{ - background:#f7f4f4; - }` - : ''} - .next-icon{ - display:flex; - align-items:'center' - } - .op-name{ - margin-left:3px; - } -} -` - -export const ArrayField = createArrayField({ - CircleButton, - TextButton: props => ( - - ), - AddIcon: () => , - RemoveIcon: () => ( - - ), - MoveDownIcon: () => ( - - ), - MoveUpIcon: () => ( - - ) -}) - -registerFormField( - 'array', - styled( - class extends ArrayField { - render() { - const { className, name, value, renderField } = this.props - const cls = this.getProps('className') - const style = this.getProps('style') - return ( -
    - {value.map((item, index) => { - return ( -
    -
    - {index + 1} -
    -
    {renderField(index)}
    -
    - {this.renderRemove(index, item)} - {this.renderMoveDown(index, item)} - {this.renderMoveUp(index)} -
    -
    - ) - })} - {this.renderEmpty()} - {value.length > 0 && this.renderAddition()} -
    - ) - } - } - )` - border: 1px solid #eee; - min-width: 400px; - .array-item { - padding: 20px; - padding-bottom: 0; - padding-top: 30px; - border-bottom: 1px solid #eee; - position: relative; - &:nth-child(even) { - background: #fafafa; - } - .array-index { - position: absolute; - top: 0; - left: 0; - display: block; - span { - position: absolute; - color: #fff; - z-index: 1; - font-size: 12px; - top: 3px; - left: 3px; - } - &::after { - content: ''; - display: block; - border-top: 20px solid transparent; - border-left: 20px solid transparent; - border-bottom: 20px solid transparent; - border-right: 20px solid #888; - transform: rotate(45deg); - position: absolute; - z-index: 0; - top: -20px; - left: -20px; - } - } - .array-item-operator { - display: flex; - border-top: 1px solid #eee; - padding-top: 20px; - } - } - .array-empty-wrapper { - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - &.disabled { - cursor: default; - } - .array-empty { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - margin: 20px; - img { - display: block; - height: 80px; - } - .next-btn-text { - color: #999; - .next-icon:before { - width: 16px !important; - font-size: 16px !important; - margin-right: 3px; - } - } - } - } - .array-item-wrapper { - margin: 0 -20px; - } - .array-item-addition { - padding: 10px 20px; - background: #fbfbfb; - .next-btn-text { - color: #888; - .next-icon:before { - width: 16px !important; - font-size: 16px !important; - margin-right: 3px; - } - } - } - ` -) diff --git a/packages/next/src/fields/boolean.ts b/packages/next/src/fields/boolean.ts index c742488cbbb..db32fcda84d 100644 --- a/packages/next/src/fields/boolean.ts +++ b/packages/next/src/fields/boolean.ts @@ -1,5 +1,5 @@ -import { connect, registerFormField } from '@uform/react' -import { acceptEnum, mapStyledProps } from '../utils' +import { connect, registerFormField } from '@uform/react-schema-renderer' +import { acceptEnum, mapStyledProps } from '../shared' import { Switch } from '@alifd/next' registerFormField( diff --git a/packages/next/src/fields/cards.tsx b/packages/next/src/fields/cards.tsx index 00b886f03e4..dc62ae1d27d 100644 --- a/packages/next/src/fields/cards.tsx +++ b/packages/next/src/fields/cards.tsx @@ -1,86 +1,131 @@ import React, { Fragment } from 'react' -import { registerFormField } from '@uform/react' -import { toArr } from '@uform/utils' -import { ArrayField } from './array' +import { Icon } from '@alifd/next' +import { + registerFormField, + ISchemaFieldComponentProps, + SchemaField +} from '@uform/react-schema-renderer' +import { toArr, isFn, FormPath } from '@uform/shared' +import { ArrayList } from '@uform/react-shared-components' +import { CircleButton, TextButton } from '../components/Button' import { Card } from '@alifd/next' import styled from 'styled-components' -const FormCardsField = styled( - class extends ArrayField { - renderOperations(item, index) { - return ( - - {this.renderRemove(index, item)} - {this.renderMoveDown(index, item)} - {this.renderMoveUp(index)} - {this.renderExtraOperations(index)} - - ) - } +const ArrayComponents = { + CircleButton, + TextButton, + AdditionIcon: () => , + RemoveIcon: () => ( + + ), + MoveDownIcon: () => ( + + ), + MoveUpIcon: () => ( + + ) +} - renderCardEmpty(title) { - return ( - - {this.renderEmpty()} - - ) +const FormCardsField = styled( + (props: ISchemaFieldComponentProps & { className: string }) => { + const { value, schema, className, editable, path, mutators } = props + const { + renderAddition, + renderRemove, + renderMoveDown, + renderMoveUp, + renderEmpty, + renderExtraOperations, + ...componentProps + } = schema.getExtendsComponentProps() || {} + const onAdd = () => { + const items = Array.isArray(schema.items) + ? schema.items[schema.items.length - 1] + : schema.items + mutators.push(items.getEmptyValue()) } - - render() { - const { value, className, schema, renderField } = this.props - /* eslint-disable @typescript-eslint/no-unused-vars */ - const { - title, - style, - className: cls, - renderAddition, - renderRemove, - renderEmpty, - renderMoveDown, - renderMoveUp, - renderOperations, - ...others - } = this.getProps() || ({} as any) - - /* eslint-enable @typescript-eslint/no-unused-vars */ - return ( -
    + {toArr(value).map((item, index) => { return ( - {index + 1}. {title || schema.title} + {index + 1}. {componentProps.title || schema.title} } - className="card-list" - key={index} - contentHeight="auto" - extra={this.renderOperations(item, index)} + extra={ + + mutators.remove(index)} + /> + mutators.moveDown(index)} + /> + mutators.moveUp(index)} + /> + {isFn(renderExtraOperations) + ? renderExtraOperations(index) + : renderExtraOperations} + + } > - {renderField(index)} + ) })} - {value.length === 0 && this.renderCardEmpty(title)} -
    - {value.length > 0 && this.renderAddition()} -
    -
    - ) - } + + {({ children }) => { + return ( + +
    {children}
    +
    + ) + }} +
    + + {({ children, isEmpty }) => { + if (!isEmpty) { + return ( +
    + {children} +
    + ) + } + }} +
    + +
    + ) } -)` +)` .next-card-body { padding-top: 30px; padding-bottom: 0 !important; @@ -94,42 +139,15 @@ const FormCardsField = styled( display: block; margin-bottom: 0px; background: #fff; - .array-empty-wrapper { - display: flex; - justify-content: center; - cursor: pointer; - margin-bottom: 0px; - &.disabled { - cursor: default; - } - .array-empty { - display: flex; - flex-direction: column; - margin-bottom: 20px; - img { - margin-bottom: 16px; - height: 85px; - } - .next-btn-text { - color: #888; - } - .next-icon:before { - width: 16px !important; - font-size: 16px !important; - margin-right: 5px; - } - } - } .next-card { box-shadow: none; } - .card-list { + .card-list-item { box-shadow: none; border: 1px solid #eee; } - - .array-item-addition { + .array-cards-addition { box-shadow: none; border: 1px solid #eee; transition: all 0.35s ease-in-out; @@ -137,22 +155,41 @@ const FormCardsField = styled( border: 1px solid #ccc; } } + .empty-wrapper { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 20px; + img { + margin-bottom: 16px; + height: 85px; + } + .next-btn-text { + color: #888; + } + .next-icon:before { + width: 16px !important; + font-size: 16px !important; + margin-right: 5px; + } + } } - .next-card.card-list { - margin-top: 20px; + .card-list-empty.card-list-item { + cursor: pointer; } - - .addition-wrapper .array-item-addition { + .next-card.card-list-item { margin-top: 20px; - margin-bottom: 3px; - } - .cricle-btn { - margin-bottom: 0; } + .next-card-extra { display: flex; + button { + margin-right: 8px; + } } - .array-item-addition { + .array-cards-addition { + margin-top: 20px; + margin-bottom: 3px; background: #fff; display: flex; cursor: pointer; @@ -168,9 +205,10 @@ const FormCardsField = styled( margin-right: 5px; } } - .card-list:first-child { + .card-list-item:first-child { margin-top: 0 !important; } ` registerFormField('cards', FormCardsField) +registerFormField('array', FormCardsField) diff --git a/packages/next/src/fields/checkbox.ts b/packages/next/src/fields/checkbox.ts index bdacbe002d6..0a449eca07c 100644 --- a/packages/next/src/fields/checkbox.ts +++ b/packages/next/src/fields/checkbox.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Checkbox } from '@alifd/next' -import { mapStyledProps, mapTextComponent } from '../utils' +import { mapStyledProps, mapTextComponent } from '../shared' const { Group: CheckboxGroup } = Checkbox diff --git a/packages/next/src/fields/date.tsx b/packages/next/src/fields/date.ts similarity index 90% rename from packages/next/src/fields/date.tsx rename to packages/next/src/fields/date.ts index 823ea9796ac..b0f75d94cf8 100644 --- a/packages/next/src/fields/date.tsx +++ b/packages/next/src/fields/date.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { DatePicker } from '@alifd/next' -import { mapStyledProps, mapTextComponent } from '../utils' +import { mapStyledProps, mapTextComponent } from '../shared' const { RangePicker, MonthPicker, YearPicker } = DatePicker diff --git a/packages/next/src/fields/index.ts b/packages/next/src/fields/index.ts new file mode 100644 index 00000000000..59c23266c82 --- /dev/null +++ b/packages/next/src/fields/index.ts @@ -0,0 +1,15 @@ +import './string' +import './number' +import './boolean' +import './date' +import './time' +import './range' +import './upload' +import './checkbox' +import './radio' +import './rating' +import './transfer' +import './cards' +import './table' +import './textarea' +import './password' \ No newline at end of file diff --git a/packages/next/src/fields/number.ts b/packages/next/src/fields/number.ts index a346a1cf4cc..ab126e4e095 100644 --- a/packages/next/src/fields/number.ts +++ b/packages/next/src/fields/number.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { NumberPicker } from '@alifd/next' -import { acceptEnum, mapStyledProps, mapTextComponent } from '../utils' +import { acceptEnum, mapStyledProps, mapTextComponent } from '../shared' registerFormField( 'number', diff --git a/packages/next/src/fields/password.tsx b/packages/next/src/fields/password.tsx index 5664e29b93d..018287bd43b 100644 --- a/packages/next/src/fields/password.tsx +++ b/packages/next/src/fields/password.tsx @@ -1,154 +1,10 @@ import React from 'react' -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Input } from '@alifd/next' import { InputProps } from '@alifd/next/types/input' +import { PasswordStrength } from '@uform/react-shared-components' import styled from 'styled-components' -import { mapStyledProps } from '../utils' - -var isNum = function(c) { - return c >= 48 && c <= 57 -} -var isLower = function(c) { - return c >= 97 && c <= 122 -} -var isUpper = function(c) { - return c >= 65 && c <= 90 -} -var isSymbol = function(c) { - return !(isLower(c) || isUpper(c) || isNum(c)) -} -var isLetter = function(c) { - return isLower(c) || isUpper(c) -} - -const getStrength = val => { - if (!val) return 0 - let num = 0 - let lower = 0 - let upper = 0 - let symbol = 0 - let MNS = 0 - let rep = 0 - let repC = 0 - let consecutive = 0 - let sequential = 0 - const len = () => num + lower + upper + symbol - const callme = () => { - var re = num > 0 ? 1 : 0 - re += lower > 0 ? 1 : 0 - re += upper > 0 ? 1 : 0 - re += symbol > 0 ? 1 : 0 - if (re > 2 && len() >= 8) { - return re + 1 - } else { - return 0 - } - } - for (var i = 0; i < val.length; i++) { - var c = val.charCodeAt(i) - if (isNum(c)) { - num++ - if (i !== 0 && i !== val.length - 1) { - MNS++ - } - if (i > 0 && isNum(val.charCodeAt(i - 1))) { - consecutive++ - } - } else if (isLower(c)) { - lower++ - if (i > 0 && isLower(val.charCodeAt(i - 1))) { - consecutive++ - } - } else if (isUpper(c)) { - upper++ - if (i > 0 && isUpper(val.charCodeAt(i - 1))) { - consecutive++ - } - } else { - symbol++ - if (i !== 0 && i !== val.length - 1) { - MNS++ - } - } - var exists = false - for (var j = 0; j < val.length; j++) { - if (val[i] === val[j] && i !== j) { - exists = true - repC += Math.abs(val.length / (j - i)) - } - } - if (exists) { - rep++ - var unique = val.length - rep - repC = unique ? Math.ceil(repC / unique) : Math.ceil(repC) - } - if (i > 1) { - var last1 = val.charCodeAt(i - 1) - var last2 = val.charCodeAt(i - 2) - if (isLetter(c)) { - if (isLetter(last1) && isLetter(last2)) { - var v = val.toLowerCase() - var vi = v.charCodeAt(i) - var vi1 = v.charCodeAt(i - 1) - var vi2 = v.charCodeAt(i - 2) - if (vi - vi1 === vi1 - vi2 && Math.abs(vi - vi1) === 1) { - sequential++ - } - } - } else if (isNum(c)) { - if (isNum(last1) && isNum(last2)) { - if (c - last1 === last1 - last2 && Math.abs(c - last1) === 1) { - sequential++ - } - } - } else { - if (isSymbol(last1) && isSymbol(last2)) { - if (c - last1 === last1 - last2 && Math.abs(c - last1) === 1) { - sequential++ - } - } - } - } - } - let sum = 0 - let length = len() - sum += 4 * length - if (lower > 0) { - sum += 2 * (length - lower) - } - if (upper > 0) { - sum += 2 * (length - upper) - } - if (num !== length) { - sum += 4 * num - } - sum += 6 * symbol - sum += 2 * MNS - sum += 2 * callme() - if (length === lower + upper) { - sum -= length - } - if (length === num) { - sum -= num - } - sum -= repC - sum -= 2 * consecutive - sum -= 3 * sequential - sum = sum < 0 ? 0 : sum - sum = sum > 100 ? 100 : sum - - if (sum >= 80) { - return 100 - } else if (sum >= 60) { - return 80 - } else if (sum >= 40) { - return 60 - } else if (sum >= 20) { - return 40 - } else { - return 20 - } -} +import { mapStyledProps } from '../shared' export interface IPasswordProps extends InputProps { checkStrength: boolean @@ -158,7 +14,6 @@ const Password = styled( class Password extends React.Component { state = { value: this.props.value || this.props.defaultValue, - strength: 0, eye: false } @@ -168,8 +23,7 @@ const Password = styled( this.props.value !== this.state.value ) { this.setState({ - value: this.props.value, - strength: getStrength(this.props.value) + value: this.props.value }) } } @@ -177,8 +31,7 @@ const Password = styled( onChangeHandler = (value, e) => { this.setState( { - value, - strength: getStrength(value) + value }, () => { if (this.props.onChange) { @@ -189,20 +42,25 @@ const Password = styled( } renderStrength() { - const { strength } = this.state return ( -
    -
    -
    -
    -
    -
    -
    + + {score => { + return ( +
    +
    +
    +
    +
    +
    +
    + ) + }} + ) } diff --git a/packages/next/src/fields/radio.ts b/packages/next/src/fields/radio.ts index 5057f842427..319a718f1f7 100644 --- a/packages/next/src/fields/radio.ts +++ b/packages/next/src/fields/radio.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Radio } from '@alifd/next' -import { mapStyledProps, mapTextComponent } from '../utils' +import { mapStyledProps, mapTextComponent } from '../shared' const { Group: RadioGroup } = Radio diff --git a/packages/next/src/fields/range.ts b/packages/next/src/fields/range.ts index e22a7780209..5398fa152ba 100644 --- a/packages/next/src/fields/range.ts +++ b/packages/next/src/fields/range.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Range } from '@alifd/next' -import { mapStyledProps } from '../utils' +import { mapStyledProps } from '../shared' registerFormField( 'range', diff --git a/packages/next/src/fields/rating.ts b/packages/next/src/fields/rating.ts index bfc8be1771d..6b0473a63a3 100644 --- a/packages/next/src/fields/rating.ts +++ b/packages/next/src/fields/rating.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Rating } from '@alifd/next' -import { mapStyledProps } from '../utils' +import { mapStyledProps } from '../shared' registerFormField( 'rating', diff --git a/packages/next/src/fields/string.ts b/packages/next/src/fields/string.ts index ab3e0bde5f6..e0139740ed1 100644 --- a/packages/next/src/fields/string.ts +++ b/packages/next/src/fields/string.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Input } from '@alifd/next' -import { acceptEnum, mapStyledProps, mapTextComponent } from '../utils' +import { acceptEnum, mapStyledProps, mapTextComponent } from '../shared' registerFormField( 'string', diff --git a/packages/next/src/fields/table.tsx b/packages/next/src/fields/table.tsx index d090ce7adc4..131a09bfaa8 100644 --- a/packages/next/src/fields/table.tsx +++ b/packages/next/src/fields/table.tsx @@ -1,358 +1,181 @@ -import React, { Component } from 'react' -import { registerFormField } from '@uform/react' -import { isFn, toArr } from '@uform/utils' -import { ArrayField } from './array' +import React from 'react' +import { Icon } from '@alifd/next' +import { + registerFormField, + ISchemaFieldComponentProps, + SchemaField, + Schema +} from '@uform/react-schema-renderer' +import { toArr, isFn, isArr, FormPath } from '@uform/shared' +import { ArrayList } from '@uform/react-shared-components' +import { CircleButton, TextButton } from '../components/Button' +import { Table, Form } from '@alifd/next' import styled from 'styled-components' - -/** - * 轻量级Table,用next table实在太重了 - **/ - -export interface IColumnProps { - title?: string - dataIndex?: string - width?: string | number - cell: (item?: any, index?: number) => React.ReactElement +import { FormItemProps } from '../compat/FormItem' +const ArrayComponents = { + CircleButton, + TextButton, + AdditionIcon: () => , + RemoveIcon: () => ( + + ), + MoveDownIcon: () => ( + + ), + MoveUpIcon: () => ( + + ) } -export interface ITableProps { - className?: string - dataSource: any[] -} - -class Column extends Component { - static displayName = '@schema-table-column' - render() { - return this.props.children - } -} - -const Table = styled( - class Table extends Component { - renderCell({ record, col, rowIndex }) { - return ( -
    - {isFn(col.cell) - ? col.cell( - record ? record[col.dataIndex] : undefined, - rowIndex, - record +const FormTableField = styled( + (props: ISchemaFieldComponentProps & { className: string }) => { + const { value, schema, className, editable, path, mutators } = props + const { + renderAddition, + renderRemove, + renderMoveDown, + renderMoveUp, + renderEmpty, + renderExtraOperations, + operations, + ...componentProps + } = schema.getExtendsComponentProps() || {} + const onAdd = () => { + const items = Array.isArray(schema.items) + ? schema.items[schema.items.length - 1] + : schema.items + mutators.push(items.getEmptyValue()) + } + const renderColumns = (items: Schema) => { + return items.mapProperties((props, key) => { + const itemProps = { + ...props.getExtendsItemProps(), + ...props.getExtendsProps() + } + return ( + { + return ( + + + ) - : record - ? record[col.dataIndex] - : undefined} -
    - ) + }} + /> + ) + }) + return [] } - - renderTable(columns, dataSource) { - return ( -
    - - - - {columns.map((col, index) => { - return ( - - ) - })} - - - - {dataSource.map((record, rowIndex) => { + return ( +
    + +
    -
    {col.title}
    -
    + {isArr(schema.items) + ? schema.items.reduce((buf, items) => { + return buf.concat(renderColumns(items)) + }, []) + : renderColumns(schema.items)} + { return ( - - {columns.map((col, colIndex) => { - return ( - - ) - })} - + +
    + mutators.remove(index)} + /> + mutators.moveDown(index)} + /> + mutators.moveUp(index)} + /> + {isFn(renderExtraOperations) + ? renderExtraOperations(index) + : renderExtraOperations} +
    +
    ) - })} - {this.renderPlacehodler(dataSource, columns)} - -
    - {this.renderCell({ - record, - col, - rowIndex - })} -
    -
    - ) - } - - renderPlacehodler(dataSource, columns) { - if (dataSource.length === 0) { - return ( - - -
    - -
    - - - ) + }} + /> + + + {({ children }) => { + return ( +
    + {children} +
    + ) + }} +
    + +
    + ) + } +)` + display: inline-block; + min-width: 600px; + max-width: 100%; + overflow: scroll; + table { + margin-bottom: 0 !important; + th, + td { + padding: 0 !important; + vertical-align: top; + .next-form-item { + margin-bottom: 0 !important; } } - - getColumns(children) { - const columns: IColumnProps[] = [] - React.Children.forEach>( - children, - child => { - if (React.isValidElement(child)) { - if ( - child.type === Column || - child.type.displayName === '@schema-table-column' - ) { - columns.push(child.props) - } - } - } - ) - - return columns + } + .array-table-addition { + padding: 10px; + background: #fbfbfb; + border-left: 1px solid #dcdee3; + border-right: 1px solid #dcdee3; + border-bottom: 1px solid #dcdee3; + .next-btn-text { + color: #888; } - - render() { - const columns = this.getColumns(this.props.children) - const dataSource = toArr(this.props.dataSource) - return ( -
    -
    -
    - {this.renderTable(columns, dataSource)} -
    -
    -
    - ) + .next-icon:before { + width: 16px !important; + font-size: 16px !important; + margin-right: 5px; } - } -)` - .next-table { - position: relative; + margin-bottom: 10px; } - .next-table, - .next-table *, - .next-table :after, - .next-table :before { - -webkit-box-sizing: border-box; - box-sizing: border-box; - } - - .next-table table { - border-collapse: collapse; - border-spacing: 0; - width: 100%; - background: #fff; - display: table !important; - margin: 0 !important; - } - - .next-table table tr:first-child td { - border-top-width: 0; - } - - .next-table th { - padding: 0; - background: #ebecf0; - color: #333; - text-align: left; - font-weight: 400; - min-width: 200px; - border: 1px solid #dcdee3; - } - - .next-table th .next-table-cell-wrapper { - padding: 12px 16px; - overflow: hidden; - text-overflow: ellipsis; - word-break: break-all; - } - - .next-table td { - padding: 0; - border: 1px solid #dcdee3; - } - - .next-table td .next-table-cell-wrapper { - padding: 12px 16px; - overflow: hidden; - text-overflow: ellipsis; - word-break: break-all; + .array-item-operator { display: flex; - } - - .next-table.zebra tr:nth-child(odd) td { - background: #fff; - } - - .next-table.zebra tr:nth-child(2n) td { - background: #f7f8fa; - } - - .next-table-empty { - color: #a0a2ad; - padding: 32px 0; - text-align: center; - } - - .next-table-row { - -webkit-transition: all 0.3s ease; - transition: all 0.3s ease; - background: #fff; - color: #333; - border: none !important; - } - - .next-table-row.hidden { - display: none; - } - - .next-table-row.hovered, - .next-table-row.selected { - background: #f2f3f7; - color: #333; - } - - .next-table-body, - .next-table-header { - overflow: auto; - font-size: 12px; - } - - .next-table-body { - font-size: 12px; + align-items: center; + button { + margin-right: 8px; + } } ` -registerFormField( - 'table', - styled( - class extends ArrayField { - createFilter(key, payload) { - const { schema } = this.props - const columnFilter: (key: string, payload: any) => boolean = - schema['x-props'] && schema['x-props'].columnFilter - - return (render, otherwise) => { - if (isFn(columnFilter)) { - return columnFilter(key, payload) - ? isFn(render) - ? render() - : render - : isFn(otherwise) - ? otherwise() - : otherwise - } else { - return render() - } - } - } - - render() { - const { - value, - schema, - locale, - className, - renderField, - getOrderProperties - } = this.props - const cls = this.getProps('className') - const style = this.getProps('style') - const operationsWidth = this.getProps('operationsWidth') - return ( -
    -
    - - {getOrderProperties(schema.items).reduce( - (buf, { key, schema }) => { - const filter = this.createFilter(key, schema) - const res = filter( - () => { - return buf.concat( - { - return renderField([index, key]) - }} - /> - ) - }, - () => { - return buf - } - ) - return res - }, - [] - )} - { - return ( -
    - {this.renderRemove(index, item)} - {this.renderMoveDown(index, item)} - {this.renderMoveUp(index)} - {this.renderExtraOperations(index)} -
    - ) - }} - /> -
    - {this.renderAddition()} -
    -
    - ) - } - } - )` - display: inline-block; - .array-item-addition { - padding: 10px; - background: #fbfbfb; - border-left: 1px solid #dcdee3; - border-right: 1px solid #dcdee3; - border-bottom: 1px solid #dcdee3; - .next-btn-text { - color: #888; - } - .next-icon:before { - width: 16px !important; - font-size: 16px !important; - margin-right: 5px; - } - } - - .next-table-cell-wrapper > .next-form-item { - margin-bottom: 0; - } - .array-item-operator { - display: flex; - } - ` -) +registerFormField('table', FormTableField) diff --git a/packages/next/src/fields/textarea.ts b/packages/next/src/fields/textarea.ts index c65ce5616f2..ed33de7a648 100644 --- a/packages/next/src/fields/textarea.ts +++ b/packages/next/src/fields/textarea.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Input } from '@alifd/next' -import { acceptEnum, mapStyledProps, mapTextComponent } from '../utils' +import { acceptEnum, mapStyledProps, mapTextComponent } from '../shared' const { TextArea } = Input diff --git a/packages/next/src/fields/time.ts b/packages/next/src/fields/time.ts index 6a046c54a39..dd15c25b647 100644 --- a/packages/next/src/fields/time.ts +++ b/packages/next/src/fields/time.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { TimePicker } from '@alifd/next' -import { mapStyledProps, mapTextComponent } from '../utils' +import { mapStyledProps, mapTextComponent } from '../shared' const transformMoment = (value, format = 'YYYY-MM-DD HH:mm:ss') => { return value && value.format ? value.format(format) : value diff --git a/packages/next/src/fields/transfer.ts b/packages/next/src/fields/transfer.ts index 006f5473cab..b38c0a9842e 100644 --- a/packages/next/src/fields/transfer.ts +++ b/packages/next/src/fields/transfer.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Transfer } from '@alifd/next' -import { mapStyledProps } from '../utils' +import { mapStyledProps } from '../shared' registerFormField( 'transfer', diff --git a/packages/next/src/fields/upload.tsx b/packages/next/src/fields/upload.tsx index 887c816a759..cb9f9905cb9 100644 --- a/packages/next/src/fields/upload.tsx +++ b/packages/next/src/fields/upload.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { connect, registerFormField } from '@uform/react' -import { toArr, isArr, isEqual, mapStyledProps } from '../utils' +import { connect, registerFormField } from '@uform/react-schema-renderer' +import { toArr, isArr, isEqual, mapStyledProps } from '../shared' import { Button, Upload } from '@alifd/next' import { UploadProps, CardProps } from '@alifd/next/types/upload' const { Card: UploadCard, Dragger: UploadDragger } = Upload diff --git a/packages/next/src/form.tsx b/packages/next/src/form.tsx deleted file mode 100644 index 50cc06be6dc..00000000000 --- a/packages/next/src/form.tsx +++ /dev/null @@ -1,434 +0,0 @@ -import React from 'react' -import classNames from 'classnames' -import styled from 'styled-components' -import { ConfigProvider, Balloon, Icon, Grid } from '@alifd/next' -import { registerFormWrapper, registerFieldMiddleware } from '@uform/react' -import { IFormItemProps, IFormProps } from '@uform/types' - -import LOCALE from './locale' -import { isFn, moveTo, isStr, stringLength } from './utils' - -/** - * 轻量级Next Form,不包含任何数据管理能力 - */ - -const { Row, Col } = Grid - -export const { - Provider: FormLayoutProvider, - Consumer: FormLayoutConsumer -} = React.createContext(undefined) - -const normalizeCol = col => { - return typeof col === 'object' ? col : { span: col } -} - -const getParentNode = (node, selector) => { - if (!node || (node && !node.matches)) return - if (node.matches(selector)) return node - else { - return getParentNode(node.parentNode || node.parentElement, selector) - } -} - -const isPopDescription = (description, maxTipsNum = 30) => { - if (isStr(description)) { - return stringLength(description) > maxTipsNum - } else { - return React.isValidElement(description) - } -} - -export const FormItem = styled( - class FormItem extends React.Component { - static defaultProps = { - prefix: 'next-' - } - - private getItemLabel() { - const { - id, - required, - label, - labelCol, - wrapperCol, - prefix, - extra, - labelAlign, - labelTextAlign, - autoAddColon, - isTableColItem, - maxTipsNum - } = this.props - - if (!label || isTableColItem) { - return null - } - - const ele = ( - // @ts-ignore - - ) - - const cls = classNames({ - [`${prefix}form-item-label`]: true, - [`${prefix}left`]: labelTextAlign === 'left' - }) - - if ((wrapperCol || labelCol) && labelAlign !== 'top') { - return ( - - {ele} - {isPopDescription(extra, maxTipsNum) && this.renderHelper()} - - ) - } - - return ( -
    - {ele} - {isPopDescription(extra, maxTipsNum) && this.renderHelper()} -
    - ) - } - - private getItemWrapper() { - const { - labelCol, - wrapperCol, - children, - extra, - label, - labelAlign, - help, - size, - prefix, - noMinHeight, - isTableColItem, - maxTipsNum - } = this.props - - const message = ( -
    - {help &&
    {help}
    } - {!help && !isPopDescription(extra, maxTipsNum) && ( -
    {extra}
    - )} -
    - ) - if ( - (wrapperCol || labelCol) && - labelAlign !== 'top' && - !isTableColItem && - label - ) { - return ( - - {React.cloneElement(children, { size })} - {message} - - ) - } - - return ( -
    - {React.cloneElement(children, { size })} - {message} -
    - ) - } - - private renderHelper() { - return ( - } - > - {this.props.extra} - - ) - } - - public render() { - /* eslint-disable @typescript-eslint/no-unused-vars */ - const { - className, - labelAlign, - labelTextAlign, - style, - prefix, - wrapperCol, - labelCol, - size, - help, - extra, - noMinHeight, - isTableColItem, - validateState, - autoAddColon, - required, - maxTipsNum, - type, - schema, - ...others - } = this.props - - /* eslint-enable @typescript-eslint/no-unused-vars */ - const itemClassName = classNames({ - [`${prefix}form-item`]: true, - [`${prefix}${labelAlign}`]: labelAlign, - [`has-${validateState}`]: !!validateState, - [`${prefix}${size}`]: !!size, - [`${className}`]: !!className, - [`field-${type}`]: !!type - }) - - // 垂直模式并且左对齐才用到 - const Tag = (wrapperCol || labelCol) && labelAlign !== 'top' ? Row : 'div' - const label = labelAlign === 'inset' ? null : this.getItemLabel() - - return ( - - {label} - {this.getItemWrapper()} - - ) - } - } -)` - margin-bottom: 4px !important; - &.field-table { - .next-form-item-control { - overflow: auto; - } - } - .next-form-item-msg { - &.next-form-item-space { - min-height: 20px; - .next-form-item-help, - .next-form-item-extra { - margin-top: 0; - } - } - } - .next-form-item-extra { - color: #888; - font-size: 12px; - line-height: 1.7; - } -` - -const toArr = val => (Array.isArray(val) ? val : val ? [val] : []) - -registerFormWrapper(OriginForm => { - OriginForm = styled(OriginForm)` - &.next-inline { - display: flex; - .rs-uform-content { - margin-right: 15px; - } - } - .next-radio-group, - .next-checkbox-group { - line-height: 28px; - & > label { - margin-right: 8px; - } - } - .next-small { - .next-radio-group, - .next-checkbox-group { - line-height: 20px; - } - } - .next-small { - .next-radio-group, - .next-checkbox-group { - line-height: 40px; - } - } - .next-card-head { - background: none; - } - .next-rating-medium { - min-height: 28px; - line-height: 28px; - } - .next-rating-small { - min-height: 20px; - line-height: 20px; - } - .next-rating-large { - min-height: 40px; - line-height: 40px; - } - ` - - return ConfigProvider.config( - class Form extends React.Component { - static defaultProps = { - component: 'form', - prefix: 'next-', - size: 'medium', - labelAlign: 'left', - locale: LOCALE, - autoAddColon: true - } - - static displayName = 'SchemaForm' - - FormRef = React.createRef() - - validateFailedHandler(onValidateFailed) { - return (...args) => { - if (isFn(onValidateFailed)) { - onValidateFailed(...args) - } - const container = this.FormRef.current as HTMLElement - if (container) { - const errors = container.querySelectorAll('.next-form-item-help') - if (errors && errors.length) { - const node = getParentNode(errors[0], '.next-form-item') - if (node) { - moveTo(node) - } - } - } - } - } - - render() { - const { - className, - inline, - size, - labelAlign, - labelTextAlign, - autoAddColon, - children, - labelCol, - wrapperCol, - style, - prefix, - maxTipsNum, - ...others - } = this.props - - const formClassName = classNames({ - [`${prefix}form`]: true, - [`${prefix}inline`]: inline, // 内联 - [`${prefix}${size}`]: size, - [className]: !!className - }) - return ( - - - {children} - - - ) - } - }, - {} - ) -}) - -const isTableColItem = (path, getSchema) => { - const schema = getSchema(path) - return schema && schema.type === 'array' && schema['x-component'] === 'table' -} - -registerFieldMiddleware(Field => { - return props => { - const { - name, - editable, - errors, - path, - schemaPath, - schema, - getSchema, - required - } = props - - if (path.length === 0) { - // 根节点是不需要包FormItem的 - return React.createElement(Field, props) - } - - return React.createElement( - FormLayoutConsumer, - {}, - ({ - labelAlign, - labelTextAlign, - labelCol, - wrapperCol, - maxTipsNum, - size, - autoAddColon - }) => { - return React.createElement( - FormItem, - { - labelAlign, - labelTextAlign, - labelCol, - wrapperCol, - autoAddColon, - maxTipsNum, - size, - ...schema['x-item-props'], - label: schema.title, - noMinHeight: schema.type === 'object' && !schema['x-component'], - isTableColItem: isTableColItem( - schemaPath.slice(0, schemaPath.length - 2), - getSchema - ), - type: schema['x-component'] || schema['type'], - id: name, - validateState: toArr(errors).length ? 'error' : undefined, - required: editable === false ? false : required, - extra: schema.description, - help: - toArr(errors).join(' , ') || - (schema['x-item-props'] && schema['x-item-props'].help) - }, - React.createElement(Field, props) - ) - } - ) - } -}) diff --git a/packages/next/src/index.tsx b/packages/next/src/index.tsx index b47f2e0a123..85d9569b902 100644 --- a/packages/next/src/index.tsx +++ b/packages/next/src/index.tsx @@ -1,45 +1,17 @@ -import './form' -import './fields/string' -import './fields/number' -import './fields/boolean' -import './fields/date' -import './fields/time' -import './fields/range' -import './fields/upload' -import './fields/checkbox' -import './fields/radio' -import './fields/rating' -import './fields/transfer' -import './fields/array' -import './fields/cards' -import './fields/table' -import './fields/textarea' -import './fields/password' - -export * from '@uform/react' -export * from './components/formButtonGroup' -export * from './components/button' -export * from './components/layout' - import React from 'react' import { - SchemaForm as InternalSchemaForm, - Field as InternalField -} from '@uform/react' -import { SchemaFormProps, FieldProps } from './type' - -export { mapStyledProps, mapTextComponent } from './utils' - -export default class SchemaForm extends React.Component> { - render() { - return - } -} - -export class Field extends React.Component< - FieldProps -> { - render() { - return - } -} + SchemaMarkupForm, + SchemaMarkupField +} from '@uform/react-schema-renderer' +import { INextSchemaFormProps, INextSchemaFieldProps } from './types' +import './fields' +import './compat' +export * from '@uform/react-schema-renderer' +export * from './components' +export * from './types' +export { mapStyledProps, mapTextComponent } from './shared' +export const SchemaForm: React.FC< + INextSchemaFormProps +> = SchemaMarkupForm as any +export const Field: React.FC = SchemaMarkupField +export default SchemaForm diff --git a/packages/next/src/locale.ts b/packages/next/src/locale.ts deleted file mode 100644 index aa28ff56c6e..00000000000 --- a/packages/next/src/locale.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable @typescript-eslint/camelcase */ -export default { - addItem: '添加', - array_invalid_minItems: '条目数不允许小于%s条', - array_invalid_maxItems: '条目数不允许大于%s条', - operations: '操作' -} diff --git a/packages/next/src/shared.ts b/packages/next/src/shared.ts new file mode 100644 index 00000000000..5d7761ae55a --- /dev/null +++ b/packages/next/src/shared.ts @@ -0,0 +1,66 @@ +import React from 'react' +import { Select } from '@alifd/next' +import { PreviewText } from '@uform/react-shared-components' +import { + MergedFieldComponentProps, + IConnectProps +} from '@uform/react-schema-renderer' +export * from '@uform/shared' + +export const mapTextComponent = ( + Target: React.JSXElementConstructor, + props: any = {}, + fieldProps: any = {} +): React.JSXElementConstructor => { + const { editable } = fieldProps + if (editable !== undefined) { + if (editable === false) { + return PreviewText + } + } + if (Array.isArray(props.dataSource)) { + return Select + } + return Target +} + +export const acceptEnum = (component: React.JSXElementConstructor) => { + return ({ dataSource, ...others }) => { + if (dataSource) { + return React.createElement(Select, { dataSource, ...others }) + } else { + return React.createElement(component, others) + } + } +} + +export const normalizeCol = ( + col: { span: number; offset?: number } | number, + defaultValue: { span: number } +): { span: number; offset?: number } => { + if (!col) { + return defaultValue + } else { + return typeof col === 'object' ? col : { span: Number(col) } + } +} + +export const mapStyledProps = ( + props: IConnectProps, + fieldProps: MergedFieldComponentProps +) => { + const { loading, errors } = fieldProps + if (loading) { + props.state = props.state || 'loading' + } else if (errors && errors.length) { + props.state = 'error' + } +} + +export const compose = (...args: any[]) => { + return (payload: any, ...extra: any[]) => { + return args.reduce((buf, fn) => { + return buf !== undefined ? fn(buf, ...extra) : fn(payload, ...extra) + }, payload) + } +} diff --git a/packages/next/src/type.tsx b/packages/next/src/type.tsx deleted file mode 100644 index 24bae70f004..00000000000 --- a/packages/next/src/type.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { ButtonProps } from '@alifd/next/types/button' -import { CardProps } from '@alifd/next/types/card' -import { RowProps, ColProps } from '@alifd/next/types/grid' -import { - IFormActions, - ISchema, - IEffects, - IFieldError, - Size, - TextAlign, - Layout, - TextEl, - LabelAlign -} from '@uform/types' -import { SwitchProps } from '@alifd/next/types/switch' -import { GroupProps as CheckboxGroupProps } from '@alifd/next/types/checkbox' -import { GroupProps as RadioGroupProps } from '@alifd/next/types/radio' -import { - DatePickerProps, - RangePickerProps, - MonthPickerProps, - YearPickerProps -} from '@alifd/next/types/date-picker' -import { NumberPickerProps } from '@alifd/next/types/number-picker' -import { IPasswordProps } from './fields/password' -import { RangeProps } from '@alifd/next/types/range' -import { RatingProps } from '@alifd/next/types/rating' -import { InputProps, TextAreaProps } from '@alifd/next/types/input' -import { TimePickerProps } from '@alifd/next/types/time-picker' -import { TransferProps } from '@alifd/next/types/transfer' -import { IUploaderProps } from './fields/upload' -import { SelectProps } from '@alifd/next/types/select' - -type ColSpanType = number | string - -export interface ColSize { - span?: ColSpanType - offset?: ColSpanType -} - -export interface ILocaleMessages { - [key: string]: string | ILocaleMessages -} - -export interface IFormLayoutProps { - className?: string - inline?: boolean - labelAlign?: LabelAlign - wrapperCol?: IColProps | number - labelCol?: IColProps | number - labelTextAlign?: TextAlign - size?: Size - style?: React.CSSProperties -} - -export interface IFormItemGridProps { - cols?: Array - description?: TextEl - gutter?: number - title?: TextEl -} - -export type TFormCardOrFormBlockProps = Omit - -export interface IFormTextBox { - text?: string - title?: TextEl - description?: TextEl - gutter?: number -} - -export interface IRowProps extends RowProps { - prefix?: string - pure?: boolean - className?: string - style?: object -} - -export interface IColProps extends ColProps { - prefix?: string - pure?: boolean - className?: string -} - -export interface IFormCardProps extends CardProps { - className?: string -} - -export interface IFormBlockProps extends CardProps { - className?: string -} - -export interface ISubmitProps extends Omit { - showLoading?: boolean -} - -export interface IFormButtonGroupProps { - sticky?: boolean - style?: React.CSSProperties - itemStyle?: React.CSSProperties - className?: string - align?: 'left' | 'right' | 'start' | 'end' | 'top' | 'bottom' | 'center' - triggerDistance?: number - zIndex?: number - span?: ColSpanType - offset?: ColSpanType -} - -export interface SchemaFormProps { - actions?: IFormActions - initialValues?: V - defaultValue?: V - value?: V - editable?: boolean | ((name: string) => boolean) - effects?: IEffects - locale?: ILocaleMessages - schema?: ISchema - onChange?: (values: V) => void - onReset?: (values: V) => void - onSubmit?: (values: V) => void - onValidateFailed?: (fieldErrors: IFieldError[]) => void - autoAddColon?: boolean - className?: string - inline?: boolean - layout?: Layout - maxTipsNum?: number - labelAlign?: LabelAlign - labelTextAlign?: TextAlign - labelCol?: ColSize | number - wrapperCol?: ColSize | number - size?: Size - style?: React.CSSProperties - prefix?: string -} - -interface InternalFieldTypes { - boolean: SwitchProps | SelectProps - checkbox: CheckboxGroupProps - date: DatePickerProps - daterange: RangePickerProps - month: MonthPickerProps - // week: WeekPickerProps - year: YearPickerProps - number: NumberPickerProps | SelectProps - password: IPasswordProps - radio: RadioGroupProps - range: RangeProps - rating: RatingProps - string: InputProps | SelectProps - textarea: TextAreaProps | SelectProps - time: TimePickerProps - transfer: TransferProps - upload: IUploaderProps -} -export interface FieldProps extends ISchema { - type?: T - name?: string - editable?: boolean - ['x-props']?: T extends keyof InternalFieldTypes ? InternalFieldTypes[T] : any -} diff --git a/packages/next/src/types.ts b/packages/next/src/types.ts new file mode 100644 index 00000000000..b93217d9e24 --- /dev/null +++ b/packages/next/src/types.ts @@ -0,0 +1,95 @@ +import { ButtonProps } from '@alifd/next/types/button' +import { FormProps, ItemProps } from '@alifd/next/types/form' +import { StepProps, ItemProps as StepItemProps } from '@alifd/next/types/step' +import { + ISchemaFormProps, + IMarkupSchemaFieldProps, + ISchemaFieldComponentProps +} from '@uform/react-schema-renderer' +import { StyledComponent } from 'styled-components' + +type ColSpanType = number | string + +export type INextSchemaFormProps = ISchemaFormProps & + FormProps & + IFormItemTopProps + +export type INextSchemaFieldProps = IMarkupSchemaFieldProps + +export interface ISubmitProps extends ButtonProps { + onSubmit?: ISchemaFormProps['onSubmit'] + showLoading?: boolean +} + +export interface IResetProps extends ButtonProps { + forceClear?: boolean + validate?: boolean +} + +export type IFormItemTopProps = React.PropsWithChildren< + Exclude< + Pick< + ItemProps, + | 'prefix' + | 'labelCol' + | 'wrapperCol' + | 'labelAlign' + | 'labelTextAlign' + | 'size' + >, + 'labelCol' | 'wrapperCol' + > & { + inline?: boolean + className?: string + style?: React.CSSProperties + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } + } +> + +export interface ICompatItemProps + extends Exclude, + Partial { + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} + +export type StyledCP

    = StyledComponent< + (props: React.PropsWithChildren

    ) => React.ReactElement, + any, + {}, + never +> + +export type StyledCC = StyledCP & Statics + +export interface IFormButtonGroupProps { + sticky?: boolean + style?: React.CSSProperties + itemStyle?: React.CSSProperties + className?: string + align?: 'left' | 'right' | 'start' | 'end' | 'top' | 'bottom' | 'center' + triggerDistance?: number + zIndex?: number + span?: ColSpanType + offset?: ColSpanType +} + +export interface IItemProps { + title?: React.ReactText + description?: React.ReactText +} + +export interface IFormItemGridProps extends IItemProps { + cols?: Array + gutter?: number +} + +export interface IFormTextBox extends IItemProps { + text?: string + gutter?: number +} + +export interface IFormStep extends StepProps { + dataSource: StepItemProps[] +} diff --git a/packages/next/src/utils.tsx b/packages/next/src/utils.tsx deleted file mode 100644 index 1da41174248..00000000000 --- a/packages/next/src/utils.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React from 'react' -import { Select } from '@alifd/next' -import styled from 'styled-components' -import { isFn } from '@uform/utils' -import { IConnectProps, IFieldProps } from '@uform/react' - -export * from '@uform/utils' - -const MoveTo = typeof window !== 'undefined' ? require('moveto') : null -const Text = styled(props => { - let value - if (props.dataSource && props.dataSource.length) { - let find = props.dataSource.filter(({ value }) => - Array.isArray(props.value) - ? props.value.some(val => val == value) - : props.value == value - ) - value = find.map(item => item.label).join(' , ') - } else { - value = Array.isArray(props.value) - ? props.value.join(' ~ ') - : String( - props.value === undefined || props.value === null ? '' : props.value - ) - } - return ( -

    - {!value ? 'N/A' : value} - {props.innerAfter ? ' ' + props.innerAfter : ''} - {props.addonTextAfter ? ' ' + props.addonTextAfter : ''} - {props.addonAfter ? ' ' + props.addonAfter : ''} -
    - ) -})` - height: 28px; - line-height: 28px; - vertical-align: middle; - font-size: 13px; - color: #333; - &.small { - height: 20px; - line-height: 20px; - } - &.large { - height: 40px; - line-height: 40px; - } -` - -export const acceptEnum = component => { - return ({ dataSource, ...others }) => { - if (dataSource) { - return React.createElement(Select, { dataSource, ...others }) - } else { - return React.createElement(component, others) - } - } -} - -export const mapStyledProps = ( - props: IConnectProps, - { loading, size, errors }: IFieldProps -) => { - if (loading) { - props.state = props.state || 'loading' - } else if (errors && errors.length) { - props.state = 'error' - } - if (size) { - props.size = size - } -} - -export const mapTextComponent = ( - Target: React.ComponentClass, - props, - { - editable, - name - }: { editable: boolean | ((name: string) => boolean); name: string } -): React.ComponentClass => { - if (editable !== undefined) { - if (isFn(editable)) { - if (!editable(name)) { - return Text - } - } else if (editable === false) { - return Text - } - } - return Target -} - -export const compose = (...args) => { - return (payload, ...extra) => { - return args.reduce((buf, fn) => { - return buf !== undefined ? fn(buf, ...extra) : fn(payload, ...extra) - }, payload) - } -} - -export const moveTo = element => { - if (!element || !MoveTo) return - if (element.scrollIntoView) { - element.scrollIntoView({ - behavior: 'smooth', - inline: 'start', - block: 'start' - }) - } else { - new MoveTo().move(element.getBoundingClientRect().top) - } -} diff --git a/packages/next/tsconfig.json b/packages/next/tsconfig.json index 1d669c29c46..7d101da7c66 100644 --- a/packages/next/tsconfig.json +++ b/packages/next/tsconfig.json @@ -3,6 +3,6 @@ "compilerOptions": { "outDir": "./lib" }, - "include": ["./src/**/*.ts", "./src/**/*.tsx"], + "include": ["./src/**/*.js", "./src/**/*.ts", "./src/**/*.tsx"], "exclude": ["./src/__tests__/*"] } diff --git a/packages/printer/package.json b/packages/printer/package.json index 33135913ec6..920ca202bed 100644 --- a/packages/printer/package.json +++ b/packages/printer/package.json @@ -1,6 +1,6 @@ { "name": "@uform/printer", - "version": "0.4.3", + "version": "0.4.0", "license": "MIT", "main": "lib", "repository": { @@ -26,7 +26,7 @@ "typescript": "^3.5.2" }, "dependencies": { - "@uform/react": "^0.4.3", + "@uform/react-schema-renderer": "^0.4.0", "react-modal": "^3.8.1", "styled-components": "^4.1.1" }, diff --git a/packages/printer/src/index.js b/packages/printer/src/index.js index 577a44ab55d..a0b9a71ce6a 100644 --- a/packages/printer/src/index.js +++ b/packages/printer/src/index.js @@ -1,6 +1,6 @@ import React, { useState } from 'react' import ReactDOM from 'react-dom' -import { createFormActions } from '@uform/react' +import { createFormActions } from '@uform/react-schema-renderer' import styled from 'styled-components' import Modal from 'react-modal' diff --git a/packages/builder-next/.npmignore b/packages/react-schema-renderer/.npmignore similarity index 100% rename from packages/builder-next/.npmignore rename to packages/react-schema-renderer/.npmignore diff --git a/packages/react/LESENCE.md b/packages/react-schema-renderer/LESENCE.md similarity index 100% rename from packages/react/LESENCE.md rename to packages/react-schema-renderer/LESENCE.md diff --git a/packages/react-schema-renderer/README.md b/packages/react-schema-renderer/README.md new file mode 100644 index 00000000000..0f89496d1c6 --- /dev/null +++ b/packages/react-schema-renderer/README.md @@ -0,0 +1,2 @@ +# @uform/react-schema-renderer +> UForm React实现 \ No newline at end of file diff --git a/packages/react/jest.config.js b/packages/react-schema-renderer/jest.config.js similarity index 100% rename from packages/react/jest.config.js rename to packages/react-schema-renderer/jest.config.js diff --git a/packages/builder-next/package.json b/packages/react-schema-renderer/package.json similarity index 59% rename from packages/builder-next/package.json rename to packages/react-schema-renderer/package.json index a3e15207104..d14d82987e5 100644 --- a/packages/builder-next/package.json +++ b/packages/react-schema-renderer/package.json @@ -1,8 +1,8 @@ { - "name": "@uform/builder-next", - "version": "0.4.3", + "name": "@uform/react-schema-renderer", + "version": "0.4.0", "license": "MIT", - "main": "lib/index.js", + "main": "lib", "repository": { "type": "git", "url": "git+https://github.com/alibaba/uform.git" @@ -14,23 +14,26 @@ "engines": { "npm": ">=3.0.0" }, + "types": "lib/index.d.ts", "scripts": { - "build": "tsc" + "build": "tsc --declaration" }, - "resolutions": { - "@types/react": "16.8.23" + "devDependencies": { + "typescript": "^3.5.2" }, "peerDependencies": { - "@alifd/next": "^1.13.1", "@babel/runtime": "^7.4.4", + "@types/json-schema": "^7.0.3", + "@types/react": "^16.8.23", "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "react-dom": ">=16.8.0", + "react-eva": "^1.1.7" }, "dependencies": { - "@uform/builder": "^0.4.3" - }, - "devDependencies": { - "typescript": "^3.5.2" + "@uform/react": "^0.4.0", + "@uform/shared": "^0.4.0", + "@uform/validator": "^0.4.0", + "pascal-case": "^2.0.1" }, "publishConfig": { "access": "public" diff --git a/packages/react/src/__tests__/actions.spec.js b/packages/react-schema-renderer/src/__old_tests__/actions.spec.js similarity index 100% rename from packages/react/src/__tests__/actions.spec.js rename to packages/react-schema-renderer/src/__old_tests__/actions.spec.js diff --git a/packages/react/src/__tests__/context.spec.js b/packages/react-schema-renderer/src/__old_tests__/context.spec.js similarity index 100% rename from packages/react/src/__tests__/context.spec.js rename to packages/react-schema-renderer/src/__old_tests__/context.spec.js diff --git a/packages/react/src/__tests__/destruct.spec.js b/packages/react-schema-renderer/src/__old_tests__/destruct.spec.js similarity index 97% rename from packages/react/src/__tests__/destruct.spec.js rename to packages/react-schema-renderer/src/__old_tests__/destruct.spec.js index 37a3ec6726f..871b83bef73 100644 --- a/packages/react/src/__tests__/destruct.spec.js +++ b/packages/react-schema-renderer/src/__old_tests__/destruct.spec.js @@ -1,6 +1,6 @@ import React, { Fragment } from 'react' import SchemaForm, { Field, registerFormField, connect } from '../index' -import { toArr } from '@uform/utils' +import { toArr } from '@uform/shared' import { render } from '@testing-library/react' registerFormField('string', connect()(props =>
    {props.value}
    )) diff --git a/packages/react/src/__tests__/display.spec.js b/packages/react-schema-renderer/src/__old_tests__/display.spec.js similarity index 100% rename from packages/react/src/__tests__/display.spec.js rename to packages/react-schema-renderer/src/__old_tests__/display.spec.js diff --git a/packages/react/src/__tests__/dynamic.spec.js b/packages/react-schema-renderer/src/__old_tests__/dynamic.spec.js similarity index 99% rename from packages/react/src/__tests__/dynamic.spec.js rename to packages/react-schema-renderer/src/__old_tests__/dynamic.spec.js index 9379aa7d1bb..00b98d15129 100644 --- a/packages/react/src/__tests__/dynamic.spec.js +++ b/packages/react-schema-renderer/src/__old_tests__/dynamic.spec.js @@ -7,7 +7,7 @@ import SchemaForm, { createFormActions, createVirtualBox } from '../index' -import { toArr } from '@uform/utils' +import { toArr } from '@uform/shared' import { render, fireEvent, act } from '@testing-library/react' let FormCard diff --git a/packages/react/src/__tests__/editable.spec.js b/packages/react-schema-renderer/src/__old_tests__/editable.spec.js similarity index 89% rename from packages/react/src/__tests__/editable.spec.js rename to packages/react-schema-renderer/src/__old_tests__/editable.spec.js index 70c8c71033b..6b29fa53977 100644 --- a/packages/react/src/__tests__/editable.spec.js +++ b/packages/react-schema-renderer/src/__old_tests__/editable.spec.js @@ -8,7 +8,7 @@ import SchemaForm, { FormPath } from '../index' import { render, act, fireEvent } from '@testing-library/react' -import { toArr } from '@uform/utils' +import { toArr } from '@uform/shared' registerFieldMiddleware(Field => { return props => { @@ -375,53 +375,3 @@ test('editable conflicts that x-props editable props with setFieldState', async await sleep(33) expect(queryByTestId('this is bbb')).toBeVisible() }) - -test('fix:#300', async () => { - const beforeValue = { - name: 'completeOrderParam', - protocol: 'dubbo', - rpcGroup: 'HSF', - rpcMethod: 'completeCommodity', - rpcService: 'com.aliyun.lx.spi.babeldemo.TradeService:1.0.1', - status: 'pre' - } - - const afterValue = { - name: 'completeCommodity', - protocol: 'pop', - rpcGroup: 'popGroup', - rpcMethod: 'popMethod', - rpcService: 'popService', - status: 'product' - } - - const TestComponent = () => { - const [values, setValues] = useState({}) - function getValue() { - setValues(afterValue) - } - return ( - - - - - ) - } - - const { queryByText } = render() - await sleep(33) - fireEvent.click(queryByText('GetValue')) - await sleep(33) - expect(queryByText('empty')).toBeVisible() -}) diff --git a/packages/react/src/__tests__/effects.spec.js b/packages/react-schema-renderer/src/__old_tests__/effects.spec.js similarity index 100% rename from packages/react/src/__tests__/effects.spec.js rename to packages/react-schema-renderer/src/__old_tests__/effects.spec.js diff --git a/packages/react/src/__tests__/mutators.spec.js b/packages/react-schema-renderer/src/__old_tests__/mutators.spec.js similarity index 100% rename from packages/react/src/__tests__/mutators.spec.js rename to packages/react-schema-renderer/src/__old_tests__/mutators.spec.js diff --git a/packages/react/src/__tests__/schema_form.spec.js b/packages/react-schema-renderer/src/__old_tests__/schema_form.spec.js similarity index 100% rename from packages/react/src/__tests__/schema_form.spec.js rename to packages/react-schema-renderer/src/__old_tests__/schema_form.spec.js diff --git a/packages/react/src/__tests__/traverse.spec.js b/packages/react-schema-renderer/src/__old_tests__/traverse.spec.js similarity index 100% rename from packages/react/src/__tests__/traverse.spec.js rename to packages/react-schema-renderer/src/__old_tests__/traverse.spec.js diff --git a/packages/react/src/__tests__/utils.spec.js b/packages/react-schema-renderer/src/__old_tests__/utils.spec.js similarity index 100% rename from packages/react/src/__tests__/utils.spec.js rename to packages/react-schema-renderer/src/__old_tests__/utils.spec.js diff --git a/packages/react/src/__tests__/validate.spec.js b/packages/react-schema-renderer/src/__old_tests__/validate.spec.js similarity index 95% rename from packages/react/src/__tests__/validate.spec.js rename to packages/react-schema-renderer/src/__old_tests__/validate.spec.js index 2bd27fcbe7d..b5b57119123 100644 --- a/packages/react/src/__tests__/validate.spec.js +++ b/packages/react-schema-renderer/src/__old_tests__/validate.spec.js @@ -437,26 +437,3 @@ test('async validate side effect', async () => { expect(queryByText('aa is required')).toBeVisible() expect(queryByText('bb is required')).toBeNull() }) - -test('async validate side effect', async () => { - const actions = createFormActions() - const TestComponent = () => { - return ( - - - - - - ) - } - const { queryByText } = render() - await sleep(33) - - fireEvent.click(queryByText('Submit')) - await sleep(33) - actions.setFieldState(FormPath.match('*'), state => { - state.editable = false - }) - await sleep(33) - expect(queryByText('aa is required')).toBeNull() -}) diff --git a/packages/react/src/__tests__/validate_relations.spec.js b/packages/react-schema-renderer/src/__old_tests__/validate_relations.spec.js similarity index 100% rename from packages/react/src/__tests__/validate_relations.spec.js rename to packages/react-schema-renderer/src/__old_tests__/validate_relations.spec.js diff --git a/packages/react/src/__tests__/value.spec.js b/packages/react-schema-renderer/src/__old_tests__/value.spec.js similarity index 97% rename from packages/react/src/__tests__/value.spec.js rename to packages/react-schema-renderer/src/__old_tests__/value.spec.js index 84ee7b50919..d3ef1224b45 100644 --- a/packages/react/src/__tests__/value.spec.js +++ b/packages/react-schema-renderer/src/__old_tests__/value.spec.js @@ -147,9 +147,9 @@ test('controlled with hooks by initalValues', async () => { await actions.reset() await sleep(33) expect(queryByTestId('test-input').value).toEqual('123') - expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') - expect(onChangeHandler).toHaveBeenCalledTimes(4) + expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') + expect(onChangeHandler).toHaveBeenCalledTimes(3) }) test('controlled with hooks by static value', async () => { @@ -192,9 +192,9 @@ test('controlled with hooks by static value', async () => { expect(onChangeHandler).toHaveBeenCalledTimes(3) actions.reset() await sleep(33) - expect(queryByTestId('outer-result').textContent).toEqual('Total is:') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:') - expect(onChangeHandler).toHaveBeenCalledTimes(4) + expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') + expect(onChangeHandler).toHaveBeenCalledTimes(3) await actions.setFieldState('a3', state => { state.value = '456' }) @@ -202,13 +202,13 @@ test('controlled with hooks by static value', async () => { expect(queryByTestId('test-input').value).toEqual('123') expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') - expect(onChangeHandler).toHaveBeenCalledTimes(6) + expect(onChangeHandler).toHaveBeenCalledTimes(5) await actions.reset() await sleep(33) expect(queryByTestId('test-input').value).toEqual('') - expect(queryByTestId('outer-result').textContent).toEqual('Total is:') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:') - expect(onChangeHandler).toHaveBeenCalledTimes(7) + expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') + expect(onChangeHandler).toHaveBeenCalledTimes(5) }) test('controlled with hooks by dynamic value', async () => { @@ -252,9 +252,9 @@ test('controlled with hooks by dynamic value', async () => { actions.reset() await sleep(33) expect(queryByTestId('test-input').value).toEqual('') - expect(queryByTestId('outer-result').textContent).toEqual('Total is:') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:') - expect(onChangeHandler).toHaveBeenCalledTimes(3) + expect(queryByTestId('outer-result').textContent).toEqual('Total is:333') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:333') + expect(onChangeHandler).toHaveBeenCalledTimes(2) await actions.setFieldState('a3', state => { state.value = '456' }) @@ -262,13 +262,13 @@ test('controlled with hooks by dynamic value', async () => { expect(queryByTestId('test-input').value).toEqual('456') expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') - expect(onChangeHandler).toHaveBeenCalledTimes(4) + expect(onChangeHandler).toHaveBeenCalledTimes(3) await actions.reset() await sleep(33) expect(queryByTestId('test-input').value).toEqual('') - expect(queryByTestId('outer-result').textContent).toEqual('Total is:') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:') - expect(onChangeHandler).toHaveBeenCalledTimes(5) + expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') + expect(onChangeHandler).toHaveBeenCalledTimes(3) }) test('invariant initialValues will not be changed when form rerender', async () => { diff --git a/packages/react/src/__tests__/virtualbox.spec.js b/packages/react-schema-renderer/src/__old_tests__/virtualbox.spec.js similarity index 100% rename from packages/react/src/__tests__/virtualbox.spec.js rename to packages/react-schema-renderer/src/__old_tests__/virtualbox.spec.js diff --git a/packages/react/src/__tests__/visible.spec.js b/packages/react-schema-renderer/src/__old_tests__/visible.spec.js similarity index 100% rename from packages/react/src/__tests__/visible.spec.js rename to packages/react-schema-renderer/src/__old_tests__/visible.spec.js diff --git a/packages/react/src/__tests__/x-component.spec.js b/packages/react-schema-renderer/src/__old_tests__/x-component.spec.js similarity index 100% rename from packages/react/src/__tests__/x-component.spec.js rename to packages/react-schema-renderer/src/__old_tests__/x-component.spec.js diff --git a/packages/react-schema-renderer/src/__tests__/__snapshots__/markup.spec.tsx.snap b/packages/react-schema-renderer/src/__tests__/__snapshots__/markup.spec.tsx.snap new file mode 100644 index 00000000000..8f8529a73aa --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/__snapshots__/markup.spec.tsx.snap @@ -0,0 +1,218 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`test all apis markup string 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": "123", + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": true, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": "aa change", + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": "123", + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": true, + "name": "aa", + "pristine": false, + "props": Object { + "default": "123", + "type": "string", + "x-props": Object { + "data-testid": "input", + }, + }, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "aa change", + "values": Array [ + "aa change", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`test all apis markup virtualbox 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": "123", + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": true, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": "aa change", + }, + "warnings": Array [], + }, + "NO_NAME_FIELD_$0": Object { + "display": true, + "displayName": "VirtualFieldState", + "initialized": true, + "mounted": false, + "name": "NO_NAME_FIELD_$0", + "props": Object { + "type": "object", + "x-component": "card", + "x-props": Object { + "title": "this is card", + }, + }, + "unmounted": false, + "visible": true, + }, + "NO_NAME_FIELD_$0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": "123", + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": true, + "name": "NO_NAME_FIELD_$0.aa", + "pristine": false, + "props": Object { + "default": "123", + "type": "string", + "x-props": Object { + "data-testid": "input", + }, + }, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "aa change", + "values": Array [ + "aa change", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": "123", + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": true, + "name": "NO_NAME_FIELD_$0.aa", + "pristine": false, + "props": Object { + "default": "123", + "type": "string", + "x-props": Object { + "data-testid": "input", + }, + }, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "aa change", + "values": Array [ + "aa change", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`test all apis markup virtualbox 2`] = ` +Object { + "properties": Object { + "NO_NAME_FIELD_$0": Object { + "properties": Object { + "aa": Object { + "default": "123", + "type": "string", + "x-props": Object { + "data-testid": "input", + }, + }, + }, + "type": "object", + "x-component": "card", + "x-props": Object { + "title": "this is card", + }, + }, + }, + "type": "object", +} +`; diff --git a/packages/react-schema-renderer/src/__tests__/__snapshots__/register.spec.tsx.snap b/packages/react-schema-renderer/src/__tests__/__snapshots__/register.spec.tsx.snap new file mode 100644 index 00000000000..52f8a58efbc --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/__snapshots__/register.spec.tsx.snap @@ -0,0 +1,259 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`test all apis registerFieldMiddleware 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": "123", + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": true, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": "aa change", + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": "123", + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": true, + "name": "aa", + "pristine": false, + "props": Object { + "default": "123", + "type": "string", + "x-props": Object { + "data-testid": "input", + }, + }, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "aa change", + "values": Array [ + "aa change", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`test all apis registerFormField 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": "123", + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": true, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": "aa change", + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": "123", + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": true, + "name": "aa", + "pristine": false, + "props": Object { + "default": "123", + "type": "string", + "x-props": Object { + "data-testid": "input", + }, + }, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "aa change", + "values": Array [ + "aa change", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`test all apis registerVirtualBox 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": "123", + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": true, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": "aa change", + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": "123", + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": true, + "name": "cc.aa", + "pristine": false, + "props": Object { + "default": "123", + "type": "string", + "x-props": Object { + "data-testid": "input", + }, + }, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "aa change", + "values": Array [ + "aa change", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "cc": Object { + "display": true, + "displayName": "VirtualFieldState", + "initialized": true, + "mounted": false, + "name": "cc", + "props": Object { + "type": "object", + "x-component": "box", + }, + "unmounted": false, + "visible": true, + }, + "cc.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": "123", + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": true, + "name": "cc.aa", + "pristine": false, + "props": Object { + "default": "123", + "type": "string", + "x-props": Object { + "data-testid": "input", + }, + }, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "aa change", + "values": Array [ + "aa change", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; diff --git a/packages/react-schema-renderer/src/__tests__/field.spec.tsx b/packages/react-schema-renderer/src/__tests__/field.spec.tsx new file mode 100644 index 00000000000..3764eab2358 --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/field.spec.tsx @@ -0,0 +1,12 @@ +describe('test all apis',()=>{ + //todo +}) + +describe('major scenes',()=>{ + //todo +}) + + +describe('bugfix',()=>{ + //todo +}) \ No newline at end of file diff --git a/packages/react-schema-renderer/src/__tests__/form.spec.tsx b/packages/react-schema-renderer/src/__tests__/form.spec.tsx new file mode 100644 index 00000000000..3764eab2358 --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/form.spec.tsx @@ -0,0 +1,12 @@ +describe('test all apis',()=>{ + //todo +}) + +describe('major scenes',()=>{ + //todo +}) + + +describe('bugfix',()=>{ + //todo +}) \ No newline at end of file diff --git a/packages/react-schema-renderer/src/__tests__/json-schema.spec.tsx b/packages/react-schema-renderer/src/__tests__/json-schema.spec.tsx new file mode 100644 index 00000000000..3764eab2358 --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/json-schema.spec.tsx @@ -0,0 +1,12 @@ +describe('test all apis',()=>{ + //todo +}) + +describe('major scenes',()=>{ + //todo +}) + + +describe('bugfix',()=>{ + //todo +}) \ No newline at end of file diff --git a/packages/react-schema-renderer/src/__tests__/markup.spec.tsx b/packages/react-schema-renderer/src/__tests__/markup.spec.tsx new file mode 100644 index 00000000000..9013675dd70 --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/markup.spec.tsx @@ -0,0 +1,105 @@ +import React from 'react' +import { + registerFormField, + createVirtualBox, + connect, + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, + createFormActions, + cleanRegistry +} from '../index' +import { render, fireEvent, wait } from '@testing-library/react' + +describe('test all apis', () => { + beforeEach(() => { + registerFormField( + 'string', + connect()(props => { + return + }) + ) + }) + + afterEach(() => { + cleanRegistry() + }) + + test('markup string', async () => { + const actions = createFormActions() + + const { queryByTestId } = render( + + + + ) + expect(queryByTestId('input').getAttribute('value')).toEqual('123') + fireEvent.change(queryByTestId('input'), { + target: { + value: 'aa change' + } + }) + await wait(() => { + expect(queryByTestId('input').getAttribute('value')).toEqual('aa change') + expect(actions.getFormGraph()).toMatchSnapshot() + expect(actions.getFormState(state => state.values)).toEqual({ + aa: 'aa change' + }) + }) + }) + + test('markup virtualbox', async () => { + const Card = createVirtualBox<{ title?: string | React.ReactElement }>( + 'card', + props => { + return ( +
    +
    Title:{props.title}
    +
    {props.children}
    +
    + ) + } + ) + + const actions = createFormActions() + + const { queryByTestId } = render( + + + + + + ) + expect(queryByTestId('input').getAttribute('value')).toEqual('123') + fireEvent.change(queryByTestId('input'), { + target: { + value: 'aa change' + } + }) + await wait(() => { + expect(queryByTestId('input').getAttribute('value')).toEqual('aa change') + expect(actions.getFormGraph()).toMatchSnapshot() + expect(actions.getFormState(state => state.values)).toEqual({ + aa: 'aa change' + }) + expect(actions.getFormSchema()).toMatchSnapshot() + }) + }) +}) + +describe('major scenes',()=>{ + //todo,核心场景回归 +}) + +describe('bugfix',()=>{ + //todo,问题修复回归 +}) \ No newline at end of file diff --git a/packages/react-schema-renderer/src/__tests__/register.spec.tsx b/packages/react-schema-renderer/src/__tests__/register.spec.tsx new file mode 100644 index 00000000000..a72934ce55a --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/register.spec.tsx @@ -0,0 +1,166 @@ +import React from 'react' +import { + registerFormField, + registerFieldMiddleware, + registerVirtualBox, + connect, + SchemaForm, + createFormActions, + cleanRegistry +} from '../index' +import { render, fireEvent, wait } from '@testing-library/react' + +describe('test all apis', () => { + afterEach(() => { + cleanRegistry() + }) + test('registerFormField', async () => { + registerFormField( + 'string', + connect()(props => { + return + }) + ) + + const actions = createFormActions() + + const { queryByTestId } = render( + + ) + expect(queryByTestId('input').getAttribute('value')).toEqual('123') + fireEvent.change(queryByTestId('input'), { + target: { + value: 'aa change' + } + }) + await wait(() => { + expect(queryByTestId('input').getAttribute('value')).toEqual('aa change') + expect(actions.getFormGraph()).toMatchSnapshot() + }) + }) + + test('registerFieldMiddleware', async () => { + registerFormField( + 'string', + connect()(props => { + return + }) + ) + + registerFieldMiddleware(FieldComponent => props => { + return ( +
    + this is wrapper + +
    + ) + }) + + const actions = createFormActions() + + const { queryByTestId, queryByText } = render( + + ) + expect(queryByTestId('input').getAttribute('value')).toEqual('123') + fireEvent.change(queryByTestId('input'), { + target: { + value: 'aa change' + } + }) + await wait(() => { + expect(queryByTestId('input').getAttribute('value')).toEqual('aa change') + expect(actions.getFormGraph()).toMatchSnapshot() + expect(queryByText('this is wrapper')).toBeTruthy() + }) + }) + + test('registerVirtualBox', async () => { + registerFormField( + 'string', + connect()(props => { + return + }) + ) + + registerVirtualBox('box', props => { + return
    this is VirtualBox.{props.children}
    + }) + + const actions = createFormActions() + + const { queryByTestId, queryByText } = render( + + ) + expect(queryByTestId('input').getAttribute('value')).toEqual('123') + fireEvent.change(queryByTestId('input'), { + target: { + value: 'aa change' + } + }) + await wait(() => { + expect(queryByTestId('input').getAttribute('value')).toEqual('aa change') + expect(actions.getFormGraph()).toMatchSnapshot() + expect(actions.getFormState(state => state.values)).toEqual({ + aa: 'aa change' + }) + expect(queryByText('this is VirtualBox.')).toBeTruthy() + }) + }) +}) + +describe('major scenes',()=>{ + //todo +}) + + +describe('bugfix',()=>{ + //todo +}) \ No newline at end of file diff --git a/packages/react-schema-renderer/src/components/SchemaField.tsx b/packages/react-schema-renderer/src/components/SchemaField.tsx new file mode 100644 index 00000000000..ae1b6110bde --- /dev/null +++ b/packages/react-schema-renderer/src/components/SchemaField.tsx @@ -0,0 +1,144 @@ +import React, { useContext, Fragment } from 'react' +import { Field, VirtualField } from '@uform/react' +import { FormPath, isFn, isStr } from '@uform/shared' +import { + ISchemaFieldProps, + ISchemaFieldComponentProps, + ISchemaVirtualFieldComponentProps +} from '../types' +import { Schema } from '../shared/schema' +import SchemaContext, { FormComponentsContext } from '../shared/context' + +export const SchemaField: React.FunctionComponent = ( + props: ISchemaFieldProps +) => { + const path = FormPath.parse(props.path) + const formSchema = useContext(SchemaContext) + const fieldSchema = formSchema.get(path) + const formRegistry = useContext(FormComponentsContext) + if (!fieldSchema) { + throw new Error(`Can not found schema node by ${path.toString()}.`) + } + if (!formRegistry) { + throw new Error(`Can not found any form components.`) + } + const schemaType = fieldSchema.type + const schemaComponent = fieldSchema.getExtendsComponent() + const schemaRenderer = fieldSchema.getExtendsRenderer() + const finalComponentName = schemaComponent || schemaType + const renderField = ( + addtionKey: string | number, + reactKey?: string | number + ) => { + return + } + const renderChildren = ( + callback: (props: ISchemaFieldComponentProps) => React.ReactElement + ) => { + return ( + + {({ state, mutators, form }) => { + const props: ISchemaFieldComponentProps = { + ...state, + schema: fieldSchema, + form, + mutators, + renderField + } + return callback(props) + }} + + ) + } + if (fieldSchema.isObject() && !schemaComponent) { + const properties = fieldSchema.mapProperties( + (schema: Schema, key: string) => { + const childPath = path.concat(key) + return + } + ) + if (path.length == 0) { + return {properties} + } + return renderChildren(props => { + return React.createElement( + formRegistry.formItemComponent, + props, + properties + ) + }) + } else { + if (isFn(finalComponentName)) { + return renderChildren(props => { + return React.createElement( + formRegistry.formItemComponent, + props, + React.createElement(finalComponentName, props) + ) + }) + } else if (isStr(finalComponentName)) { + if (formRegistry.fields[finalComponentName]) { + return renderChildren(props => { + const renderComponent = (): React.ReactElement => + React.createElement( + formRegistry.formItemComponent, + props, + React.createElement( + formRegistry.fields[finalComponentName], + props + ) + ) + if (isFn(schemaRenderer)) { + return schemaRenderer({ ...props, renderComponent }) + } + return renderComponent() + }) + } else if (formRegistry.virtualFields[finalComponentName]) { + return ( + + {({ state, form }) => { + const props: ISchemaVirtualFieldComponentProps = { + ...state, + schema: fieldSchema, + form, + renderField, + children: fieldSchema.mapProperties( + (schema: Schema, key: string) => { + const childPath = path.concat(key) + return ( + + ) + } + ) + } + const renderComponent = () => + React.createElement( + formRegistry.virtualFields[finalComponentName], + props + ) + if (isFn(schemaRenderer)) { + return schemaRenderer({ ...props, renderComponent }) + } + return renderComponent() + }} + + ) + } else { + throw new Error( + `Can not found any custom component in ${path.toString()}.` + ) + } + } + } +} diff --git a/packages/react-schema-renderer/src/components/SchemaForm.tsx b/packages/react-schema-renderer/src/components/SchemaForm.tsx new file mode 100644 index 00000000000..c31870b6e10 --- /dev/null +++ b/packages/react-schema-renderer/src/components/SchemaForm.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { ISchemaFormProps } from '../types' +import { Form } from '@uform/react' +import { SchemaField } from './SchemaField' +import { useSchemaForm } from '../hooks/useSchemaForm' +import SchemaContext, { FormComponentsContext } from '../shared/context' + +export const SchemaForm: React.FC = props => { + const { + fields, + virtualFields, + formComponent, + formItemComponent, + formComponentProps, + schema, + form, + children + } = useSchemaForm(props) + return ( + + + + {React.createElement( + formComponent, + { + ...formComponentProps, + onSubmit: () => { + form.submit() + }, + onReset: () => { + form.reset({ validate: false, forceClear: false }) + } + }, + , + children + )} + + + + ) +} + +SchemaForm.defaultProps = { + schema: {} +} + +export default SchemaForm diff --git a/packages/react-schema-renderer/src/components/SchemaMarkup.tsx b/packages/react-schema-renderer/src/components/SchemaMarkup.tsx new file mode 100644 index 00000000000..684988bb099 --- /dev/null +++ b/packages/react-schema-renderer/src/components/SchemaMarkup.tsx @@ -0,0 +1,131 @@ +import React, { Fragment, createContext, useContext } from 'react' +import { registerVirtualBox } from '../shared/registry' +import { SchemaForm } from './SchemaForm' +import { Schema } from '../shared/schema' +import { render } from '../shared/virtual-render' +import { + ISchemaFormProps, + IMarkupSchemaFieldProps, + ISchemaVirtualFieldComponentProps +} from '../types' + +const env = { + nonameId: 0 +} + +const MarkupContext = createContext(null) + +const getRadomName = () => { + return `NO_NAME_FIELD_$${env.nonameId++}` +} + +export const SchemaMarkupField: React.FC = ({ + name, + children, + ...props +}) => { + const parentSchema = useContext(MarkupContext) + if (!parentSchema) return + if (parentSchema.isObject()) { + const propName = name || getRadomName() + const schema = parentSchema.setProperty(propName, props) + return ( + {children} + ) + } else if (parentSchema.isArray()) { + const schema = parentSchema.setArrayItems(props) + return ( + {children} + ) + } else { + return (children as React.ReactElement) || + } +} + +SchemaMarkupField.displayName = 'SchemaMarkupField' + +export const SchemaMarkupForm: React.FC = props => { + let alreadyHasSchema = false + let finalSchema: Schema + if (props.schema) { + alreadyHasSchema = true + finalSchema = new Schema(props.schema) + } else { + finalSchema = new Schema({ type: 'object' }) + } + env.nonameId = 0 + return ( + + {!alreadyHasSchema && + render( + + {props.children} + + )} + + + ) +} + +SchemaMarkupForm.displayName = 'SchemaMarkupForm' + +export function createVirtualBox( + key: string, + component?: React.JSXElementConstructor +) { + registerVirtualBox( + key, + component + ? ({ props, children }) => { + return React.createElement(component, { + ...props['x-props'], + ...props['x-component-props'], + children + }) + } + : () => + ) + const VirtualBox: React.FC = ({ + children, + name, + ...props + }) => { + return ( + + {children} + + ) + } + return VirtualBox +} + +export function createControllerBox( + key: string, + component?: React.JSXElementConstructor +) { + registerVirtualBox(key, component ? component : () => ) + const VirtualBox: React.FC = ({ + children, + name, + ...props + }) => { + return ( + + {children} + + ) + } + return VirtualBox +} diff --git a/packages/react-schema-renderer/src/hooks/useSchemaForm.ts b/packages/react-schema-renderer/src/hooks/useSchemaForm.ts new file mode 100644 index 00000000000..1bd1c998523 --- /dev/null +++ b/packages/react-schema-renderer/src/hooks/useSchemaForm.ts @@ -0,0 +1,71 @@ +import { useMemo, useRef } from 'react' +import { useForm } from '@uform/react' +import { Schema } from '../shared/schema' +import { deprecate } from '@uform/shared' +import { useEva } from 'react-eva' +import { ISchemaFormProps } from '../types' +import { createSchemaFormActions } from '../shared/actions' +import { getRegistry } from '../shared/registry' + +const useInternalSchemaForm = (props: ISchemaFormProps) => { + const { + fields, + virtualFields, + formComponent, + formItemComponent, + component, + schema, + value, + initialValues, + actions, + effects, + onChange, + onSubmit, + onReset, + onValidateFailed, + useDirty, + children, + editable, + validateFirst, + ...formComponentProps + } = props + const { implementActions } = useEva({ + actions + }) + const registry = getRegistry() + return { + form: useForm(props), + formComponentProps, + fields: { + ...registry.fields, + ...fields + }, + virtualFields: { + ...registry.virtualFields, + ...virtualFields + }, + formComponent: formComponent ? formComponent : registry.formComponent, + formItemComponent: formItemComponent + ? formItemComponent + : registry.formItemComponent, + schema: useMemo(() => { + const result = new Schema(schema) + implementActions({ + getSchema: deprecate(() => result, 'Please use the getFormSchema.'), + getFormSchema: () => result + }) + return result + }, [schema]), + children + } +} + +export const useSchemaForm = (props: ISchemaFormProps) => { + const actionsRef = useRef(null) + actionsRef.current = + actionsRef.current || props.actions || createSchemaFormActions() + return useInternalSchemaForm({ + ...props, + actions: actionsRef.current + }) +} diff --git a/packages/react-schema-renderer/src/index.tsx b/packages/react-schema-renderer/src/index.tsx new file mode 100644 index 00000000000..118b190a965 --- /dev/null +++ b/packages/react-schema-renderer/src/index.tsx @@ -0,0 +1,16 @@ +import { + createAsyncSchemaFormActions, + createSchemaFormActions +} from './shared/actions' +export * from '@uform/react' +export * from './components/SchemaField' +export * from './components/SchemaForm' +export * from './components/SchemaMarkup' +export * from './hooks/useSchemaForm' +export * from './shared/connect' +export * from './shared/registry' +export * from './shared/schema' +export * from './types' + +export const createFormActions = createSchemaFormActions +export const createAsyncFormActions = createAsyncSchemaFormActions diff --git a/packages/react-schema-renderer/src/shared/actions.ts b/packages/react-schema-renderer/src/shared/actions.ts new file mode 100644 index 00000000000..7f3fff6d0a0 --- /dev/null +++ b/packages/react-schema-renderer/src/shared/actions.ts @@ -0,0 +1,15 @@ +import { createFormActions, createAsyncFormActions } from '@uform/react' +import { mergeActions, createActions, createAsyncActions } from 'react-eva' +import { ISchemaFormActions, ISchemaFormAsyncActions } from '../types' + +export const createSchemaFormActions = (): ISchemaFormActions => + mergeActions( + createFormActions(), + createActions('getSchema', 'getFormSchema') + ) as ISchemaFormActions + +export const createAsyncSchemaFormActions = (): ISchemaFormAsyncActions => + mergeActions( + createAsyncFormActions(), + createAsyncActions('getSchema', 'getFormSchema') + ) as ISchemaFormAsyncActions diff --git a/packages/react-schema-renderer/src/shared/connect.ts b/packages/react-schema-renderer/src/shared/connect.ts new file mode 100644 index 00000000000..159fcac127c --- /dev/null +++ b/packages/react-schema-renderer/src/shared/connect.ts @@ -0,0 +1,123 @@ +import React from 'react' +import { isArr, each, isFn } from '@uform/shared' +import { + ISchema, + IConnectOptions, + ISchemaFieldComponentProps, + IConnectProps +} from '../types' + +const createEnum = (enums: any) => { + if (isArr(enums)) { + return enums.map(item => { + if (typeof item === 'object') { + return { + ...item + } + } else { + return { + ...item, + label: item, + value: item + } + } + }) + } + + return [] +} + +const bindEffects = ( + props: {}, + effect: ISchema['x-effect'], + notify: (type: string, payload?: any) => void +): any => { + each(effect(notify, { ...props }), (event, key) => { + const prevEvent = key === 'onChange' ? props[key] : undefined + props[key] = (...args: any[]) => { + if (isFn(prevEvent)) { + prevEvent(...args) + } + if (isFn(event)) { + return event(...args) + } + } + }) + return props +} + +export const connect = (options?: IConnectOptions) => { + options = { + valueName: 'value', + eventName: 'onChange', + ...options + } + return (Component: React.JSXElementConstructor) => { + return (fieldProps: ISchemaFieldComponentProps) => { + const { + value, + name, + mutators, + form, + schema, + editable, + props + } = fieldProps + let componentProps: IConnectProps = { + ...options.defaultProps, + ...props['x-props'], + ...props['x-component-props'], + [options.valueName]: value, + [options.eventName]: (event: any, ...args: any[]) => { + mutators.change( + options.getValueFromEvent + ? options.getValueFromEvent.call(schema, event, ...args) + : event, + ...args + ) + }, + onBlur: () => mutators.blur(), + onFocus: () => mutators.focus() + } + if (editable !== undefined) { + if (isFn(editable)) { + if (!editable(name)) { + componentProps.disabled = true + componentProps.readOnly = true + } + } else if (editable === false) { + componentProps.disabled = true + componentProps.readOnly = true + } + } + + const extendsEffect = schema.getExtendsEffect() + + if (isFn(extendsEffect)) { + componentProps = bindEffects(componentProps, extendsEffect, form.notify) + } + + if (isFn(options.getProps)) { + const newProps = options.getProps(componentProps, fieldProps) + if (newProps !== undefined) { + componentProps = newProps as any + } + } + + if (isArr(schema.enum) && !componentProps.dataSource) { + componentProps.dataSource = createEnum(schema.enum) + } + + if (componentProps.editable !== undefined) { + delete componentProps.editable + } + + return React.createElement( + isFn(options.getComponent) + ? options.getComponent(Component, props, fieldProps) + : Component, + componentProps + ) + } + } +} diff --git a/packages/react-schema-renderer/src/shared/context.ts b/packages/react-schema-renderer/src/shared/context.ts new file mode 100644 index 00000000000..29b52885b26 --- /dev/null +++ b/packages/react-schema-renderer/src/shared/context.ts @@ -0,0 +1,7 @@ +import { createContext } from 'react' +import { Schema } from './schema' +import { ISchemaFormRegistry } from '../types' + +export const FormComponentsContext = createContext(null) + +export default createContext(null) diff --git a/packages/react-schema-renderer/src/shared/registry.ts b/packages/react-schema-renderer/src/shared/registry.ts new file mode 100644 index 00000000000..5ffcadc5749 --- /dev/null +++ b/packages/react-schema-renderer/src/shared/registry.ts @@ -0,0 +1,134 @@ +import { isFn, lowercase, reduce, each, deprecate } from '@uform/shared' +import { + ComponentWithStyleComponent, + ISchemaFieldWrapper, + ISchemaFormRegistry, + ISchemaFieldComponent, + ISchemaFieldComponentProps, + ISchemaVirtualFieldComponentProps +} from '../types' +import pascalCase from 'pascal-case' + +const registry: ISchemaFormRegistry = { + fields: {}, + virtualFields: {}, + wrappers: [], + formItemComponent: ({ children }) => children, + formComponent: 'form' +} + +export const getRegistry = () => { + return { + fields: registry.fields, + virtualFields: registry.virtualFields, + formItemComponent: registry.formItemComponent, + formComponent: registry.formComponent + } +} + +export const cleanRegistry = () => { + registry.fields = {} + registry.virtualFields = {} + registry.wrappers = [] +} + +export function registerFormComponent( + component: React.JSXElementConstructor +) { + if (isFn(component)) { + registry.formComponent = component + } +} + +function compose(payload: T, args: P[], revert: boolean) { + return reduce( + args, + (buf: T, fn: P) => { + return isFn(fn) ? fn(buf) : buf + }, + payload, + revert + ) +} + +export function registerFormField( + name: string, + component: ComponentWithStyleComponent, + noWrapper: boolean = false +) { + if ( + name && + (isFn(component) || typeof component.styledComponentId === 'string') + ) { + name = lowercase(name) + if (noWrapper) { + registry.fields[name] = component + registry.fields[name].__WRAPPERS__ = [] + } else { + registry.fields[name] = compose( + component, + registry.wrappers, + true + ) + registry.fields[name].__WRAPPERS__ = registry.wrappers + } + registry.fields[name].displayName = pascalCase(name) + } +} + +export function registerFormFields(object: ISchemaFormRegistry['fields']) { + each( + object, + (component, key) => { + registerFormField(key, component) + } + ) +} + +export function registerVirtualBox( + name: string, + component: ComponentWithStyleComponent +) { + if ( + name && + (isFn(component) || typeof component.styledComponentId === 'string') + ) { + name = lowercase(name) + registry.virtualFields[name] = component + registry.virtualFields[name].displayName = pascalCase(name) + } +} + +export function registerFormItemComponent( + component: React.JSXElementConstructor +) { + if (isFn(component)) { + registry.formItemComponent = component + } +} + +type FieldMiddleware = ISchemaFieldWrapper + +export const registerFieldMiddleware = deprecate< + FieldMiddleware, + FieldMiddleware, + FieldMiddleware +>(function registerFieldMiddleware( + ...wrappers: ISchemaFieldWrapper[] +) { + each( + registry.fields, + (component, key) => { + if ( + !component.__WRAPPERS__.some(wrapper => wrappers.indexOf(wrapper) > -1) + ) { + registry.fields[key] = compose( + registry.fields[key], + wrappers, + true + ) + registry.fields[key].__WRAPPERS__ = wrappers + } + } + ) +}) diff --git a/packages/react-schema-renderer/src/shared/schema.ts b/packages/react-schema-renderer/src/shared/schema.ts new file mode 100644 index 00000000000..facc840ba7e --- /dev/null +++ b/packages/react-schema-renderer/src/shared/schema.ts @@ -0,0 +1,392 @@ +import React from 'react' +import { + ValidatePatternRules, + ValidateDescription, + CustomValidator, + getMessage +} from '@uform/validator' +import { + lowercase, + map, + each, + isEmpty, + isEqual, + isArr, + toArr, + isBool, + isValid, + FormPathPattern, + FormPath +} from '@uform/shared' +import { ISchemaFieldComponentProps, SchemaMessage, ISchema } from '../types' + +const numberRE = /^\d+$/ + +type SchemaProperties = { + [key: string]: Schema +} + +export class Schema implements ISchema { + /** base json schema spec**/ + public title?: SchemaMessage + public description?: SchemaMessage + public default?: any + public readOnly?: boolean + public writeOnly?: boolean + public type?: 'string' | 'object' | 'array' | 'number' | string + public enum?: Array + public const?: any + public multipleOf?: number + public maximum?: number + public exclusiveMaximum?: number + public minimum?: number + public exclusiveMinimum?: number + public maxLength?: number + public minLength?: number + public pattern?: string | RegExp + public maxItems?: number + public minItems?: number + public uniqueItems?: boolean + public maxProperties?: number + public minProperties?: number + public required?: string[] | boolean + public format?: string + /** nested json schema spec **/ + public properties?: SchemaProperties + public items?: Schema | Schema[] + public additionalItems?: Schema + public patternProperties?: { + [key: string]: Schema + } + public additionalProperties?: Schema + /** extend json schema specs */ + public editable?: boolean + public ['x-props']?: { [name: string]: any } + public ['x-index']?: number + public ['x-rules']?: ValidatePatternRules + public ['x-component']?: string | React.JSXElementConstructor + public ['x-component-props']?: { [name: string]: any } + public ['x-render']?: ( + props: T & { + renderComponent: () => React.ReactElement + } + ) => React.ReactElement + public ['x-effect']?: ( + dispatch: (type: string, payload: any) => void, + option?: object + ) => { [key: string]: any } + /** schema class self specs**/ + + public parent?: Schema + + public _isJSONSchemaObject = true + + constructor(json: ISchema, parent?: Schema) { + if (parent) { + this.parent = parent + } + return this.fromJSON(json) as any + } + /** + * getters + */ + get(path?: FormPathPattern) { + if (!path) { + return this + } + let res: Schema = this + let suc = 0 + path = FormPath.parse(path) + path.forEach(key => { + if (res && !isEmpty(res.properties)) { + res = res.properties[key] + suc++ + } else if (res && !isEmpty(res.items) && numberRE.test(key as string)) { + res = isArr(res.items) ? res.items[key] : res.items + suc++ + } + }) + return suc === path.length ? res : undefined + } + + getEmptyValue() { + if (this.type === 'string') { + return '' + } + if (this.type === 'array') { + return [] + } + if (this.type === 'object') { + return {} + } + if (this.type === 'number') { + return 0 + } + } + + getSelfProps() { + const { + _isJSONSchemaObject, + properties, + additionalProperties, + additionalItems, + patternProperties, + items, + parent, + ...props + } = this + return props + } + getExtendsRules() { + let rules: Array = [] + if (this.format) { + rules.push({ format: this.format }) + } + if (isValid(this.maxItems)) { + rules.push({ max: this.maxItems }) + } + if (isValid(this.maxLength)) { + rules.push({ max: this.maxLength }) + } + if (isValid(this.maximum)) { + rules.push({ maximum: this.maximum }) + } + if (isValid(this.minimum)) { + rules.push({ minimum: this.minimum }) + } + if (isValid(this.exclusiveMaximum)) { + rules.push({ exclusiveMaximum: this.exclusiveMaximum }) + } + if (isValid(this.exclusiveMinimum)) { + rules.push({ exclusiveMinimum: this.exclusiveMinimum }) + } + if (isValid(this.pattern)) { + rules.push({ pattern: this.pattern }) + } + if (isValid(this.const)) { + rules.push({ + validator: value => { + return value === this.const ? '' : getMessage('schema.const') + } + }) + } + if (isValid(this.multipleOf)) { + rules.push({ + validator: value => { + return value % this.multipleOf === 0 + ? '' + : getMessage('schema.multipleOf') + } + }) + } + if (isValid(this.maxProperties)) { + rules.push({ + validator: value => { + return Object.keys(value || {}).length <= this.maxProperties + ? '' + : getMessage('schema.maxProperties') + } + }) + } + if (isValid(this.minProperties)) { + rules.push({ + validator: value => { + return Object.keys(value || {}).length >= this.minProperties + ? '' + : getMessage('schema.minProperties') + } + }) + } + if (isValid(this.uniqueItems) && this.uniqueItems) { + rules.push({ + validator: value => { + value = toArr(value) + return value.some((item: any, index: number) => { + for (let start = index; start < value.length; start++) { + if (isEqual(value[start], item)) { + return false + } + } + }) + ? getMessage('schema.uniqueItems') + : '' + } + }) + } + /**剩余校验的都是关联型复杂校验,不抹平,让用户自己处理 */ + if (isValid(this['x-rules'])) { + rules = rules.concat(this['x-rules']) + } + + return rules + } + getExtendsRequired() { + if (isBool(this.required)) { + return this.required + } + } + getExtendsEditable() { + if (isValid(this.editable)) { + return this.editable + } else if (isValid(this['x-props'] && this['x-props'].editable)) { + return this['x-props'].editable + } else if (isValid(this.readOnly)) { + return !this.readOnly + } + } + getExtendsTriggerType() { + const itemProps = this.getExtendsItemProps() + const props = this.getExtendsProps() + const componentProps = this.getExtendsComponentProps() + if (itemProps.triggerType) { + return itemProps.triggerType + } else if (props.triggerType) { + return props.triggerType + } else if (componentProps.triggerType) { + return componentProps.triggerType + } + } + getExtendsItemProps() { + return this['x-item-props'] || {} + } + getExtendsComponent() { + return this['x-component'] + } + getExtendsRenderer() { + return this['x-render'] + } + getExtendsEffect() { + return this['x-effect'] + } + getExtendsProps() { + return this['x-props'] || {} + } + getExtendsComponentProps() { + return { ...this['x-props'], ...this['x-component-props'] } + } + /** + * getters + */ + setProperty(key: string, schema: ISchema) { + this.properties = this.properties || {} + this.properties[key] = new Schema(schema, this) + return this.properties[key] + } + setProperties(properties: SchemaProperties) { + each, ISchema>(properties, (schema, key) => { + this.setProperty(key, schema) + }) + return this.properties + } + setArrayItems(schema: ISchema) { + this.items = new Schema(schema, this) + return this.items + } + toJSON() { + const result: ISchema = this.getSelfProps() + if (isValid(this.properties)) { + result.properties = map(this.properties, schema => { + return schema.toJSON() + }) + } + if (isValid(this.items)) { + result.items = isArr(this.items) + ? this.items.map(schema => schema.toJSON()) + : this.items.toJSON() + } + if (isValid(this.additionalItems)) { + result.additionalItems = this.additionalItems.toJSON() + } + if (isValid(this.additionalProperties)) { + result.additionalProperties = this.additionalProperties.toJSON() + } + if (isValid(this.patternProperties)) { + result.patternProperties = map(this.patternProperties, schema => { + return schema.toJSON() + }) + } + return result + } + + fromJSON(json: ISchema = {}) { + if (typeof json === 'boolean') return json + if (json instanceof Schema) return json + Object.assign(this, json) + if (isValid(json.type)) { + this.type = lowercase(String(json.type)) + } + if (isValid(json['x-component'])) { + this['x-component'] = lowercase(json['x-component']) + } + if (!isEmpty(json.properties)) { + this.properties = map(json.properties, item => { + return new Schema(item, this) + }) + if (isValid(json.additionalProperties)) { + this.additionalProperties = new Schema(json.additionalProperties, this) + } + if (isValid(json.patternProperties)) { + this.patternProperties = map(json.patternProperties, item => { + return new Schema(item, this) + }) + } + } else if (!isEmpty(json.items)) { + this.items = isArr(json.items) + ? map(json.items, item => new Schema(item, this)) + : new Schema(json.items) + if (isValid(json.additionalItems)) { + this.additionalItems = new Schema(json.additionalItems, this) + } + } + return this + } + /** + * tools + */ + isObject() { + return this.type === 'object' + } + isArray() { + return this.type === 'array' + } + + mapProperties(callback?: (schema: Schema, key: string) => any) { + return this.getOrderProperties().map(({ schema, key }) => { + return callback(schema, key) + }) + } + + getOrderProperties() { + return Schema.getOrderProperties(this) + } + + getOrderPatternProperties() { + return Schema.getOrderProperties(this, 'patternProperties') + } + + mapPatternProperties(callback?: (schema: Schema, key: string) => any) { + return this.getOrderPatternProperties().map(({ schema, key }) => { + return callback(schema, key) + }) + } + + static getOrderProperties = ( + schema: ISchema = {}, + propertiesName: string = 'properties' + ) => { + const newSchema = new Schema(schema) + const properties = [] + each(newSchema[propertiesName], (item, key) => { + const index = item['x-index'] + if (typeof index === 'number') { + properties[index] = { + schema: item, + key + } + } else { + properties.push({ schema: item, key }) + } + }) + return properties + } +} diff --git a/packages/react-schema-renderer/src/shared/virtual-render.tsx b/packages/react-schema-renderer/src/shared/virtual-render.tsx new file mode 100644 index 00000000000..671550f921b --- /dev/null +++ b/packages/react-schema-renderer/src/shared/virtual-render.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { globalThisPolyfill } from '@uform/shared' + +const env = { + portalDOM: null +} + +export const render = (element: React.ReactElement) => { + if (globalThisPolyfill['document']) { + env.portalDOM = + env.portalDOM || globalThisPolyfill['document'].createElement('div') + return require('react-dom').createPortal(element, env.portalDOM) + } else { + return + } +} diff --git a/packages/react-schema-renderer/src/types.ts b/packages/react-schema-renderer/src/types.ts new file mode 100644 index 00000000000..6ffedd1265a --- /dev/null +++ b/packages/react-schema-renderer/src/types.ts @@ -0,0 +1,180 @@ +import React from 'react' +import { FormPathPattern } from '@uform/shared' +import { + IFieldState, + IVirtualFieldState, + IMutators, + IFormProps, + IForm, + IFormActions, + IFormAsyncActions +} from '@uform/react' +import { ValidatePatternRules } from '@uform/validator' +import { Schema } from './shared/schema' +export interface ISchemaFieldProps { + path?: FormPathPattern +} + +export type ComponentWithStyleComponent< + ComponentProps +> = React.JSXElementConstructor & { + styledComponentId?: string + displayName?: string +} + +export interface ISchemaFieldComponentProps extends IFieldState { + schema: Schema + mutators: IMutators + form: IForm + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +export interface ISchemaVirtualFieldComponentProps extends IVirtualFieldState { + schema: Schema + form: IForm + children: React.ReactElement[] + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} + +export interface ISchemaFieldWrapper { + (Traget: ISchemaFieldComponent): + | React.FC + | React.ClassicComponent +} + +export type ISchemaFieldComponent = ComponentWithStyleComponent< + ISchemaFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} + +export type ISchemaVirtualFieldComponent = ComponentWithStyleComponent< + ISchemaVirtualFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} + +export interface ISchemaFormRegistry { + fields: { + [key: string]: ISchemaFieldComponent + } + virtualFields: { + [key: string]: ISchemaVirtualFieldComponent + } + wrappers?: ISchemaFieldWrapper[] + formItemComponent: React.JSXElementConstructor + formComponent: string | React.JSXElementConstructor +} + +export type SchemaMessage = React.ReactNode + +export interface ISchema { + /** base json schema spec**/ + title?: SchemaMessage + description?: SchemaMessage + default?: any + readOnly?: boolean + writeOnly?: boolean + type?: 'string' | 'object' | 'array' | 'number' | string + enum?: Array + const?: any + multipleOf?: number + maximum?: number + exclusiveMaximum?: number + minimum?: number + exclusiveMinimum?: number + maxLength?: number + minLength?: number + pattern?: string | RegExp + maxItems?: number + minItems?: number + uniqueItems?: boolean + maxProperties?: number + minProperties?: number + required?: string[] | boolean + format?: string + /** nested json schema spec **/ + properties?: { + [key: string]: ISchema + } + items?: ISchema | ISchema[] + additionalItems?: ISchema + patternProperties?: { + [key: string]: ISchema + } + additionalProperties?: ISchema + /** extend json schema specs */ + editable?: boolean + ['x-props']?: { [name: string]: any } + ['x-index']?: number + ['x-rules']?: ValidatePatternRules + ['x-component']?: string | React.JSXElementConstructor + ['x-component-props']?: { [name: string]: any } + ['x-render']?: ( + props: T & { + renderComponent: () => React.ReactElement + } + ) => React.ReactElement + ['x-effect']?: ( + dispatch: (type: string, payload: any) => void, + option?: object + ) => { [key: string]: any } +} + +export interface ISchemaFormProps + extends IFormProps< + any, + any, + any, + ISchemaFormActions | ISchemaFormAsyncActions + > { + schema?: ISchema + component?: string | React.JSXElementConstructor + fields?: ISchemaFormRegistry['fields'] + virtualFields?: ISchemaFormRegistry['virtualFields'] + formComponent?: ISchemaFormRegistry['formComponent'] + formItemComponent?: ISchemaFormRegistry['formItemComponent'] +} + +export interface IMarkupSchemaFieldProps extends ISchema { + name?: string +} + +export type MergedFieldComponentProps = Partial< + ISchemaFieldComponentProps & ISchemaVirtualFieldComponentProps +> + +export interface IConnectOptions { + valueName?: string + eventName?: string + defaultProps?: {} + getValueFromEvent?: (event?: any, value?: any) => any + getProps?: ( + componentProps: {}, + fieldProps: MergedFieldComponentProps + ) => {} | void + getComponent?: ( + Target: any, + componentProps: {}, + fieldProps: MergedFieldComponentProps + ) => React.JSXElementConstructor +} + +export interface IConnectProps { + [key: string]: any +} + +export interface ISchemaFormActions extends IFormActions { + getSchema(): Schema + getFormSchema(): Schema +} + +export interface ISchemaFormAsyncActions extends IFormAsyncActions { + getSchema(): Promise + getFormSchema(): Promise +} diff --git a/packages/react-schema-renderer/tsconfig.json b/packages/react-schema-renderer/tsconfig.json new file mode 100644 index 00000000000..1d669c29c46 --- /dev/null +++ b/packages/react-schema-renderer/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./lib" + }, + "include": ["./src/**/*.ts", "./src/**/*.tsx"], + "exclude": ["./src/__tests__/*"] +} diff --git a/packages/types/.npmignore b/packages/react-shared-components/.npmignore similarity index 100% rename from packages/types/.npmignore rename to packages/react-shared-components/.npmignore diff --git a/packages/builder-next/LICENSE.md b/packages/react-shared-components/LICENSE.md similarity index 100% rename from packages/builder-next/LICENSE.md rename to packages/react-shared-components/LICENSE.md diff --git a/packages/react-shared-components/README.md b/packages/react-shared-components/README.md new file mode 100644 index 00000000000..0e8a2b774af --- /dev/null +++ b/packages/react-shared-components/README.md @@ -0,0 +1,2 @@ +# @uform/react-shared +> UForm React通用库 \ No newline at end of file diff --git a/packages/utils/jest.config.js b/packages/react-shared-components/jest.config.js similarity index 100% rename from packages/utils/jest.config.js rename to packages/react-shared-components/jest.config.js diff --git a/packages/utils/package.json b/packages/react-shared-components/package.json similarity index 83% rename from packages/utils/package.json rename to packages/react-shared-components/package.json index c2bb19778ca..a479c36aa14 100644 --- a/packages/utils/package.json +++ b/packages/react-shared-components/package.json @@ -1,6 +1,6 @@ { - "name": "@uform/utils", - "version": "0.4.3", + "name": "@uform/react-shared-components", + "version": "0.4.0", "license": "MIT", "main": "lib", "types": "lib/index.d.ts", @@ -29,7 +29,7 @@ "@babel/runtime": "^7.4.4" }, "dependencies": { - "@uform/types": "^0.4.3", - "camel-case": "^3.0.0" + "@uform/types": "^0.4.0", + "@uform/shared": "^0.4.0" } } diff --git a/packages/react-shared-components/src/ArrayList.tsx b/packages/react-shared-components/src/ArrayList.tsx new file mode 100644 index 00000000000..abf09c3f0b2 --- /dev/null +++ b/packages/react-shared-components/src/ArrayList.tsx @@ -0,0 +1,232 @@ +import React, { createContext, useContext, Fragment, useMemo } from 'react' +import { isNum, isFn, toArr } from '@uform/shared' +import { IArrayList, IArrayListProps } from './types' + +const ArrayContext = createContext({}) + +export const ArrayList: IArrayList = props => { + return ( + + {props.children} + + ) +} + +const useArrayList = (index: number = 0) => { + const { + value, + disabled, + editable, + minItems, + maxItems, + renders, + ...props + } = useContext(ArrayContext) + + const renderWith = ( + name: string, + render: (node: any) => React.ReactElement, + wrapper: any + ) => { + let children: any + if (renders && renders[name]) { + if (isFn(renders[name]) || renders[name].styledComponentId) { + children = renders[name](context.currentIndex) + } else { + children = render(renders[name]) + } + } else { + children = render(renders[name]) + } + if (isFn(wrapper)) { + return wrapper({ ...context, children }) || + } + return children || + } + + const newValue = toArr(value) + + const isEmpty = !newValue || (newValue && newValue.length <= 0) + const isDisable = disabled || editable === false + const allowMoveUp = newValue && newValue.length > 1 && !isDisable + const allowMoveDown = newValue && newValue.length > 1 && !isDisable + const allowRemove = isNum(minItems) ? newValue.length > minItems : !isDisable + const allowAddition = isNum(maxItems) + ? newValue.length <= maxItems + : !isDisable + + const context = { + ...props, + currentIndex: index, + isEmpty, + isDisable, + allowRemove, + allowAddition, + allowMoveDown, + allowMoveUp, + renderWith + } + + return context +} + +const useComponent = (name: string) => { + const { components } = useContext(ArrayContext) + return useMemo(() => { + if (isFn(components[name]) || components[name].styledComponentId) + return components[name] + return (props: {}) => { + return React.isValidElement(components[name]) ? ( + React.cloneElement(components[name], props) + ) : ( + + ) + } + }, []) +} + +const createButtonCls = (props: any = {}, hasText: any) => { + return { + className: `${hasText ? 'has-text' : ''} ${props.className || ''}` + } +} + +ArrayList.useArrayList = useArrayList +ArrayList.useComponent = useComponent + +ArrayList.Remove = ({ children, component, index, ...props }) => { + const { allowRemove, renderWith } = ArrayList.useArrayList(index) + const Button = ArrayList.useComponent(component) + const RemoveIcon = ArrayList.useComponent('RemoveIcon') + if (allowRemove) { + return renderWith( + 'renderRemove', + text => ( + + ), + children + ) + } + + return React.createElement(React.Fragment) +} + +ArrayList.Remove.defaultProps = { + component: 'CircleButton' +} + +ArrayList.Addition = ({ children, component, ...props }) => { + const { allowAddition, renderWith } = ArrayList.useArrayList() + const Button = ArrayList.useComponent(component) + const AdditionIcon = ArrayList.useComponent('AdditionIcon') + + if (allowAddition) { + return renderWith( + 'renderAddition', + text => ( + + ), + children + ) + } + return React.createElement(React.Fragment) +} + +ArrayList.Addition.defaultProps = { + component: 'TextButton' +} + +ArrayList.MoveUp = ({ children, component, index, ...props }) => { + const { allowMoveUp, renderWith } = ArrayList.useArrayList(index) + const Button = ArrayList.useComponent(component) + const MoveUpIcon = ArrayList.useComponent('MoveUpIcon') + + if (allowMoveUp) { + return renderWith( + 'renderMoveUp', + text => ( + + ), + children + ) + } + return React.createElement(React.Fragment) +} + +ArrayList.MoveUp.defaultProps = { + component: 'CircleButton' +} + +ArrayList.MoveDown = ({ children, component, index, ...props }) => { + const { allowMoveDown, renderWith } = ArrayList.useArrayList(index) + const Button = ArrayList.useComponent(component) + const MoveUpIcon = ArrayList.useComponent('MoveDownIcon') + + if (allowMoveDown) { + return renderWith( + 'renderMoveDown', + text => ( + + ), + children + ) + } + return React.createElement(React.Fragment) +} + +ArrayList.MoveDown.defaultProps = { + component: 'CircleButton' +} + +ArrayList.Empty = ({ children, component, ...props }) => { + const { allowAddition, isEmpty, renderWith } = ArrayList.useArrayList() + const Button = ArrayList.useComponent(component) + const AdditionIcon = ArrayList.useComponent('AdditionIcon') + let addtion: any + if (allowAddition) { + addtion = renderWith('renderAddition', text => ( + + )) + } + + if (isEmpty) { + return renderWith( + 'renderEmpty', + text => { + return ( + + + {text} + {addtion} + + ) + }, + children + ) + } + return React.createElement(React.Fragment) +} + +ArrayList.Empty.defaultProps = { + component: 'TextButton' +} diff --git a/packages/react-shared-components/src/PasswordStrength.tsx b/packages/react-shared-components/src/PasswordStrength.tsx new file mode 100644 index 00000000000..4495f10c30f --- /dev/null +++ b/packages/react-shared-components/src/PasswordStrength.tsx @@ -0,0 +1,156 @@ +import React, { Fragment } from 'react' +import { IPasswordStrengthProps } from './types' +import { isFn } from '@uform/shared' + +var isNum = function(c) { + return c >= 48 && c <= 57 +} +var isLower = function(c) { + return c >= 97 && c <= 122 +} +var isUpper = function(c) { + return c >= 65 && c <= 90 +} +var isSymbol = function(c) { + return !(isLower(c) || isUpper(c) || isNum(c)) +} +var isLetter = function(c) { + return isLower(c) || isUpper(c) +} + +const getStrength = val => { + if (!val) return 0 + let num = 0 + let lower = 0 + let upper = 0 + let symbol = 0 + let MNS = 0 + let rep = 0 + let repC = 0 + let consecutive = 0 + let sequential = 0 + const len = () => num + lower + upper + symbol + const callme = () => { + var re = num > 0 ? 1 : 0 + re += lower > 0 ? 1 : 0 + re += upper > 0 ? 1 : 0 + re += symbol > 0 ? 1 : 0 + if (re > 2 && len() >= 8) { + return re + 1 + } else { + return 0 + } + } + for (var i = 0; i < val.length; i++) { + var c = val.charCodeAt(i) + if (isNum(c)) { + num++ + if (i !== 0 && i !== val.length - 1) { + MNS++ + } + if (i > 0 && isNum(val.charCodeAt(i - 1))) { + consecutive++ + } + } else if (isLower(c)) { + lower++ + if (i > 0 && isLower(val.charCodeAt(i - 1))) { + consecutive++ + } + } else if (isUpper(c)) { + upper++ + if (i > 0 && isUpper(val.charCodeAt(i - 1))) { + consecutive++ + } + } else { + symbol++ + if (i !== 0 && i !== val.length - 1) { + MNS++ + } + } + var exists = false + for (var j = 0; j < val.length; j++) { + if (val[i] === val[j] && i !== j) { + exists = true + repC += Math.abs(val.length / (j - i)) + } + } + if (exists) { + rep++ + var unique = val.length - rep + repC = unique ? Math.ceil(repC / unique) : Math.ceil(repC) + } + if (i > 1) { + var last1 = val.charCodeAt(i - 1) + var last2 = val.charCodeAt(i - 2) + if (isLetter(c)) { + if (isLetter(last1) && isLetter(last2)) { + var v = val.toLowerCase() + var vi = v.charCodeAt(i) + var vi1 = v.charCodeAt(i - 1) + var vi2 = v.charCodeAt(i - 2) + if (vi - vi1 === vi1 - vi2 && Math.abs(vi - vi1) === 1) { + sequential++ + } + } + } else if (isNum(c)) { + if (isNum(last1) && isNum(last2)) { + if (c - last1 === last1 - last2 && Math.abs(c - last1) === 1) { + sequential++ + } + } + } else { + if (isSymbol(last1) && isSymbol(last2)) { + if (c - last1 === last1 - last2 && Math.abs(c - last1) === 1) { + sequential++ + } + } + } + } + } + let sum = 0 + let length = len() + sum += 4 * length + if (lower > 0) { + sum += 2 * (length - lower) + } + if (upper > 0) { + sum += 2 * (length - upper) + } + if (num !== length) { + sum += 4 * num + } + sum += 6 * symbol + sum += 2 * MNS + sum += 2 * callme() + if (length === lower + upper) { + sum -= length + } + if (length === num) { + sum -= num + } + sum -= repC + sum -= 2 * consecutive + sum -= 3 * sequential + sum = sum < 0 ? 0 : sum + sum = sum > 100 ? 100 : sum + + if (sum >= 80) { + return 100 + } else if (sum >= 60) { + return 80 + } else if (sum >= 40) { + return 60 + } else if (sum >= 20) { + return 40 + } else { + return 20 + } +} + +export const PasswordStrength: React.FC = props => { + if (isFn(props.children)) { + return props.children(getStrength(String(props.value))) + } else { + return {props.children} + } +} diff --git a/packages/react-shared-components/src/PreviewText.tsx b/packages/react-shared-components/src/PreviewText.tsx new file mode 100644 index 00000000000..b2ddca4e642 --- /dev/null +++ b/packages/react-shared-components/src/PreviewText.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import { IPreviewTextProps } from './types' + +export const PreviewText: React.FC = props => { + let value: any + if (props.dataSource && props.dataSource.length) { + let find = props.dataSource.filter(({ value }) => + Array.isArray(props.value) + ? props.value.some(val => val == value) + : props.value == value + ) + value = find.map(item => item.label).join(' , ') + } else { + value = Array.isArray(props.value) + ? props.value.join(' ~ ') + : String( + props.value === undefined || props.value === null ? '' : props.value + ) + } + return ( +

    + {props.addonBefore ? ' ' + props.addonBefore : ''} + {props.innerBefore ? ' ' + props.innerBefore : ''} + {props.addonTextBefore ? ' ' + props.addonTextBefore : ''} + {!value ? 'N/A' : value} + {props.addonTextAfter ? ' ' + props.addonTextAfter : ''} + {props.innerAfter ? ' ' + props.innerAfter : ''} + {props.addonAfter ? ' ' + props.addonAfter : ''} +

    + ) +} diff --git a/packages/react-shared-components/src/index.ts b/packages/react-shared-components/src/index.ts new file mode 100644 index 00000000000..c7a46558638 --- /dev/null +++ b/packages/react-shared-components/src/index.ts @@ -0,0 +1,4 @@ +export * from './ArrayList' +export * from './PreviewText' +export * from './PasswordStrength' +export * from './types' diff --git a/packages/react-shared-components/src/types.ts b/packages/react-shared-components/src/types.ts new file mode 100644 index 00000000000..eb4949ed662 --- /dev/null +++ b/packages/react-shared-components/src/types.ts @@ -0,0 +1,88 @@ +export type IArrayList = React.FC & { + Remove: React.FC + Addition: React.FC + MoveUp: React.FC + MoveDown: React.FC + Empty: React.FC + useComponent: (name: string) => ReactComponent + useArrayList: (index?: number) => IArrayListProps & ArrayListInfo +} + +interface ArrayListInfo { + currentIndex: number + isEmpty: boolean + isDisable: boolean + allowRemove: boolean + allowAddition: boolean + allowMoveDown: boolean + allowMoveUp: boolean + renderWith: ( + name: string, + render: (node: any) => React.ReactElement, + wrapper?: any + ) => any +} + +type ReactComponent = React.JSXElementConstructor + +type ReactRenderPropsChildren = + | React.ReactNode + | ((props: T) => React.ReactElement) + +export interface IArrayListProps { + value?: any[] + maxItems?: number + minItems?: number + editable?: boolean + disabled?: boolean + components?: { + CircleButton?: ReactComponent + TextButton?: ReactComponent + AdditionIcon?: ReactComponent + RemoveIcon?: ReactComponent + MoveDownIcon?: ReactComponent + MoveUpIcon?: ReactComponent + } + renders?: { + renderAddition?: ReactRenderPropsChildren + renderEmpty?: ReactRenderPropsChildren + renderMoveDown?: ReactRenderPropsChildren + renderMoveUp?: ReactRenderPropsChildren + renderRemove?: ReactRenderPropsChildren + } + context?: any +} + +export interface IArrayListOperatorProps { + children?: React.ReactElement | ((method: T) => React.ReactElement) +} + +export interface IPreviewTextProps { + className?: React.ReactText + dataSource?: any[] + value?: any + addonBefore?: React.ReactNode + innerBefore?: React.ReactNode + addonTextBefore?: React.ReactNode + addonTextAfter?: React.ReactNode + innerAfter?: React.ReactNode + addonAfter?: React.ReactNode +} + +export interface OperatorButtonProps { + index?: number + children?: ReactRenderPropsChildren + component?: string + onClick?: (event: any) => void +} + +export interface IPasswordStrengthProps { + value?: React.ReactText + children?: ReactRenderPropsChildren +} + +export type IArrayListRemoveProps = OperatorButtonProps +export type IArrayListAdditionProps = OperatorButtonProps +export type IArrayListMoveUpProps = OperatorButtonProps +export type IArrayListMoveDownProps = OperatorButtonProps +export type IArrayListEmptyProps = IArrayListAdditionProps diff --git a/packages/react-shared-components/tsconfig.json b/packages/react-shared-components/tsconfig.json new file mode 100644 index 00000000000..1d669c29c46 --- /dev/null +++ b/packages/react-shared-components/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./lib" + }, + "include": ["./src/**/*.ts", "./src/**/*.tsx"], + "exclude": ["./src/__tests__/*"] +} diff --git a/packages/types/LICENSE.md b/packages/react/LICENSE.md similarity index 100% rename from packages/types/LICENSE.md rename to packages/react/LICENSE.md diff --git a/packages/react/README.md b/packages/react/README.md index 6162875a43d..749d5be34d4 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -1,2 +1,299 @@ # @uform/react -> UForm React实现 \ No newline at end of file + +> UForm React Pure Package + + +### ArrayStringList + +```jsx +import React, { useState } from 'react' +import { + Form, + Field, + FormPath, + createFormActions, + FormSpy, + FormProvider, + FormConsumer, + FormEffectHooks +} from './src' + +const actions = createFormActions() + +const Input = props => ( + + {({ state, mutators }) => ( +
    + + {state.errors} + {state.warnings} +
    + )} +
    +) + +const { onFormInit$, onFormInputChange$, onFieldInputChange$ } = FormEffectHooks + +const App = () => { + const [values, setValues] = useState({}) + const [editable, setEditable] = useState(true) + return ( + +
    { + onFormInit$().subscribe(() => { + console.log('初始化') + }) + onFieldInputChange$().subscribe(state => { + console.log('输入变化', state) + }) + }} + onChange={() => {}} + > + + {({ state, mutators }) => { + return ( +
    + {state.value.map((item, index) => { + return ( + + + + + ) + })} + +
    + ) + }} +
    +
    +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### ArrayObjectList + +```jsx +import React, { useState } from 'react' +import { + Form, + Field, + FormPath, + createFormActions, + FormSpy, + FormProvider, + FormConsumer, + FormEffectHooks +} from './src' + +const actions = createFormActions() + +const Input = props => ( + + {({ state, mutators }) => ( +
    + + {state.errors} + {state.warnings} +
    + )} +
    +) + +const { onFormInit$, onFormInputChange$, onFieldInputChange$ } = FormEffectHooks + +const App = () => { + const [values, setValues] = useState({}) + const [editable, setEditable] = useState(true) + return ( + +
    { + onFormInit$().subscribe(() => { + console.log('初始化') + }) + onFieldInputChange$().subscribe(state => { + console.log('输入变化', state) + }) + }} + onChange={() => {}} + > + + {({ state, mutators }) => { + return ( +
    + {state.value.map((item, index) => { + return ( + + + { + if (value == '123') { + return { + type: 'warning', + message: '这个是一个提示' + } + } + }} + /> + + + ) + })} + +
    + ) + }} +
    +
    +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Dynamic Object + +```jsx +import React, { useState } from 'react' +import { + Form, + Field, + FormPath, + createFormActions, + FormSpy, + FormProvider, + FormConsumer, + FormEffectHooks +} from './src' + +const actions = createFormActions() + +const Input = props => ( + + {({ state, mutators }) => ( +
    + + {state.errors} + {state.warnings} +
    + )} +
    +) + +const { onFormInit$, onFormInputChange$, onFieldInputChange$ } = FormEffectHooks + +const App = () => { + const [values, setValues] = useState({}) + const [editable, setEditable] = useState(true) + return ( + +
    { + onFormInit$().subscribe(() => { + console.log('初始化') + }) + onFieldInputChange$().subscribe(state => { + console.log('输入变化', state) + }) + }} + onChange={() => {}} + > + + {({ state, mutators }) => { + return ( + + {Object.keys(state.value).map(key => { + if (!mutators.exist(key)) return + return ( + + + + + ) + })} + + + ) + }} + +
    +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` diff --git a/packages/react/package.json b/packages/react/package.json index 1c9aa1b1338..47a8480d8a8 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,12 +1,13 @@ { "name": "@uform/react", - "version": "0.4.3", + "version": "0.4.0", "license": "MIT", "main": "lib", "repository": { "type": "git", "url": "git+https://github.com/alibaba/uform.git" }, + "types": "lib/index.d.ts", "bugs": { "url": "https://github.com/alibaba/uform/issues" }, @@ -14,30 +15,27 @@ "engines": { "npm": ">=3.0.0" }, - "types": "lib/index.d.ts", "scripts": { - "build": "tsc --declaration" + "build": "rm -rf lib && tsc --declaration" }, "devDependencies": { "typescript": "^3.5.2" }, "peerDependencies": { "@babel/runtime": "^7.4.4", + "scheduler": ">=0.11.2", "@types/react": "^16.8.23", "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "react-dom": ">=16.8.0", + "react-eva": "^1.0.0", + "rxjs": "^6.5.1" }, "dependencies": { - "@uform/core": "^0.4.3", - "@uform/types": "^0.4.3", - "@uform/utils": "^0.4.3", - "@uform/validator": "^0.4.3", - "pascal-case": "^2.0.1", - "react-eva": "^1.0.0", - "rxjs-compat": "^6.5.1" + "@uform/core": "^0.4.0", + "@uform/shared": "^0.4.0" }, "publishConfig": { "access": "public" }, "gitHead": "4d068dad6183e8da294a4c899a158326c0b0b050" -} +} \ No newline at end of file diff --git a/packages/react/src/__tests__/actions.spec.tsx b/packages/react/src/__tests__/actions.spec.tsx new file mode 100644 index 00000000000..3764eab2358 --- /dev/null +++ b/packages/react/src/__tests__/actions.spec.tsx @@ -0,0 +1,12 @@ +describe('test all apis',()=>{ + //todo +}) + +describe('major scenes',()=>{ + //todo +}) + + +describe('bugfix',()=>{ + //todo +}) \ No newline at end of file diff --git a/packages/react/src/__tests__/consumer.spec.tsx b/packages/react/src/__tests__/consumer.spec.tsx new file mode 100644 index 00000000000..3764eab2358 --- /dev/null +++ b/packages/react/src/__tests__/consumer.spec.tsx @@ -0,0 +1,12 @@ +describe('test all apis',()=>{ + //todo +}) + +describe('major scenes',()=>{ + //todo +}) + + +describe('bugfix',()=>{ + //todo +}) \ No newline at end of file diff --git a/packages/react/src/__tests__/effects.spec.tsx b/packages/react/src/__tests__/effects.spec.tsx new file mode 100644 index 00000000000..3764eab2358 --- /dev/null +++ b/packages/react/src/__tests__/effects.spec.tsx @@ -0,0 +1,12 @@ +describe('test all apis',()=>{ + //todo +}) + +describe('major scenes',()=>{ + //todo +}) + + +describe('bugfix',()=>{ + //todo +}) \ No newline at end of file diff --git a/packages/react/src/__tests__/field.spec.tsx b/packages/react/src/__tests__/field.spec.tsx new file mode 100644 index 00000000000..3764eab2358 --- /dev/null +++ b/packages/react/src/__tests__/field.spec.tsx @@ -0,0 +1,12 @@ +describe('test all apis',()=>{ + //todo +}) + +describe('major scenes',()=>{ + //todo +}) + + +describe('bugfix',()=>{ + //todo +}) \ No newline at end of file diff --git a/packages/react/src/__tests__/form.spec.tsx b/packages/react/src/__tests__/form.spec.tsx new file mode 100644 index 00000000000..041f97923ce --- /dev/null +++ b/packages/react/src/__tests__/form.spec.tsx @@ -0,0 +1,71 @@ +import React from 'react' +import { + Form, + Field, + createFormActions, + FormEffectHooks, + IFieldProps +} from '../index' +import { render } from '@testing-library/react' + +const Input: React.FC = props => ( + + {({ state, mutators }) => ( +
    + + {state.errors} + {state.warnings} +
    + )} +
    +) + +const { onFieldValueChange$ } = FormEffectHooks + +describe('test all apis', () => { + test('Form', async () => { + const actions = createFormActions() + const onSubmitHandler = jest.fn() + const onValidateFailedHandler = jest.fn() + render( +
    { + onFieldValueChange$('aaa').subscribe(fieldState => { + $.setFieldState('aaa', state => { + state.value = 'hello world' + }) + }) + }} + > + +
    + ) + try { + await actions.submit() + } catch (e) { + expect(e).toEqual([{ path: 'aaa', messages: ['This field is required'] }]) + } + actions.setFieldState('aaa', state => { + state.value = '123' + }) + await actions.submit() + expect(onSubmitHandler).toBeCalledWith({ aaa: 'hello world' }) + }) +}) + +describe('major scenes', () => { + //todo +}) + +describe('bugfix', () => { + //todo +}) diff --git a/packages/react/src/__tests__/provider.spec.tsx b/packages/react/src/__tests__/provider.spec.tsx new file mode 100644 index 00000000000..3764eab2358 --- /dev/null +++ b/packages/react/src/__tests__/provider.spec.tsx @@ -0,0 +1,12 @@ +describe('test all apis',()=>{ + //todo +}) + +describe('major scenes',()=>{ + //todo +}) + + +describe('bugfix',()=>{ + //todo +}) \ No newline at end of file diff --git a/packages/react/src/__tests__/spy.spec.tsx b/packages/react/src/__tests__/spy.spec.tsx new file mode 100644 index 00000000000..3764eab2358 --- /dev/null +++ b/packages/react/src/__tests__/spy.spec.tsx @@ -0,0 +1,12 @@ +describe('test all apis',()=>{ + //todo +}) + +describe('major scenes',()=>{ + //todo +}) + + +describe('bugfix',()=>{ + //todo +}) \ No newline at end of file diff --git a/packages/react/src/__tests__/useDirty.spec.tsx b/packages/react/src/__tests__/useDirty.spec.tsx new file mode 100644 index 00000000000..3764eab2358 --- /dev/null +++ b/packages/react/src/__tests__/useDirty.spec.tsx @@ -0,0 +1,12 @@ +describe('test all apis',()=>{ + //todo +}) + +describe('major scenes',()=>{ + //todo +}) + + +describe('bugfix',()=>{ + //todo +}) \ No newline at end of file diff --git a/packages/react/src/__tests__/useField.spec.tsx b/packages/react/src/__tests__/useField.spec.tsx new file mode 100644 index 00000000000..3764eab2358 --- /dev/null +++ b/packages/react/src/__tests__/useField.spec.tsx @@ -0,0 +1,12 @@ +describe('test all apis',()=>{ + //todo +}) + +describe('major scenes',()=>{ + //todo +}) + + +describe('bugfix',()=>{ + //todo +}) \ No newline at end of file diff --git a/packages/react/src/__tests__/useForceUpdate.spec.tsx b/packages/react/src/__tests__/useForceUpdate.spec.tsx new file mode 100644 index 00000000000..3764eab2358 --- /dev/null +++ b/packages/react/src/__tests__/useForceUpdate.spec.tsx @@ -0,0 +1,12 @@ +describe('test all apis',()=>{ + //todo +}) + +describe('major scenes',()=>{ + //todo +}) + + +describe('bugfix',()=>{ + //todo +}) \ No newline at end of file diff --git a/packages/react/src/__tests__/useForm.spec.tsx b/packages/react/src/__tests__/useForm.spec.tsx new file mode 100644 index 00000000000..3764eab2358 --- /dev/null +++ b/packages/react/src/__tests__/useForm.spec.tsx @@ -0,0 +1,12 @@ +describe('test all apis',()=>{ + //todo +}) + +describe('major scenes',()=>{ + //todo +}) + + +describe('bugfix',()=>{ + //todo +}) \ No newline at end of file diff --git a/packages/react/src/__tests__/virtual.spec.tsx b/packages/react/src/__tests__/virtual.spec.tsx new file mode 100644 index 00000000000..3764eab2358 --- /dev/null +++ b/packages/react/src/__tests__/virtual.spec.tsx @@ -0,0 +1,12 @@ +describe('test all apis',()=>{ + //todo +}) + +describe('major scenes',()=>{ + //todo +}) + + +describe('bugfix',()=>{ + //todo +}) \ No newline at end of file diff --git a/packages/react/src/components/Field.tsx b/packages/react/src/components/Field.tsx new file mode 100644 index 00000000000..1011f64b6cb --- /dev/null +++ b/packages/react/src/components/Field.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import { useField } from '../hooks/useField' +import { isFn } from '@uform/shared' +import { IMutators, IFieldState } from '@uform/core' +import { IFieldProps } from '../types' +import { getValueFromEvent } from '../shared' + +const createFieldMutators = ( + mutators: IMutators, + props: IFieldProps, + state: IFieldState +): IMutators => { + return { + ...mutators, + change: (...args) => { + args[0] = isFn(props.getValueFromEvent) + ? props.getValueFromEvent(...args) + : args[0] + mutators.change(...args.map(event => getValueFromEvent(event))) + if (props.triggerType === 'onChange') { + mutators.validate() + } + }, + blur: () => { + mutators.blur() + if (props.triggerType === 'onBlur') { + mutators.validate() + } + } + } +} + +export const Field: React.FunctionComponent = props => { + const { state, props: innerProps, mutators, form } = useField(props) + if (!state.visible || !state.display) return + if (isFn(props.children)) { + return props.children({ + form, + state, + props: innerProps, + mutators: createFieldMutators(mutators, props, state) + }) + } else { + return {props.children} + } +} + +Field.displayName = 'ReactInternalField' + +Field.defaultProps = { + path: '', + triggerType: 'onChange' +} diff --git a/packages/react/src/components/Form.tsx b/packages/react/src/components/Form.tsx new file mode 100644 index 00000000000..948feb95544 --- /dev/null +++ b/packages/react/src/components/Form.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import { isFn } from '@uform/shared' +import { useForm } from '../hooks/useForm' +import FormContext from '../context' +import { IFormProps, IFormActions, IFormAsyncActions } from '../types' + +export const Form: React.FunctionComponent< + IFormProps +> = (props = {}) => { + const form = useForm(props) + return ( + + {isFn(props.children) ? props.children(form) : props.children} + + ) +} + +Form.displayName = 'ReactInternalForm' diff --git a/packages/react/src/components/FormConsumer.tsx b/packages/react/src/components/FormConsumer.tsx new file mode 100644 index 00000000000..49ac2963a19 --- /dev/null +++ b/packages/react/src/components/FormConsumer.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { isFn, deprecate } from '@uform/shared' +import { FormSpy } from './FormSpy' +import { IForm, LifeCycleTypes } from '@uform/core' +import { IFormConsumerProps, IFormConsumerAPI } from '../types' + +const transformStatus = (type: string) => { + switch (type) { + case LifeCycleTypes.ON_FORM_INIT: + return 'initialize' + case LifeCycleTypes.ON_FORM_SUBMIT_START: + return 'submitting' + case LifeCycleTypes.ON_FORM_SUBMIT_END: + return 'submitted' + case LifeCycleTypes.ON_FORM_VALIDATE_START: + return 'validating' + case LifeCycleTypes.ON_FORM_VALIDATE_END: + return 'validated' + default: + return type + } +} + +const transformFormAPI = (api: IForm, type: string): IFormConsumerAPI => { + deprecate('FormConsumer', 'Please use FormSpy Component.') + return { + status: transformStatus(type), + state: api.getFormState(), + submit: api.submit, + reset: api.reset + } +} + +export const FormConsumer: React.FunctionComponent< + IFormConsumerProps +> = props => { + return ( + + {({ form, type }) => { + return isFn(props.children) + ? props.children(transformFormAPI(form, type)) + : props.children + }} + + ) +} diff --git a/packages/react/src/components/FormProvider.tsx b/packages/react/src/components/FormProvider.tsx new file mode 100644 index 00000000000..ac73cc3cbdd --- /dev/null +++ b/packages/react/src/components/FormProvider.tsx @@ -0,0 +1,12 @@ +import React, { useMemo } from 'react' +import { BroadcastContext } from '../context' +import { Broadcast } from '@uform/shared' + +const { Provider } = BroadcastContext + +export const FormProvider: React.FunctionComponent = props => { + const broadcast = useMemo(() => { + return new Broadcast() + }, []) + return {props.children} +} diff --git a/packages/react/src/components/FormSpy.tsx b/packages/react/src/components/FormSpy.tsx new file mode 100644 index 00000000000..444ac63cdbd --- /dev/null +++ b/packages/react/src/components/FormSpy.tsx @@ -0,0 +1,73 @@ +import { + useContext, + useMemo, + useRef, + useEffect, + useCallback, + useState, + useReducer +} from 'react' +import { FormHeartSubscriber, LifeCycleTypes } from '@uform/core' +import { isFn, isStr, FormPath, isArr } from '@uform/shared' +import { IFormSpyProps } from '../types' +import FormContext, { BroadcastContext } from '../context' + +export const FormSpy: React.FunctionComponent = props => { + const broadcast = useContext(BroadcastContext) + const form = useContext(FormContext) + const initializedRef = useRef(false) + const [type, setType] = useState(LifeCycleTypes.ON_FORM_INIT) + const [state, dispatch] = useReducer( + (state, action) => props.reducer(state, action, form), + {} + ) + const subscriber = useCallback(({ type, payload }) => { + if (initializedRef.current) return + if (isStr(props.selector) && FormPath.parse(props.selector).match(type)) { + setType(type) + dispatch({ + type, + payload + }) + } else if (isArr(props.selector) && props.selector.indexOf(type) > -1) { + setType(type) + dispatch({ + type, + payload + }) + } + }, []) + useMemo(() => { + initializedRef.current = true + if (form) { + form.subscribe(subscriber) + } else if (broadcast) { + broadcast.subscribe(subscriber) + } + initializedRef.current = false + }, []) + useEffect(() => { + return () => { + if (form) { + form.unsubscribe(subscriber) + } else if (broadcast) { + broadcast.unsubscribe(subscriber) + } + } + }, []) + if (isFn(props.children)) { + const formApi = form ? form : broadcast && broadcast.getContext() + return props.children({ form: formApi, type, state }) + } else { + return props.children + } +} + +FormSpy.displayName = 'ReactInternalFormSpy' + +FormSpy.defaultProps = { + selector: `*`, + reducer: (state, action) => { + return state + } +} diff --git a/packages/react/src/components/VirtualField.tsx b/packages/react/src/components/VirtualField.tsx new file mode 100644 index 00000000000..84bacbdb4fe --- /dev/null +++ b/packages/react/src/components/VirtualField.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import { useVirtualField } from '../hooks/useVirtualField' +import { isFn } from '@uform/shared' +import { IVirtualFieldProps } from '../types' + +export const VirtualField: React.FunctionComponent< + IVirtualFieldProps +> = props => { + const { state, props: innerProps, form } = useVirtualField(props) + if (!state.visible || !state.display) return + if (isFn(props.children)) { + return props.children({ + form, + state, + props: innerProps + }) + } else { + return props.children + } +} + +VirtualField.displayName = 'ReactInternalVirtualField' + +VirtualField.defaultProps = { + path: '' +} diff --git a/packages/react/src/context.ts b/packages/react/src/context.ts new file mode 100644 index 00000000000..4097188b652 --- /dev/null +++ b/packages/react/src/context.ts @@ -0,0 +1,7 @@ +import { createContext } from 'react' +import { Broadcast } from '@uform/shared' +import { IForm } from '@uform/core' + +export const BroadcastContext = createContext(null) + +export default createContext(null) diff --git a/packages/react/src/decorators/connect.ts b/packages/react/src/decorators/connect.ts deleted file mode 100644 index 040870a4a46..00000000000 --- a/packages/react/src/decorators/connect.ts +++ /dev/null @@ -1,203 +0,0 @@ -import React, { PureComponent } from 'react' -import { ISchema, Dispatcher } from '@uform/types' -import { isArr, isFn, each } from '../utils' -import { IEventTargetOption, IFieldProps } from '../type' - -const isEvent = (candidate: React.SyntheticEvent): boolean => - !!(candidate && candidate.stopPropagation && candidate.preventDefault) - -const isReactNative = - typeof window !== 'undefined' && - window.navigator && - window.navigator.product && - window.navigator.product === 'ReactNative' - -const getSelectedValues = (options?: IEventTargetOption[]) => { - const result = [] - if (options) { - for (let index = 0; index < options.length; index++) { - const option = options[index] - if (option.selected) { - result.push(option.value) - } - } - } - return result -} - -// TODO 需要 any ? -const getValue = ( - event: React.SyntheticEvent | any, - isReactNative: boolean -) => { - if (isEvent(event)) { - if ( - !isReactNative && - event.nativeEvent && - event.nativeEvent.text !== undefined - ) { - return event.nativeEvent.text - } - if (isReactNative && event.nativeEvent !== undefined) { - return event.nativeEvent.text - } - - const detypedEvent = event - const { - target: { type, value, checked, files }, - dataTransfer - } = detypedEvent - - if (type === 'checkbox') { - return !!checked - } - - if (type === 'file') { - return files || (dataTransfer && dataTransfer.files) - } - - if (type === 'select-multiple') { - return getSelectedValues(event.target.options) - } - return value - } - return event -} - -const createEnum = (enums: any, enumNames: string | any[]) => { - if (isArr(enums)) { - return enums.map((item, index) => { - if (typeof item === 'object') { - return { - ...item - } - } else { - return { - ...item, - label: isArr(enumNames) ? enumNames[index] || item : item, - value: item - } - } - }) - } - - return [] -} - -const bindEffects = ( - props: IConnectProps, - effect: ISchema['x-effect'], - dispatch: Dispatcher -) => { - each(effect(dispatch, { ...props }), (event, key) => { - const prevEvent = key === 'onChange' ? props[key] : undefined - props[key] = (...args) => { - if (isFn(prevEvent)) { - prevEvent(...args) - } - if (isFn(event)) { - return event(...args) - } - } - }) - return props -} - -// 这个不枚举了,因为是 x-props 的 -export interface IConnectProps extends IFieldProps { - disabled?: boolean - readOnly?: boolean - showTime?: boolean - dataSource?: any[] - [name: string]: any -} - -export interface IConnectOptions { - valueName?: string - eventName?: string - defaultProps?: object - getValueFromEvent?: (event?: any, value?: any) => any - getProps?: ( - props: IConnectProps, - componentProps: IFieldProps - ) => IConnectProps | void - getComponent?: ( - Target: T, - props, - { - editable, - name - }: { editable: boolean | ((name: string) => boolean); name: string } - ) => T -} - -export const connect = >( - opts?: IConnectOptions -) => (Target: T): React.ComponentClass => { - opts = { - valueName: 'value', - eventName: 'onChange', - ...opts - } - return class extends PureComponent { - render() { - const { value, name, mutators, schema, editable } = this.props - - let props = { - ...opts.defaultProps, - ...schema['x-props'], - [opts.valueName]: value, - [opts.eventName]: (event, ...args) => { - mutators.change( - opts.getValueFromEvent - ? opts.getValueFromEvent.call( - { props: schema['x-props'] || {} }, - event, - ...args - ) - : getValue(event, isReactNative) - ) - } - } as IConnectProps - - if (editable !== undefined) { - if (isFn(editable)) { - if (!editable(name)) { - props.disabled = true - props.readOnly = true - } - } else if (editable === false) { - props.disabled = true - props.readOnly = true - } - } - - if (isFn(schema['x-effect'])) { - props = bindEffects(props, schema['x-effect'], mutators.dispatch) - } - - if (isFn(opts.getProps)) { - const newProps = opts.getProps(props, this.props) - if (newProps !== undefined) { - // @ts-ignore TODO - props = newProps - } - } - - if (isArr(schema.enum) && !props.dataSource) { - props.dataSource = createEnum(schema.enum, schema.enumNames) - } - - if (props.editable !== undefined) { - delete props.editable - } - - return React.createElement( - isFn(opts.getComponent) - ? opts.getComponent(Target, props, this.props) - : Target, - props - ) - } - } -} diff --git a/packages/react/src/decorators/markup.tsx b/packages/react/src/decorators/markup.tsx deleted file mode 100644 index 66e0350235c..00000000000 --- a/packages/react/src/decorators/markup.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React, { Component, useContext } from 'react' -import { createPortal } from 'react-dom' - -import { ISchemaFormProps } from '../type' -import { - createHOC, - schemaIs, - clone, - filterSchemaPropertiesAndReactChildren -} from '../utils' -import { MarkupContext } from '../shared/context' - -let nonameId = 0 - -const getRadomName = () => { - return `UFORM_NO_NAME_FIELD_$${nonameId++}` -} - -export const SchemaField = props => { - const parent = useContext(MarkupContext) - if (schemaIs(parent, 'object')) { - const name = props.name || getRadomName() - parent.properties = parent.properties || {} - parent.properties[name] = clone( - props, - filterSchemaPropertiesAndReactChildren - ) - return ( - - {props.children} - - ) - } else if (schemaIs(parent, 'array')) { - parent.items = clone(props, filterSchemaPropertiesAndReactChildren) - return ( - - {props.children} - - ) - } else { - return props.children || - } -} - -export const SchemaMarkup = createHOC((options, SchemaForm) => { - return class extends Component { - public static displayName = 'SchemaMarkupParser' - - public portalRoot = document.createElement('div') - - public render() { - const { - children, - initialValues, - defaultValue, - value, - schema, - ...others - } = this.props - - let alreadyHasSchema = false - let finalSchema = {} - if (schema) { - alreadyHasSchema = true - finalSchema = schema - } else { - finalSchema = { type: 'object' } - } - - nonameId = 0 - - return ( - - {!alreadyHasSchema && - // 只是为了去收集 schema 数据 - createPortal( - - {children} - , - this.portalRoot - )} - - {children} - - - ) - } - } -}) diff --git a/packages/react/src/hooks/useDirty.ts b/packages/react/src/hooks/useDirty.ts new file mode 100644 index 00000000000..f49b3af82fb --- /dev/null +++ b/packages/react/src/hooks/useDirty.ts @@ -0,0 +1,17 @@ +import React from 'react' +import { isEqual } from '@uform/shared' + +export const useDirty = (input: any = {}, keys: string[] = []) => { + const ref = React.useRef({ data: {...input}, dirtys: {}, num: 0 }) + ref.current.num = 0 + keys.forEach(key => { + if (!isEqual(input[key], ref.current.data[key])) { + ref.current.data[key] = input[key] + ref.current.dirtys[key] = true + ref.current.num++ + } else { + ref.current.dirtys[key] = false + } + }) + return ref.current +} diff --git a/packages/react/src/hooks/useField.ts b/packages/react/src/hooks/useField.ts new file mode 100644 index 00000000000..4d6b653c259 --- /dev/null +++ b/packages/react/src/hooks/useField.ts @@ -0,0 +1,76 @@ +import { useMemo, useEffect, useRef, useContext } from 'react' +import { each } from '@uform/shared' +import { IFieldStateProps, IFieldState, IForm, IField } from '@uform/core' +import { raf } from '../shared' +import { useDirty } from './useDirty' +import { useForceUpdate } from './useForceUpdate' +import { IFieldHook } from '../types' +import FormContext from '../context' + +export const useField = (options: IFieldStateProps): IFieldHook => { + const forceUpdate = useForceUpdate() + const dirty = useDirty(options, ['props', 'rules', 'required', 'editable']) + const ref = useRef<{ field: IField; unmounted: boolean }>({ + field: null, + unmounted: false + }) + const form = useContext(FormContext) + if (!form) { + throw new Error('Form object cannot be found from context.') + } + useMemo(() => { + let initialized = false + ref.current.field = form.registerField({ + ...options, + onChange() { + if (ref.current.unmounted) return + /** + * 同步Field状态只需要forceUpdate一下触发重新渲染,因为字段状态全部代理在uform core内部 + */ + if (initialized) { + raf(() => { + forceUpdate() + }) + } + } + }) + initialized = true + }, []) + + useEffect(() => { + if (dirty.num > 0) { + ref.current.field.setState((state: IFieldState) => { + each(dirty.dirtys, (result, key) => { + if (result) { + state[key] = options[key] + } + }) + }) + } + }) + + useEffect(() => { + ref.current.field.unsafe_setSourceState(state => { + state.mounted = true + }) + return () => { + ref.current.unmounted = true + ref.current.field.setState((state: IFieldState) => { + state.unmounted = true + }) + } + }, []) + + const state = ref.current.field.getState() + return { + form, + state: { + ...state, + errors: state.errors.join(', ') + }, + mutators: form.createMutators(ref.current.field), + props: state.props + } +} + +export default useField diff --git a/packages/react/src/hooks/useForceUpdate.ts b/packages/react/src/hooks/useForceUpdate.ts new file mode 100644 index 00000000000..193c8916af5 --- /dev/null +++ b/packages/react/src/hooks/useForceUpdate.ts @@ -0,0 +1,18 @@ +import { useCallback, useState } from 'react'; + +// Returning a new object reference guarantees that a before-and-after +// equivalence check will always be false, resulting in a re-render, even +// when multiple calls to forceUpdate are batched. + +export function useForceUpdate(): () => void { + const [ , dispatch ] = useState<{}>(Object.create(null)); + + // Turn dispatch(required_parameter) into dispatch(). + const memoizedDispatch = useCallback( + (): void => { + dispatch(Object.create(null)); + }, + [ dispatch ], + ); + return memoizedDispatch; +} \ No newline at end of file diff --git a/packages/react/src/hooks/useForm.ts b/packages/react/src/hooks/useForm.ts new file mode 100644 index 00000000000..3958e2aea7e --- /dev/null +++ b/packages/react/src/hooks/useForm.ts @@ -0,0 +1,128 @@ +import { useMemo, useEffect, useRef, useContext } from 'react' +import { + createForm, + IFormCreatorOptions, + LifeCycleTypes, + FormLifeCycle, + IForm, + IModel, + isStateModel, + IFormState +} from '@uform/core' +import { useDirty } from './useDirty' +import { useEva } from 'react-eva' +import { IFormProps } from '../types' +import { BroadcastContext } from '../context' +import { createFormEffects, createFormActions } from '../shared' +import { isValid } from '@uform/shared' +const FormHookSymbol = Symbol('FORM_HOOK') + +const useInternalForm = ( + options: IFormCreatorOptions & { form?: IForm } = {} +) => { + const dirty = useDirty(options, ['initialValues', 'values', 'editable']) + const alreadyHaveForm = !!options.form + const alreadyHaveHookForm = options.form && options.form[FormHookSymbol] + const form = useMemo(() => { + return alreadyHaveForm ? options.form : createForm(options) + }, []) + + useEffect(() => { + if (alreadyHaveHookForm) return + if (dirty.num > 0) { + form.setFormState(state => { + if (dirty.dirtys.values && isValid(options.values)) { + state.values = options.values + } + if (dirty.dirtys.initialValues && isValid(options.initialValues)) { + state.values = options.initialValues + state.initialValues = options.initialValues + } + if (dirty.dirtys.editable && isValid(options.editable)) { + state.editable = options.editable + } + }) + } + }) + + useEffect(() => { + if (alreadyHaveHookForm) return + form.setFormState(state => { + state.mounted = true + }) + return () => { + form.setFormState(state => { + state.unmounted = true + }) + } + }, []) + ;(form as any)[FormHookSymbol] = true + + return form +} + +export const useForm = < + Value = any, + DefaultValue = any, + EffectPayload = any, + EffectAction = any +>( + props: IFormProps +) => { + const actionsRef = useRef(null) + actionsRef.current = + actionsRef.current || props.actions || createFormActions() + const broadcast = useContext(BroadcastContext) + const { implementActions, dispatch } = useEva({ + actions: actionsRef.current, + effects: createFormEffects(props.effects, actionsRef.current) + }) + const form = useInternalForm({ + form: props.form, + values: props.value, + initialValues: props.initialValues, + useDirty: props.useDirty, + editable: props.editable, + validateFirst: props.validateFirst, + lifecycles: [ + new FormLifeCycle( + ({ type, payload }: { type: string; payload: IModel }) => { + dispatch.lazy(type, () => { + return isStateModel(payload) ? payload.getState() : payload + }) + if (type === LifeCycleTypes.ON_FORM_INPUT_CHANGE) { + if (props.onChange) { + props.onChange( + isStateModel(payload) + ? payload.getState((state: IFormState) => state.values) + : {} + ) + } + } + if (broadcast) { + broadcast.notify({ type, payload }) + } + } + ), + new FormLifeCycle( + LifeCycleTypes.ON_FORM_WILL_INIT, + (payload: IModel, form: IForm) => { + const actions = { + ...form, + dispatch: form.notify + } + if (broadcast) { + broadcast.setContext(actions) + } + implementActions(actions) + } + ) + ], + onReset: props.onReset, + onSubmit: props.onSubmit, + onValidateFailed: props.onValidateFailed + }) + return form +} + +export default useForm diff --git a/packages/react/src/hooks/useVirtualField.ts b/packages/react/src/hooks/useVirtualField.ts new file mode 100644 index 00000000000..3f5f21645dc --- /dev/null +++ b/packages/react/src/hooks/useVirtualField.ts @@ -0,0 +1,71 @@ +import { useMemo, useEffect, useRef, useContext } from 'react' +import { each } from '@uform/shared' +import { IVirtualFieldStateProps, IVirtualFieldState, IForm } from '@uform/core' +import { useDirty } from './useDirty' +import { useForceUpdate } from './useForceUpdate' +import { raf } from '../shared' +import { IVirtualFieldHook } from '../types' +import FormContext from '../context' + +export const useVirtualField = ( + options: IVirtualFieldStateProps +): IVirtualFieldHook => { + const forceUpdate = useForceUpdate() + const dirty = useDirty(options, ['props']) + const ref = useRef({ + field: null, + unmounted: false + }) + const form = useContext(FormContext) + if (!form) { + throw new Error('Form object cannot be found from context.') + } + useMemo(() => { + let initialized = false + ref.current.field = form.registerVirtualField({ + ...options, + onChange() { + if (ref.current.unmounted) return + /** + * 同步Field状态只需要forceUpdate一下触发重新渲染,因为字段状态全部代理在uform core内部 + */ + if (initialized) { + raf(() => { + forceUpdate() + }) + } + } + }) + initialized = true + }, []) + + useEffect(() => { + if (dirty.num > 0) { + ref.current.field.setState((state: IVirtualFieldState) => { + each(dirty.dirtys, (result, key) => { + if (result) { + state[key] = options[key] + } + }) + }) + } + }) + + useEffect(() => { + return () => { + ref.current.unmounted = true + ref.current.field.setState((state: IVirtualFieldState) => { + state.unmounted = true + }) + } + }, []) + + const state = ref.current.field.getState() + return { + state, + form, + props: state.props + } +} + +export default useVirtualField diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts new file mode 100644 index 00000000000..7427cdcb193 --- /dev/null +++ b/packages/react/src/index.ts @@ -0,0 +1,24 @@ +import { + FormEffectHooks, + createEffectHook, + createFormActions, + createAsyncFormActions +} from './shared' +export * from '@uform/core' +export * from './components/Form' +export * from './components/Field' +export * from './components/VirtualField' +export * from './components/FormSpy' +export * from './components/FormProvider' +export * from './components/FormConsumer' +export * from './hooks/useForm' +export * from './hooks/useField' +export * from './hooks/useVirtualField' +export * from './types' + +export { + FormEffectHooks, + createEffectHook, + createFormActions, + createAsyncFormActions +} diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx deleted file mode 100644 index 0523bb43043..00000000000 --- a/packages/react/src/index.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as React from 'react' -import { setLocale, setLanguage } from '@uform/validator' -import { FormPath } from '@uform/core' -import { IFormActions, IAsyncFormActions } from '@uform/types' -import { createActions, createAsyncActions } from 'react-eva' - -import { - OriginForm, - registerFieldMiddleware, - registerFormFieldPropsTransformer, - registerFormField, - registerFormFields, - registerFormWrapper -} from './shared/core' -import { FormField } from './state/field' -import { calculateSchemaInitialValues } from './utils' -import { SchemaField, SchemaMarkup } from './decorators/markup' -import initialize from './initialize' -import { ISchemaFormProps } from './type' - -export * from './type' -export * from './shared/virtualbox' -export * from './decorators/connect' -export * from './shared/broadcast' -export * from './shared/array' - -initialize() - -export const SchemaForm = SchemaMarkup()( - React.forwardRef((props: ISchemaFormProps, ref: React.Ref) => { - // 这个时候就有 schema 数据 - const { children, className, ...others } = props - return ( - -
    - -
    - {children} -
    - ) - }) -) - -export const Field = SchemaField - -export const setValidationLocale = setLocale - -export const setValidationLanguage = setLanguage - -export const createFormActions = (): IFormActions => - createActions( - 'getFormState', - 'getFieldState', - 'setFormState', - 'setFieldState', - 'getSchema', - 'reset', - 'submit', - 'validate', - 'dispatch' - ) - -export const createAsyncFormActions = (): IAsyncFormActions => - createAsyncActions( - 'getFormState', - 'getFieldState', - 'setFormState', - 'setFieldState', - 'getSchema', - 'reset', - 'submit', - 'validate', - 'dispatch' - ) - -export { - registerFormField, - registerFormFields, - registerFormWrapper, - registerFieldMiddleware, - registerFormFieldPropsTransformer, - calculateSchemaInitialValues, - FormPath -} - -SchemaForm.displayName = 'SchemaForm' - -export default SchemaForm diff --git a/packages/react/src/initialize/index.ts b/packages/react/src/initialize/index.ts deleted file mode 100644 index 7d3fcdf1949..00000000000 --- a/packages/react/src/initialize/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import initialObject from './object' -import initialRender from './render' -import initialVirtualbox from './virtualbox' -import { initialContainer } from '../shared/core' -import intialState from '../state' - -export default () => { - initialContainer() - intialState() - initialObject() - initialRender() - initialVirtualbox() -} diff --git a/packages/react/src/initialize/object.tsx b/packages/react/src/initialize/object.tsx deleted file mode 100644 index b499e06588d..00000000000 --- a/packages/react/src/initialize/object.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react' -import { IFieldProps } from '../type' -import { registerFormField } from '../shared/core' -import { each } from '../utils' - -export default () => - registerFormField( - 'object', - class ObjectField extends React.Component { - public render() { - return this.renderProperties() - } - private renderProperties() { - const { renderField, getOrderProperties } = this.props - const properties = getOrderProperties() - const children = [] - each(properties, ({ key }: { key?: string } = {}) => { - if (key) { - children.push(renderField(key, true)) - } - }) - return children - } - } - ) diff --git a/packages/react/src/initialize/render.tsx b/packages/react/src/initialize/render.tsx deleted file mode 100644 index 4f2e99f7ed3..00000000000 --- a/packages/react/src/initialize/render.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react' -import { IFieldProps } from '../type' -import { registerFieldRenderer } from '../shared/core' -import { isFn } from '../utils' - -export default () => - registerFieldRenderer( - class extends React.Component { - public static displayName = 'FieldXRenderer' - public render() { - if (isFn(this.props.schema['x-render'])) { - return this.props.schema['x-render'](this.props) - } - return - } - } - ) diff --git a/packages/react/src/initialize/virtualbox.tsx b/packages/react/src/initialize/virtualbox.tsx deleted file mode 100644 index 3fba8214da2..00000000000 --- a/packages/react/src/initialize/virtualbox.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react' -import { IFieldProps } from '../type' -import { registerFormField } from '../shared/core' -import { registerVirtualboxFlag } from '../utils' - -export default () => { - registerVirtualboxFlag('slot') - registerFormField( - 'slot', - class extends React.Component { - public static displayName = 'FormSlot' - public render() { - const { schema } = this.props - return {schema.renderChildren} - } - } - ) -} diff --git a/packages/react/src/shared.ts b/packages/react/src/shared.ts new file mode 100644 index 00000000000..3bc79a02713 --- /dev/null +++ b/packages/react/src/shared.ts @@ -0,0 +1,270 @@ +import { isFn, FormPath, globalThisPolyfill } from '@uform/shared' +import { IFormEffect, IFormActions, IFormAsyncActions } from './types' +import { Observable } from 'rxjs/internal/Observable' +import { filter } from 'rxjs/internal/operators' +import { createActions, createAsyncActions } from 'react-eva' +import { + LifeCycleTypes, + IFormState, + FormGraph, + IFieldState, + IVirtualFieldState +} from '@uform/core' + +export const createFormActions = (): IFormActions => { + if (env.currentActions) { + return env.currentActions + } + return createActions( + 'submit', + 'reset', + 'validate', + 'setFormState', + 'getFormState', + 'setFieldState', + 'getFieldState', + 'getFormGraph', + 'setFormGraph', + 'subscribe', + 'unsubscribe', + 'notify', + 'dispatch', + 'setFieldValue', + 'getFieldValue', + 'setFieldInitialValue', + 'getFieldInitialValue' + ) as IFormActions +} + +export const createAsyncFormActions = (): IFormAsyncActions => + createAsyncActions( + 'submit', + 'reset', + 'validate', + 'setFormState', + 'getFormState', + 'setFieldState', + 'getFieldState', + 'getFormGraph', + 'setFormGraph', + 'subscribe', + 'unsubscribe', + 'notify', + 'dispatch', + 'setFieldValue', + 'getFieldValue', + 'setFieldInitialValue', + 'getFieldInitialValue' + ) as IFormAsyncActions + +export interface IEventTargetOption { + selected: boolean + value: any +} + +const isEvent = (candidate: any): boolean => + candidate && + (candidate.stopPropagation || candidate.preventDefault || candidate.bubbles) + +const isReactNative = + typeof window !== 'undefined' && + window.navigator && + window.navigator.product && + window.navigator.product === 'ReactNative' + +const getSelectedValues = (options?: IEventTargetOption[]) => { + const result = [] + if (options) { + for (let index = 0; index < options.length; index++) { + const option = options[index] + if (option.selected) { + result.push(option.value) + } + } + } + return result +} + +export const getValueFromEvent = (event: any) => { + if (isEvent(event)) { + if ( + !isReactNative && + event.nativeEvent && + event.nativeEvent.text !== undefined + ) { + return event.nativeEvent.text + } + if (isReactNative && event.nativeEvent !== undefined) { + return event.nativeEvent.text + } + + const detypedEvent = event + const { + target: { type, value, checked, files }, + dataTransfer + } = detypedEvent + + if (type === 'checkbox') { + return !!checked + } + + if (type === 'file') { + return files || (dataTransfer && dataTransfer.files) + } + + if (type === 'select-multiple') { + return getSelectedValues(event.target.options) + } + return value + } + return event +} + +const compactScheduler = ([raf, caf, priority], fresh: boolean) => { + return [fresh ? callback => raf(priority, callback) : raf, caf] +} + +const getScheduler = () => { + if (!globalThisPolyfill.requestAnimationFrame) { + return [globalThisPolyfill.setTimeout, globalThisPolyfill.clearTimeout] + } + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const scheduler = require('scheduler') as any + return compactScheduler( + [ + scheduler.scheduleCallback || scheduler.unstable_scheduleCallback, + scheduler.cancelCallback || scheduler.unstable_cancelCallback, + scheduler.NormalPriority || scheduler.unstable_NormalPriority + ], + !!scheduler.unstable_requestPaint + ) + } catch (err) { + return [self.requestAnimationFrame, self.cancelAnimationFrame] + } +} + +export const env = { + effectStart: false, + effectSelector: null, + effectEnd: false, + currentActions: null +} + +export const [raf, caf] = getScheduler() + +export const createFormEffects = ( + effects: IFormEffect | null, + actions: Actions +) => { + if (isFn(effects)) { + return (selector: (type: string) => Observable) => { + env.effectEnd = false + env.effectStart = true + env.currentActions = actions + env.effectSelector = ( + type: string, + matcher?: string | ((payload: T) => boolean) + ) => { + const observable$: Observable = selector(type) + if (matcher) { + return observable$.pipe( + filter( + isFn(matcher) + ? matcher + : (payload: T): boolean => { + return FormPath.parse(matcher).match( + payload && (payload as any).name + ) + } + ) + ) + } + return observable$ + } + Object.assign(env.effectSelector, actions) + effects(env.effectSelector, actions) + env.effectStart = false + env.effectEnd = true + env.currentActions = null + } + } else { + return () => {} + } +} + +export const createEffectHook = = any[]>( + type: string +) => (...args: Props): Observable => { + if (!env.effectStart || env.effectEnd) { + throw new Error( + 'EffectHook must be called synchronously within the effects callback function.' + ) + } + if (!env.effectSelector) { + throw new Error('Can not found effect hook selector.') + } + return env.effectSelector(type, ...args) +} + +type FieldMergeState = Partial & Partial + +export const FormEffectHooks = { + onFormWillInit$: createEffectHook( + LifeCycleTypes.ON_FORM_WILL_INIT + ), + onFormInit$: createEffectHook(LifeCycleTypes.ON_FORM_INIT), + onFormChange$: createEffectHook(LifeCycleTypes.ON_FORM_CHANGE), + onFormInputChange$: createEffectHook( + LifeCycleTypes.ON_FORM_INPUT_CHANGE + ), + onFormInitialValueChange$: createEffectHook( + LifeCycleTypes.ON_FORM_INITIAL_VALUES_CHANGE + ), + onFormReset$: createEffectHook(LifeCycleTypes.ON_FORM_RESET), + onFormSubmit$: createEffectHook(LifeCycleTypes.ON_FORM_SUBMIT), + onFormSubmitStart$: createEffectHook( + LifeCycleTypes.ON_FORM_SUBMIT_START + ), + onFormSubmitEnd$: createEffectHook( + LifeCycleTypes.ON_FORM_SUBMIT_END + ), + onFormMount$: createEffectHook(LifeCycleTypes.ON_FORM_MOUNT), + onFormUnmount$: createEffectHook(LifeCycleTypes.ON_FORM_UNMOUNT), + onFormValidateStart$: createEffectHook( + LifeCycleTypes.ON_FORM_VALIDATE_START + ), + onFormValidateEnd$: createEffectHook( + LifeCycleTypes.ON_FORM_VALIDATE_END + ), + onFormValuesChange$: createEffectHook( + LifeCycleTypes.ON_FORM_VALUES_CHANGE + ), + + onFormGraphChange$: createEffectHook( + LifeCycleTypes.ON_FORM_GRAPH_CHANGE + ), + + onFieldWillInit$: createEffectHook( + LifeCycleTypes.ON_FIELD_WILL_INIT + ), + onFieldInit$: createEffectHook(LifeCycleTypes.ON_FIELD_INIT), + onFieldChange$: createEffectHook( + LifeCycleTypes.ON_FIELD_CHANGE + ), + onFieldMount$: createEffectHook( + LifeCycleTypes.ON_FIELD_MOUNT + ), + onFieldUnmount$: createEffectHook( + LifeCycleTypes.ON_FIELD_UNMOUNT + ), + onFieldInputChange$: createEffectHook( + LifeCycleTypes.ON_FIELD_INPUT_CHANGE + ), + onFieldValueChange$: createEffectHook( + LifeCycleTypes.ON_FIELD_VALUE_CHANGE + ), + onFieldInitialValueChange$: createEffectHook( + LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE + ) +} diff --git a/packages/react/src/shared/array.tsx b/packages/react/src/shared/array.tsx deleted file mode 100644 index b113282f63b..00000000000 --- a/packages/react/src/shared/array.tsx +++ /dev/null @@ -1,309 +0,0 @@ -import React from 'react' -import { isFn, getIn, camelCase, isEqual } from '../utils' -import { IFieldProps } from '../type' - -export interface ICircleButtonProps { - onClick: React.MouseEvent - hasText: boolean -} - -export interface IArrayFieldOptions { - TextButton: React.ComponentType - CircleButton: React.ComponentType - AddIcon: React.ComponentType - RemoveIcon: React.ComponentType - MoveDownIcon: React.ComponentType - MoveUpIcon: React.ComponentType -} - -export interface IArrayFieldProps extends IFieldProps { - className?: string -} - -export class ArrayFieldComponent

    extends React.Component

    { - public isActive: (key: string, value: any) => boolean - public onClearErrorHandler: () => () => void - public renderRemove: (index: number, item: any) => React.ReactElement - public renderMoveDown: (index: number, item: any) => React.ReactElement - public renderMoveUp: (index: number) => React.ReactElement - public renderExtraOperations: (index: number) => React.ReactElement - public renderEmpty: (title?: string) => React.ReactElement - public renderAddition: () => React.ReactElement - public getProps: (path?: string) => any -} - -export type TypeArrayField

    = new (props: P, context) => ArrayFieldComponent< - P -> - -export const createArrayField = ( - options: IArrayFieldOptions -): TypeArrayField => { - const { - TextButton, - CircleButton, - AddIcon, - RemoveIcon, - MoveDownIcon, - MoveUpIcon - } = { - TextButton: () =>

    You Should Pass The TextButton.
    , - CircleButton: () =>
    You Should Pass The CircleButton.
    , - AddIcon: () =>
    You Should Pass The AddIcon.
    , - RemoveIcon: () =>
    You Should Pass The RemoveIcon.
    , - MoveDownIcon: () =>
    You Should Pass The MoveDownIcon.
    , - MoveUpIcon: () =>
    You Should Pass The MoveUpIcon.
    , - ...options - } - - return class ArrayFieldComponent extends React.Component { - public isActive = (key: string, value: any): boolean => { - const readOnly: - | boolean - | ((key: string, value: any) => boolean) = this.getProps('readOnly') - const disabled = this.getDisabled() - if (isFn(disabled)) { - return disabled(key, value) - } else if (isFn(readOnly)) { - return readOnly(key, value) - } else { - return !readOnly && !disabled - } - } - - public getApi(index: number) { - const { value } = this.props - return { - index, - isActive: this.isActive, - dataSource: value, - record: value[index], - add: this.onAddHandler(), - remove: this.onRemoveHandler(index), - moveDown: e => { - return this.onMoveHandler( - index, - index + 1 > value.length - 1 ? 0 : index + 1 - )(e) - }, - moveUp: e => { - return this.onMoveHandler( - index, - index - 1 < 0 ? value.length - 1 : index - 1 - )(e) - } - } - } - - public getProps(path?: string): any { - return getIn(this.props.schema, `x-props${path ? '.' + path : ''}`) - } - - public renderWith(name: string, index, defaultRender?) { - const render = this.getProps(camelCase(`render-${name}`)) - if (isFn(index)) { - defaultRender = index - index = 0 - } - if (isFn(render)) { - return render(this.getApi(index)) - } else if (defaultRender) { - return isFn(defaultRender) - ? defaultRender(this.getApi(index), render) - : defaultRender - } - } - - public renderAddition() { - const { locale } = this.props - const { value } = this.props - return ( - this.isActive('addition', value) && - this.renderWith( - 'addition', - ( - { - add - }: { add?: (event: React.MouseEvent) => void } = {}, - text: string - ) => { - return ( -
    - - - {text || locale.addItem || '添加'} - -
    - ) - } - ) - ) - } - - public renderEmpty() { - const { locale, value } = this.props - return ( - value.length === 0 && - this.renderWith('empty', ({ add, isActive }, text) => { - const active = isActive('empty', value) - return ( -
    -
    - - {active && ( - - - {text || locale.addItem || '添加'} - - )} -
    -
    - ) - }) - ) - } - - public renderRemove(index: number, item: any): React.ReactElement { - return ( - this.isActive(`${index}.remove`, item) && - this.renderWith('remove', index, ({ remove }, text) => { - return ( - - - {text && {text}} - - ) - }) - ) - } - - public renderMoveDown(index: number, item: any) { - const { value } = this.props - return ( - value.length > 1 && - this.isActive(`${index}.moveDown`, item) && - this.renderWith('moveDown', index, ({ moveDown }, text) => { - return ( - - - {text} - - ) - }) - ) - } - - public renderMoveUp(index: number) { - const { value } = this.props - return ( - value.length > 1 && - this.isActive(`${index}.moveUp`, value) && - this.renderWith('moveUp', index, ({ moveUp }, text) => { - return ( - - - {text} - - ) - }) - ) - } - - public renderExtraOperations(index: number) { - return this.renderWith('extraOperations', index) - } - - public getDisabled(): boolean | ((key: string, value: any) => boolean) { - const { editable, name } = this.props - const disabled = this.getProps('disabled') - if (editable !== undefined) { - if (isFn(editable)) { - if (!editable(name)) { - return true - } - } else if (editable === false) { - return true - } - } - return disabled - } - - // TODO e 类型 - public onRemoveHandler(index: number): (e: any) => void { - const { value, mutators, schema, locale } = this.props - const { minItems } = schema - return e => { - e.stopPropagation() - if (minItems >= 0 && value.length - 1 < minItems) { - mutators.errors(locale.array_invalid_minItems, minItems) - } else { - mutators.remove(index) - } - } - } - - public onMoveHandler(from: number, to: number): (e: any) => void { - const { mutators } = this.props - return e => { - e.stopPropagation() - mutators.move(from, to) - } - } - - public onAddHandler() { - const { value, mutators, schema, locale } = this.props - const { maxItems } = schema - return e => { - e.stopPropagation() - if (maxItems >= 0 && value.length + 1 > maxItems) { - mutators.errors(locale.array_invalid_maxItems, maxItems) - } else { - mutators.push() - } - } - } - - public onClearErrorHandler() { - return () => { - const { value, mutators, schema } = this.props - const { maxItems, minItems } = schema - if ( - (maxItems >= 0 && value.length <= maxItems) || - (minItems >= 0 && value.length >= minItems) - ) { - mutators.errors() - } - } - } - - public validate() { - const { value, mutators, schema, locale } = this.props - const { maxItems, minItems } = schema - if (value.length > maxItems) { - mutators.errors(locale.array_invalid_maxItems, maxItems) - } else if (value.length < minItems) { - mutators.errors(locale.array_invalid_minItems, minItems) - } else { - mutators.errors() - } - } - - public componentDidUpdate(prevProps) { - if (!isEqual(prevProps.value, this.props.value)) { - this.validate() - } - } - - public componentDidMount() { - this.validate() - } - } -} diff --git a/packages/react/src/shared/broadcast.tsx b/packages/react/src/shared/broadcast.tsx deleted file mode 100644 index 03595faa69d..00000000000 --- a/packages/react/src/shared/broadcast.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import React, { Component, useContext, useMemo, useState } from 'react' -import { IBroadcast, isArr, isFn, isStr } from '@uform/utils' -import { ISelector, IFormActions } from '@uform/types' -import { useEva, createActions } from 'react-eva' -import { Broadcast } from '../utils' -import { BroadcastContext, StateContext } from './context' - -type ChildrenFunction = (broadcast: IBroadcast) => React.ReactNode - -interface IFormProviderProps { - children?: React.ReactNode | ChildrenFunction -} - -export class FormProvider extends Component { - public static displayName = 'FormProvider' - - public broadcast = new Broadcast() - - public componentWillUnmount() { - this.broadcast.unsubscribe() - } - - public render() { - const { children } = this.props - return ( - - {isFn(children) ? children(this.broadcast) : children} - - ) - } -} - -export const FormBridge = () => (Target: React.ComponentType) => { - const Broadcast = props => { - const broadcast = useContext(BroadcastContext) - if (!broadcast) { - return ( - - {broadcast => } - - ) - } - return - } - - Broadcast.displayName = 'FormBroadcast' - - return Broadcast -} - -export interface IOption { - selector?: ((payload: any) => boolean) | string[] | string -} - -export interface IFormState { - status?: any - state?: object - schema?: object -} - -export const useForm = (options: IOption = {}) => { - const [value, setState] = useState({}) - const broadcast = useContext(BroadcastContext) - let initialized = false - let finalValue = value - - useMemo(() => { - if (broadcast) { - broadcast.subscribe(({ type, state, schema, ...others }) => { - if (type !== 'submit' && type !== 'reset') { - if (initialized) { - if (options.selector) { - if ( - (isFn(options.selector) && options.selector({ type, state })) || - (isArr(options.selector) && - options.selector.indexOf(type) > -1) || - (isStr(options.selector) && options.selector === type) - ) { - setState({ - status: type, - state, - schema, - ...others - }) - } - } - } else { - finalValue = { - status: type, - state, - schema, - ...others - } - } - } - }) - initialized = true - } - }, [broadcast]) - - const { status, state, schema } = finalValue as IFormState - - return { - status, - state, - schema, - submit: () => { - if (broadcast) { - broadcast.notify({ type: 'submit' }) - } - }, - reset: () => { - if (broadcast) { - broadcast.notify({ type: 'reset' }) - } - }, - dispatch: (name, payload) => { - if (broadcast) { - broadcast.notify({ type: 'dispatch', name, payload }) - } - } - } -} - -interface IFormControllerOptions { - actions: IFormActions - effects: (selector: ISelector, actions: IFormActions) => void -} - -export const useFormController = ({ - actions, - effects -}: IFormControllerOptions) => { - const { implementActions } = useEva({ actions }) - const context = useContext(StateContext) - const dispatch = useMemo(() => { - if (context && context.form) { - effects(context.form.selectEffect, context.actions) - return context.form.dispatchEffect - } - }, []) - return { - dispatch, - implementActions - } -} - -useFormController.createActions = createActions - -export const FormConsumer = ({ - children, - selector -}: { - // TODO formApi - children: React.ReactElement | ((formApi: any) => React.ReactElement) - selector?: IOption['selector'] -}): React.ReactElement => { - const formApi = useForm({ selector }) - if (!formApi) { - return - } - if (isFn(children)) { - return children(formApi) - } else { - return children || - } -} diff --git a/packages/react/src/shared/context.tsx b/packages/react/src/shared/context.tsx deleted file mode 100644 index db019bed755..00000000000 --- a/packages/react/src/shared/context.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react' -import { Form } from '@uform/core' -import { ISchema, IFormActions } from '@uform/types' -import { IBroadcast } from '@uform/utils' - -export interface IStateContext { - getSchema: (path: string) => ISchema - form: Form - actions: IFormActions - locale: { [key: string]: any } - broadcast: IBroadcast -} - -export const MarkupContext = React.createContext>(null) -export const StateContext = React.createContext>(null) -export const BroadcastContext = React.createContext>(null) diff --git a/packages/react/src/shared/core.tsx b/packages/react/src/shared/core.tsx deleted file mode 100644 index 277768e9cd4..00000000000 --- a/packages/react/src/shared/core.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import * as React from 'react' -import pascalCase from 'pascal-case' - -import { ISchemaFormProps, IFieldProps } from '../type' -import { isFn, isNotEmptyStr, lowercase, each, compose } from '../utils' - -// 最原生的 Form,用到了 DOM 的 form 标签 -export interface INativeFormProps { - component: string - formRef?: React.Ref -} - -export interface IRegisteredFieldsMap { - [name: string]: ComponentWithStyleComponent -} - -export type ComponentWithStyleComponent = React.ComponentType< - ComponentProps -> & { - styledComponentId?: string -} - -let FIELD_WRAPPERS -let FORM_FIELDS -let FIELD_PROPS_TRANSFORMERS -let FIELD_RENDERER -let FORM_COMPONENT - -export const initialContainer = () => { - FIELD_WRAPPERS = [] - FORM_FIELDS = {} - FIELD_PROPS_TRANSFORMERS = {} - FIELD_RENDERER = undefined - FORM_COMPONENT = class extends React.Component { - public static defaultProps = { - component: 'form' - } - - public static displayName = 'Form' - - public render() { - const { formRef, component, ...props } = this.props - return React.createElement(component, { - ...props, - ref: formRef - }) - } - } -} - -export const registerFormField = ( - name: string, - component: ComponentWithStyleComponent, - notWrapper?: boolean -) => { - if ( - isNotEmptyStr(name) && - (isFn(component) || typeof component.styledComponentId === 'string') - ) { - if (notWrapper) { - FORM_FIELDS[lowercase(name)] = component - FORM_FIELDS[lowercase(name)].registerMiddlewares = [] - } else { - FORM_FIELDS[lowercase(name)] = compose( - component, - FIELD_WRAPPERS, - true - ) - FORM_FIELDS[lowercase(name)].registerMiddlewares = FIELD_WRAPPERS - } - FORM_FIELDS[lowercase(name)].displayName = pascalCase(name) - } -} - -export const registerFormFields = (fields: IRegisteredFieldsMap) => { - each(fields, (component, name) => { - registerFormField(name, component) - }) -} - -export const registerFieldMiddleware = (...wrappers: any[]) => { - FIELD_WRAPPERS = FIELD_WRAPPERS.concat(wrappers) - each(FORM_FIELDS, (component, key) => { - if ( - !component.registerMiddlewares.some( - wrapper => wrappers.indexOf(wrapper) > -1 - ) - ) { - FORM_FIELDS[key] = compose( - FORM_FIELDS[key], - wrappers, - true - ) - FORM_FIELDS[key].registerMiddlewares = FIELD_WRAPPERS - } - }) -} - -export const registerFormWrapper = (...wrappers: any[]) => { - FORM_COMPONENT = wrappers.reduce((buf, fn, index) => { - const comp = isFn(fn) ? fn(buf) : buf - comp.displayName = `FormWrapperLevel${index}` - return comp - }, FORM_COMPONENT) -} - -export const registerFieldRenderer = (renderer: React.ComponentType) => { - FIELD_RENDERER = renderer -} - -// TODO transformer type -export const registerFormFieldPropsTransformer = ( - name: string, - transformer -) => { - if (isFn(transformer)) { - FIELD_PROPS_TRANSFORMERS[name] = transformer - } -} - -export const getFormFieldPropsTransformer = (name: string) => - FIELD_PROPS_TRANSFORMERS[name] - -export const getFormField = (name: string) => { - return FORM_FIELDS[name] -} - -export const getFieldRenderer = () => FIELD_RENDERER - -export const OriginForm = React.forwardRef( - (props: ISchemaFormProps, ref: React.Ref) => - React.createElement(FORM_COMPONENT, { ...props, ref }) -) diff --git a/packages/react/src/shared/mutators.ts b/packages/react/src/shared/mutators.ts deleted file mode 100644 index d016ce1db13..00000000000 --- a/packages/react/src/shared/mutators.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { isArr, flatArr, isNum, toArr } from '../utils' - -export const createMutators = props => { - return { - change(value: any): void { - props.form.setValue(props.name, value) - }, - - dispatch(name: string, payload: any) { - props.form.dispatchEffect(name, { - name: props.name, - path: props.path, - payload - }) - }, - - errors(errors: string[] | string, ...args) { - props.form.setErrors(props.name, flatArr(toArr(errors)), ...args) - }, - - push(value: any): void { - const arr = toArr(props.form.getValue(props.name)) - props.form.setValue(props.name, arr.concat(value)) - }, - - pop(): void { - const arr = [].concat(toArr(props.form.getValue(props.name))) - arr.pop() - props.form.setValue(props.name, arr) - }, - - insert(index: number, value: any): void { - const arr = [].concat(toArr(props.form.getValue(props.name))) - arr.splice(index, 0, value) - props.form.setValue(props.name, arr) - }, - - remove(index: number): void { - let val = props.form.getValue(props.name) - if (isNum(index) && isArr(val)) { - val = [].concat(val) - val.splice(index, 1) - props.form.setValue(props.name, val) - } else { - props.form.removeValue(props.name) - } - }, - - unshift(value: any): void { - const arr = [].concat(toArr(props.form.getValue(props.name))) - arr.unshift(value) - props.form.setValue(props.name, arr) - }, - - shift(): void { - const arr = [].concat(toArr(props.form.getValue(props.name))) - arr.shift() - props.form.setValue(props.name, arr) - }, - - move($from: number, $to: number): void { - const arr = [].concat(toArr(props.form.getValue(props.name))) - const item = arr[$from] - arr.splice($from, 1) - arr.splice($to, 0, item) - props.form.setValue(props.name, arr) - } - } -} diff --git a/packages/react/src/shared/virtualbox.tsx b/packages/react/src/shared/virtualbox.tsx deleted file mode 100644 index da7865fc043..00000000000 --- a/packages/react/src/shared/virtualbox.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react' -import pascalCase from 'pascal-case' -import { registerFormField, ComponentWithStyleComponent } from './core' -import { SchemaField } from '../decorators/markup' -import { registerVirtualboxFlag } from '../utils' -import { FormField } from '../state/field' -import { IFieldProps } from '../type' - -export type TVirtualBoxProps = React.PropsWithChildren<{ - name?: string - render?: (fieldProps: IFieldProps) => string | JSX.Element | null -}> - -export const createVirtualBox =

    ( - name: string, - component: ComponentWithStyleComponent, - isController?: boolean -) => { - registerVirtualboxFlag(name) - registerFormField( - name, - class extends React.PureComponent { - public static displayName = 'VirtualBoxWrapper' - - public render() { - const { schema, schemaPath, path, getOrderProperties } = this.props - const parentPath = path.slice(0, path.length - 1) - const properties = getOrderProperties(schema) - const children = properties.map(({ key }) => { - const newPath = parentPath.concat(key) - const newName = newPath.join('.') - const newSchemaPath = schemaPath.concat(key) - return ( - - ) - }) - return React.createElement( - component, - isController ? this.props : (schema['x-props'] as any), - children - ) - } - }, - true - ) - - const VirtualBox = ({ - children, - name: fieldName, - render, - ...props - }: P & TVirtualBoxProps) => ( - - {children} - - ) - - if (component.defaultProps) { - VirtualBox.defaultProps = component.defaultProps - } - - VirtualBox.displayName = pascalCase(name) - - return VirtualBox -} - -export const createControllerBox =

    ( - name: string, - component: ComponentWithStyleComponent -) => createVirtualBox

    (name, component, true) - -export const FormSlot = ({ name, children }) => { - return ( - - ) -} diff --git a/packages/react/src/state/field.tsx b/packages/react/src/state/field.tsx deleted file mode 100644 index 183b1fbc068..00000000000 --- a/packages/react/src/state/field.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import React, { Component, useContext } from 'react' -import { ISchema } from '@uform/types' - -import { - createHOC, - isEqual, - each, - schemaIs, - filterSchema, - lowercase -} from '../utils' -import { createMutators } from '../shared/mutators' -import { StateContext } from '../shared/context' -import { getFieldRenderer, getFormField } from '../shared/core' -import { IStateFieldProps, IStateFieldState, IFieldProps } from '../type' - -const StateField = createHOC((options, Field) => { - class StateField extends Component { - public static displayName = 'StateField' - - private initialized: boolean - private unmounted: boolean - private field: any - // TODO mutators 文件应该暴露出来 interface - private mutators: any - - constructor(props) { - super(props) - this.initialized = false - this.state = {} - this.field = props.form.registerField( - props.name || props.schemaPath.join('.'), - { - path: props.schemaPath, - onChange: this.onChangeHandler(), - props: props.schema - } - ) - this.mutators = createMutators(props) - this.initialized = true - } - - public onChangeHandler() { - return fieldState => { - if (this.unmounted) { - return - } - if (this.initialized) { - this.setState(fieldState) - } else { - // eslint-disable-next-line react/no-direct-mutation-state - this.state = fieldState - } - } - } - - public componentWillUnmount() { - this.unmounted = true - this.field.unmount() - } - - public componentDidMount() { - this.unmounted = false - this.field.mount() - } - - public componentDidUpdate(prevProps) { - if (!isEqual(this.props.schema, prevProps.schema, filterSchema)) { - this.field.changeProps(this.props.schema) - } - } - - public renderField = (key, addReactKey: boolean) => { - const path = this.props.path.concat(key) - const schemaPath = this.props.schemaPath.concat(key) - const name = path.join('.') - - return ( - - ) - } - - public getOrderProperties = (outerSchema?: ISchema) => { - const { schema: innerSchema, path } = this.props - if (!innerSchema && !outerSchema) { - return [] - } - - const properties = [] - each((outerSchema || innerSchema || {}).properties, (item, key) => { - const index = item['x-index'] - const newPath = path.concat(key) - const newName = newPath.join('.') - if (typeof index === 'number') { - properties[index] = { - schema: item, - key, - path: newPath, - name: newName - } - } else { - properties.push({ schema: item, key, path: newPath, name: newName }) - } - }) - return properties.reduce((buf, item) => { - return item ? buf.concat(item) : buf - }, []) - } - - public render() { - const { - name, - path, - schemaPath, - broadcast, - schema, - form, - locale, - getSchema - } = this.props - const { - value, - visible, - display, - props, - errors, - loading, - editable, - required - } = this.state - const newValue = schemaIs(props, 'object') - ? value || {} - : schemaIs(props, 'array') - ? value || [] - : value - //todo: 重置schema children,这里有点恶心,后面重构的时候需要想下怎么重置更合适 - if (schema.properties) { - props.properties = schema.properties - } else if (schema.items) { - props.items = schema.items - } - return visible === false || display === false ? ( - - ) : ( - - ) - } - } - - return props => { - const { name, path, schemaPath } = props - const { form, getSchema, locale, broadcast } = useContext(StateContext) - return ( - - ) - } -}) - -export const FormField = StateField()((props: IFieldProps) => { - const schema = props.schema - const fieldComponentName = lowercase(schema['x-component'] || schema.type) - const renderComponent = schema['x-render'] - ? (innerProps: any) => { - return React.createElement(getFormField(fieldComponentName), { - ...props, - ...innerProps, - schema, - path: props.path, - name: props.name - }) - } - : undefined - - const component = schema['x-render'] - ? getFieldRenderer() - : getFormField(fieldComponentName) - - if (component) { - return React.createElement(component, { ...props, renderComponent }) - } else { - if (console && console.error) { - if (fieldComponentName) { - console.error( - `The schema field \`${fieldComponentName}\`'s component is not found.` - ) - } else { - console.error( - "The schema field's component is not found, or field's schema is not defined." - ) - } - } - return - } -}) diff --git a/packages/react/src/state/form.tsx b/packages/react/src/state/form.tsx deleted file mode 100644 index e5cf43859cf..00000000000 --- a/packages/react/src/state/form.tsx +++ /dev/null @@ -1,317 +0,0 @@ -import React, { Component } from 'react' -import { connect } from 'react-eva' -import { createForm, Form } from '@uform/core' -import { IFormState, IFormActions } from '@uform/types' - -import { - createHOC, - getSchemaNodeFromPath, - isEqual, - isObj, - clone, - isEmpty -} from '../utils' -import { StateContext } from '../shared/context' -import { getFormFieldPropsTransformer } from '../shared/core' -import { FormBridge } from '../shared/broadcast' -import { IStateFormProps } from '../type' - -export const StateForm = createHOC((options, Form) => { - class StateForm extends Component { - public static displayName = 'StateForm' - - public static defaultProps = { - locale: {} - } - - private unmounted: boolean - private initialized: boolean - private lastFormValues: IFormState - private formState: IFormState - private form: Form - private unsubscribe: () => void - - constructor(props) { - super(props) - this.initialized = false - this.form = createForm({ - initialValues: props.defaultValue || props.initialValues, - values: props.value, - effects: props.effects, - subscribes: props.subscribes, - schema: props.schema, - editable: props.editable, - traverse: schema => { - const traverse = - schema && - getFormFieldPropsTransformer(schema['x-component'] || schema.type) - return traverse ? traverse(schema) : schema - }, - onSubmit: this.onSubmitHandler(props), - onFormChange: this.onFormChangeHandler(props), - onFieldChange: this.onFieldChangeHandler(props), - onValidateFailed: this.onValidateFailed(props), - onReset: this.onResetHandler(props), - onFormWillInit: form => { - props.implementActions(this.getActions(form)) - } - }) - this.formState = {} as IFormState - this.initialized = true - } - - public getActions(form: Form): IFormActions { - return { - setFormState: form.setFormState, - getFormState: form.getFormState, - setFieldState: form.setFieldState, - getFieldState: form.getFieldState, - reset: this.reset, - submit: this.submit, - validate: this.validate, - getSchema: this.getSchema, - dispatch: this.dispatch - } - } - - public notify(payload) { - const { broadcast, schema } = this.props - if (broadcast) { - payload.schema = schema - broadcast.notify(payload) - } - } - - public onValidateFailed = $props => { - return (...args) => { - const props = this.props || $props - if (props.onValidateFailed) { - return props.onValidateFailed(...args) - } - } - } - - public onFormChangeHandler(props) { - let lastState = this.formState - return ({ formState }) => { - if (this.unmounted) { - return - } - if (lastState && lastState.pristine !== formState.pristine) { - if (lastState.pristine) { - this.notify({ - type: 'changed', - state: formState - }) - } else { - this.notify({ - type: 'reseted', - state: formState - }) - } - } - - lastState = formState - - // eslint-disable-next-line react/no-direct-mutation-state - this.formState = formState - - if (!this.initialized) { - this.notify({ - type: 'initialize', - state: formState - }) - } - } - } - - public onFieldChangeHandler = $props => { - return ({ formState }) => { - const props = this.props || $props - if (props.onChange) { - const values = formState.values - if (!isEqual(this.lastFormValues, values)) { - props.onChange(values) - this.lastFormValues = clone(values) - } - } - } - } - - public getSchema = path => { - return getSchemaNodeFromPath(this.props.schema, path) - } - - public onSubmitHandler = $props => { - return ({ formState }) => { - const props = this.props || $props - if (props.onSubmit) { - const promise = props.onSubmit(clone(formState.values)) - if (promise && promise.then) { - this.notify({ - type: 'submitting', - state: this.formState - }) - promise.then( - () => { - this.notify({ - type: 'submitted', - state: this.formState - }) - }, - error => { - this.notify({ - type: 'submitted', - state: this.formState - }) - throw error - } - ) - } - } - } - } - - public onResetHandler($props) { - return ({ formState }) => { - const props = this.props || $props - if (props.onReset) { - props.onReset(clone(formState.values)) - } - } - } - - public componentDidUpdate(prevProps) { - const { value, editable, initialValues } = this.props - if (!isEmpty(value) && !isEqual(value, prevProps.value)) { - this.form.changeValues(value) - } else if (this.form.isDirtyValues(value)) { - this.form.changeValues(value) - } - if ( - !isEmpty(initialValues) && - !isEqual(initialValues, prevProps.initialValues) - ) { - this.form.initialize({ - values: initialValues, - initialValues - }) - } - if (!isEmpty(editable) && !isEqual(editable, prevProps.editable)) { - this.form.changeEditable(editable) - } - } - - public componentDidMount() { - this.unmounted = false - this.form.dispatchEffect('onFormMount', this.form.publishState()) - - this.unsubscribe = this.props.broadcast.subscribe( - ({ type, name, payload }) => { - if (this.unmounted) { - return - } - if (type === 'submit') { - this.submit() - } else if (type === 'reset') { - this.reset() - } else if (type === 'dispatch') { - this.form.dispatchEffect(name, payload) - } - } - ) - } - - public componentWillUnmount() { - this.unmounted = true - if (this.form) { - this.form.destructor() - this.unsubscribe() - delete this.form - } - } - - public onNativeSubmitHandler = e => { - if (e.preventDefault) { - e.stopPropagation() - e.preventDefault() - } - this.form.submit().catch(e => { - if (console && console.error) { - console.error(e) - } - }) - } - - public getValues = () => { - return this.form.getValue() - } - - public submit = () => { - return this.form.submit() - } - - public reset = ( - params?: boolean | { forceClear?: boolean; validate?: boolean }, - validate: boolean = true - ) => { - let forceClear: boolean - if (isObj(params)) { - forceClear = !!params.forceClear - validate = !isEmpty(params.validate) ? params.validate : validate - } - this.form.reset(forceClear, validate) - } - - public validate = () => { - return this.form.validate() - } - - public dispatch = (type, payload) => { - this.form.dispatchEffect(type, payload) - } - - public render() { - /* eslint-disable @typescript-eslint/no-unused-vars */ - const { - onSubmit, - onChange, - onReset, - onValidateFailed, - initialValues, - defaultValue, - effects, - implementActions, - dispatch, - editable, - subscribes, - subscription, - children, - schema, - broadcast, - locale, - value, - ...others - } = this.props - /* eslint-enable @typescript-eslint/no-unused-vars */ - - return ( - -

    - {children} -
    - - ) - } - } - - return connect({ autoRun: false })(FormBridge()(StateForm)) -}) diff --git a/packages/react/src/state/index.tsx b/packages/react/src/state/index.tsx deleted file mode 100644 index 101fadf7015..00000000000 --- a/packages/react/src/state/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { registerFormWrapper } from '../shared/core' -import { StateForm } from './form' - -export default () => { - registerFormWrapper(StateForm()) -} diff --git a/packages/react/src/type.ts b/packages/react/src/type.ts deleted file mode 100644 index 572efd5c194..00000000000 --- a/packages/react/src/type.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { IFormOptions, IFormPayload, ISchema, IField } from '@uform/types' -import { Form } from '@uform/core' -import { IBroadcast } from '@uform/utils' - -export interface IEventTargetOption { - selected: boolean - value: any -} - -export interface IEnhanceSchema extends ISchema { - renderChildren?: React.ReactElement -} - -export interface IFieldProps - extends Omit, - IStateFieldProps { - state?: string - size?: string - children?: React.ReactNode - schema: IEnhanceSchema - getOrderProperties: (schema?: ISchema) => any - renderField: (key, addReactKey?: boolean) => React.ReactElement - editable: boolean | ((name: string) => boolean) -} - -export interface IStateFieldProps { - name: string - schema: ISchema - path: string[] - schemaPath: any - locale: { [key: string]: any } - getSchema: (path: string) => ISchema - broadcast: IBroadcast - form: Form - // TODO mutators 文件应该暴露出来 interface - mutators?: any -} - -export interface IStateFieldState { - value?: any - props?: any - errors?: any - visible?: boolean - display?: boolean - loading?: boolean - editable?: boolean - required?: boolean -} - -export interface ISchemaFormProps extends IFormOptions { - className?: string - children?: React.ReactNode - value?: V - onChange?: (payload: IFormPayload) => void -} - -export interface IStateFormProps extends ISchemaFormProps { - broadcast: IBroadcast - - // eva - implementActions: (actions: object) => object - dispatch: (type: string, ...args: any) => void - subscription: () => void - - // ConfigProvider - locale: { [key: string]: any } -} diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts new file mode 100644 index 00000000000..2a1ac48ce5e --- /dev/null +++ b/packages/react/src/types.ts @@ -0,0 +1,181 @@ +import React from 'react' +import { + IFieldStateProps, + IVirtualFieldStateProps, + IForm, + IMutators, + IFieldState, + IFormValidateResult, + IFormState, + IFormResetOptions, + IFormSubmitResult, + FormHeartSubscriber, + IFormGraph +} from '@uform/core' +import { FormPathPattern } from '@uform/shared' +import { Observable } from 'rxjs/internal/Observable' +export interface IFormEffect { + ( + selector: IFormExtendsEffectSelector, + actions: Actions + ): void +} + +export interface IFormEffectSelector { + ( + type: string, + matcher?: string | ((payload: Payload) => boolean) + ): Observable +} + +export type IFormExtendsEffectSelector< + Payload = any, + Actions = {} +> = IFormEffectSelector & Actions + +export interface IFormProps< + Value = {}, + DefaultValue = {}, + EffectPayload = any, + EffectActions = {} +> { + value?: Value + initialValues?: DefaultValue + actions?: EffectActions + effects?: IFormEffect + form?: IForm + onChange?: (values: Value) => void + onSubmit?: (values: Value) => void | Promise + onReset?: () => void + onValidateFailed?: (valideted: IFormValidateResult) => void + children?: React.ReactElement | ((form: IForm) => React.ReactElement) + useDirty?: boolean + editable?: boolean + validateFirst?: boolean +} + +export interface IFieldAPI { + state: IFieldState + form: IForm + props: {} + mutators: IMutators +} + +export interface IVirtualFieldAPI { + state: IFieldState + form: IForm + props: {} +} + +export interface IFieldProps extends IFieldStateProps { + triggerType?: 'onChange' | 'onBlur' + getValueFromEvent?: (...args: any[]) => any + children?: React.ReactElement | ((api: IFieldAPI) => React.ReactElement) +} + +export interface IVirtualFieldProps extends IVirtualFieldStateProps { + children?: + | React.ReactElement + | ((api: IVirtualFieldAPI) => React.ReactElement) +} + +export interface IFormSpyAPI { + form: IForm + type: string + state: any +} + +export interface IFormSpyProps { + selector?: string[] | string + reducer?: ( + state: any, + action: { type: string; payload: any }, + form: IForm + ) => any + children?: React.ReactElement | ((api: IFormSpyAPI) => React.ReactElement) +} + +export interface IFormConsumerAPI { + status: string + state: IFormState + submit: IForm['submit'] + reset: IForm['reset'] +} + +export interface IFormConsumerProps { + selector?: string[] | string + children?: + | React.ReactElement + | ((api: IFormConsumerAPI) => React.ReactElement) +} + +export interface IFieldHook { + form: IForm + state: IFieldState + props: {} + mutators: IMutators +} + +export interface IVirtualFieldHook { + form: IForm + state: IFieldState + props: {} +} + +export interface IFormActions { + submit( + onSubmit?: (values: IFormState['values']) => void | Promise + ): Promise + reset(options?: IFormResetOptions): void + validate(path?: FormPathPattern, options?: {}): Promise + setFormState(callback?: (state: IFormState) => any): void + getFormState(callback?: (state: IFormState) => any): any + clearErrors: (pattern?: FormPathPattern) => void + setFieldState( + path: FormPathPattern, + callback?: (state: IFieldState) => void + ): void + getFieldState( + path: FormPathPattern, + callback?: (state: IFieldState) => any + ): any + getFormGraph(): IFormGraph + setFormGraph(graph: IFormGraph): void + subscribe(callback?: FormHeartSubscriber): void + unsubscribe(callback?: FormHeartSubscriber): void + notify: (type: string, payload: T) => void + dispatch: (type: string, payload: T) => void + setFieldValue(path?: FormPathPattern, value?: any): void + getFieldValue(path?: FormPathPattern): any + setFieldInitialValue(path?: FormPathPattern, value?: any): void + getFieldInitialValue(path?: FormPathPattern): any +} + +export interface IFormAsyncActions { + submit( + onSubmit?: (values: IFormState['values']) => void | Promise + ): Promise + reset(options?: IFormResetOptions): Promise + clearErrors: (pattern?: FormPathPattern) => Promise + validate(path?: FormPathPattern, options?: {}): Promise + setFormState(callback?: (state: IFormState) => any): Promise + getFormState(callback?: (state: IFormState) => any): Promise + setFieldState( + path: FormPathPattern, + callback?: (state: IFieldState) => void + ): Promise + getFieldState( + path: FormPathPattern, + callback?: (state: IFieldState) => any + ): Promise + getFormGraph(): Promise + setFormGraph(graph: IFormGraph): Promise + subscribe(callback?: FormHeartSubscriber): Promise + unsubscribe(callback?: FormHeartSubscriber): Promise + notify: (type: string, payload: T) => Promise + dispatch: (type: string, payload: T) => void + setFieldValue(path?: FormPathPattern, value?: any): Promise + getFieldValue(path?: FormPathPattern): Promise + setFieldInitialValue(path?: FormPathPattern, value?: any): Promise + getFieldInitialValue(path?: FormPathPattern): Promise +} diff --git a/packages/react/src/utils.tsx b/packages/react/src/utils.tsx deleted file mode 100644 index f2d2e7bc2ec..00000000000 --- a/packages/react/src/utils.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react' -import { reduce, isFn, isStr, isArr } from '@uform/utils' - -export * from '@uform/utils' - -export const isNum = (value: string | number): boolean => - typeof value === 'number' - -export const isNotEmptyStr = (str: string): boolean => !!(isStr(str) && str) - -export const flatArr = (arr: any[]) => - arr.reduce((buf, item) => { - return isArr(item) ? buf.concat(flatArr(item)) : buf.concat(item) - }, []) - -export const compose = (payload: any, args: any[], revert: boolean) => - reduce( - args, - (buf, fn: any) => { - return isFn(fn) ? fn(buf) : buf - }, - payload, - revert - ) - -export const createHOC = (wrapper?: (options: object, Target) => any) => ( - options?: object -) => (Target: React.ComponentType) => { - return wrapper({ ...options }, Target) -} - -export const filterSchema = (_, key): boolean => - ['items', 'properties'].indexOf(key) < 0 - -export const filterSchemaPropertiesAndReactChildren = (_, key): boolean => { - return ['items', 'properties', 'children'].indexOf(key) < 0 -} diff --git a/packages/utils/.npmignore b/packages/shared/.npmignore similarity index 100% rename from packages/utils/.npmignore rename to packages/shared/.npmignore diff --git a/packages/utils/LICENSE.md b/packages/shared/LICENSE.md similarity index 100% rename from packages/utils/LICENSE.md rename to packages/shared/LICENSE.md diff --git a/packages/utils/README.md b/packages/shared/README.md similarity index 57% rename from packages/utils/README.md rename to packages/shared/README.md index a4984e5d18d..83e6261b3c4 100644 --- a/packages/utils/README.md +++ b/packages/shared/README.md @@ -1,2 +1,2 @@ -# @uform/utils +# @uform/shared > UForm工具函数集 \ No newline at end of file diff --git a/packages/shared/jest.config.js b/packages/shared/jest.config.js new file mode 100644 index 00000000000..dfc970503b3 --- /dev/null +++ b/packages/shared/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../scripts/jest.base') diff --git a/packages/types/package.json b/packages/shared/package.json similarity index 68% rename from packages/types/package.json rename to packages/shared/package.json index dd9878ab43d..fdf93918d05 100644 --- a/packages/types/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { - "name": "@uform/types", - "version": "0.4.3", + "name": "@uform/shared", + "version": "0.4.0", "license": "MIT", "main": "lib", "types": "lib/index.d.ts", @@ -15,6 +15,10 @@ "engines": { "npm": ">=3.0.0" }, + "publishConfig": { + "access": "public" + }, + "gitHead": "f513fc2dcca781b3f7aa588c4419bce20cba2d8b", "scripts": { "build": "tsc --declaration" }, @@ -22,14 +26,12 @@ "typescript": "^3.5.2" }, "peerDependencies": { - "@babel/runtime": "^7.4.4", - "rxjs": "^6.3.3" + "@babel/runtime": "^7.4.4" }, "dependencies": { - "rxjs": "^6.3.3" - }, - "publishConfig": { - "access": "public" - }, - "gitHead": "f513fc2dcca781b3f7aa588c4419bce20cba2d8b" + "@uform/types": "^0.4.0", + "camel-case": "^3.0.0", + "cool-path": "^0.1.1", + "deepmerge": "^4.0.0" + } } diff --git a/packages/shared/src/__tests__/index.spec.ts b/packages/shared/src/__tests__/index.spec.ts new file mode 100644 index 00000000000..8b0cfc66ae2 --- /dev/null +++ b/packages/shared/src/__tests__/index.spec.ts @@ -0,0 +1,116 @@ +import { isEqual } from '../compare' +import { toArr, every, some, findIndex, find, includes } from '../array' +import { clone } from '../clone' + + +test('toArr', () => { + expect(isEqual(toArr([123]), [123])).toBeTruthy() + expect(isEqual(toArr(123), [123])).toBeTruthy() + expect(isEqual(toArr(null), [])).toBeTruthy() +}) + +test('clone form data', () => { + var dd = new Map() + dd.set('aaa', { bb: 123 }) + var a = { + aa: 123123, + bb: [{ bb: 111 }, { bb: 222 }], + cc: () => { + // eslint-disable-next-line no-console + console.log('123') + }, + dd + } + var 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).toBeFalsy() + expect(a.dd.get('aaa') === cloned.dd.get('aaa')).toBeTruthy() + expect(a.cc === cloned.cc).toBeTruthy() +}) + +test('filter equal', () => { + var a = { + aa: { + bb: 123 + } + } + var b = { + aa: { + bb: 123 + } + } + + expect(isEqual(a, b)).toBeTruthy() + expect(isEqual(a, b, (_, key) => key !== 'aa')).toBeTruthy() +}) + +test('filter clone', () => { + var a = { + aa: { + bb: 123 + }, + cc: { + dd: [1, 3, 4, 5] + } + } + + var b = clone(a, (_, key) => key !== 'aa') + + expect(a.aa === b.aa).toBeTruthy() + expect(a.cc === b.cc).toBeFalsy() + expect(isEqual(a.cc, b.cc)).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() +}) diff --git a/packages/utils/src/array.ts b/packages/shared/src/array.ts similarity index 71% rename from packages/utils/src/array.ts rename to packages/shared/src/array.ts index 042580e08b8..a5b8e5981a0 100644 --- a/packages/utils/src/array.ts +++ b/packages/shared/src/array.ts @@ -1,11 +1,20 @@ -import { isArr, isObj, isStr } from '@uform/types' +import { isArr, isObj, isStr } from './types' type EachArrayIterator = (currentValue: T, key: number) => void | boolean type EachStringIterator = (currentValue: string, key: number) => void | boolean -type EachObjectIterator = ( - currentValue: T[keyof T], +type EachObjectIterator = ( + currentValue: T, key: string ) => void | boolean +type MapArrayIterator = ( + currentValue: TItem, + key: number +) => TResult +type MapStringIterator = (currentValue: string, key: number) => TResult +type MapObjectIterator = ( + currentValue: TItem, + key: string +) => TResult type MemoArrayIterator = ( previousValue: U, currentValue: T, @@ -16,11 +25,11 @@ type MemoStringIterator = ( currentValue: string, key: number ) => T -type MemoObjectIterator = ( - previousValue: U, - currentValue: T[keyof T], +type MemoObjectIterator = ( + previousValue: TResult, + currentValue: TValue, key: string -) => U +) => TResult export const toArr = (val: any): any[] => (isArr(val) ? val : val ? [val] : []) @@ -34,12 +43,12 @@ export function each( iterator: EachArrayIterator, revert?: boolean ): void -export function each( +export function each( val: T, - iterator: EachObjectIterator, + iterator: EachObjectIterator, revert?: boolean ): void -export function each(val: any, iterator: any, revert?: boolean): object { +export function each(val: any, iterator: any, revert?: boolean): void { if (isArr(val) || isStr(val)) { if (revert) { for (let i: number = val.length - 1; i >= 0; i--) { @@ -48,7 +57,7 @@ export function each(val: any, iterator: any, revert?: boolean): object { } } } else { - for (let i = 0, length = val.length; i < length; i++) { + for (let i = 0; i < val.length; i++) { if (iterator(val[i], i) === false) { return } @@ -66,21 +75,21 @@ export function each(val: any, iterator: any, revert?: boolean): object { } } -export function map( +export function map( val: string, - iterator: EachStringIterator, + iterator: MapStringIterator, revert?: boolean -): string[] -export function map( - val: T[], - iterator: EachArrayIterator, +): any +export function map( + val: TItem[], + iterator: MapArrayIterator, revert?: boolean -): T[] -export function map( +): any +export function map( val: T, - iterator: EachObjectIterator, + iterator: MapObjectIterator, revert?: boolean -): object +): any export function map(val: any, iterator: any, revert?: boolean): any { const res = isArr(val) || isStr(val) ? [] : {} each( @@ -110,12 +119,12 @@ export function reduce( accumulator?: T, revert?: boolean ): T -export function reduce( +export function reduce( val: T, - iterator: MemoObjectIterator, - accumulator?: U, + iterator: MemoObjectIterator, + accumulator?: TResult, revert?: boolean -): U +): TResult export function reduce( val: any, iterator: any, @@ -133,8 +142,8 @@ export function reduce( return result } -export function every( - val: string, +export function every( + val: T, iterator: EachStringIterator, revert?: boolean ): boolean @@ -143,9 +152,9 @@ export function every( iterator: EachArrayIterator, revert?: boolean ): boolean -export function every( +export function every( val: T, - iterator: EachObjectIterator, + iterator: EachObjectIterator, revert?: boolean ): boolean export function every(val: any, iterator: any, revert?: boolean): boolean { @@ -163,8 +172,8 @@ export function every(val: any, iterator: any, revert?: boolean): boolean { return res } -export function some( - val: string, +export function some( + val: T, iterator: EachStringIterator, revert?: boolean ): boolean @@ -173,9 +182,9 @@ export function some( iterator: EachArrayIterator, revert?: boolean ): boolean -export function some( +export function some( val: T, - iterator: EachObjectIterator, + iterator: EachObjectIterator, revert?: boolean ): boolean export function some(val: any, iterator: any, revert?: boolean): boolean { @@ -193,8 +202,8 @@ export function some(val: any, iterator: any, revert?: boolean): boolean { return res } -export function findIndex( - val: string, +export function findIndex( + val: T, iterator: EachStringIterator, revert?: boolean ): number @@ -203,9 +212,9 @@ export function findIndex( iterator: EachArrayIterator, revert?: boolean ): number -export function findIndex( +export function findIndex( val: T, - iterator: EachObjectIterator, + iterator: EachObjectIterator, revert?: boolean ): keyof T export function findIndex( @@ -227,8 +236,8 @@ export function findIndex( return res } -export function find( - val: string, +export function find( + val: T, iterator: EachStringIterator, revert?: boolean ): any @@ -237,9 +246,9 @@ export function find( iterator: EachArrayIterator, revert?: boolean ): T -export function find( +export function find( val: T, - iterator: EachObjectIterator, + iterator: EachObjectIterator, revert?: boolean ): T[keyof T] export function find(val: any, iterator: any, revert?: boolean): any { @@ -257,8 +266,8 @@ export function find(val: any, iterator: any, revert?: boolean): any { return res } -export function includes( - val: string[], +export function includes( + val: T, searchElement: string, revert?: boolean ): boolean @@ -266,6 +275,8 @@ export function includes( val: T[], searchElement: T, revert?: boolean -): boolean { +): boolean +export function includes(val: any, searchElement: any, revert?: boolean) { + if (isStr(val)) return val.includes(searchElement) return some(val, item => item === searchElement, revert) } diff --git a/packages/utils/src/broadcast.ts b/packages/shared/src/broadcast.ts similarity index 58% rename from packages/utils/src/broadcast.ts rename to packages/shared/src/broadcast.ts index d2764fe5683..d96454511bf 100644 --- a/packages/utils/src/broadcast.ts +++ b/packages/shared/src/broadcast.ts @@ -1,5 +1,5 @@ -import { isFn } from '@uform/types' import { each } from './array' +import { isFn } from './types' type Subscriber = (notification: N) => void @@ -10,12 +10,13 @@ const noop = () => undefined // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface IBroadcast extends Broadcast {} -export class Broadcast { +export class Broadcast { private entries = [] private buffer = [] private length: number + private context: any - public subscribe(subscriber: Subscriber, subscription?: any) { + public subscribe(subscriber: Subscriber, subscription?: any) { if (!isFn(subscriber)) { return noop } @@ -30,9 +31,23 @@ export class Broadcast { } } - public unsubscribe() { - this.entries.length = 0 - this.buffer.length = 0 + public setContext(context?: any) { + this.context = context + } + + public getContext() { + return this.context + } + + public unsubscribe(subscriber?: Subscriber) { + if (subscriber) { + this.entries = this.entries.filter(suber => { + return suber.subscriber !== subscriber + }) + } else { + this.entries.length = 0 + this.buffer.length = 0 + } } public flushBuffer({ subscriber, subscription }) { @@ -40,15 +55,15 @@ export class Broadcast { if (isFn(filter)) { const notification = filter(payload, subscription) if (notification !== undefined) { - subscriber(notification) + subscriber.call(this.context, notification) } } else { - subscriber(payload, subscription) + subscriber.call(this.context, payload, subscription) } }) } - public notify(payload: P, filter?: Filter) { + public notify(payload: Payload, filter?: Filter) { if (this.length === 0) { this.buffer.push({ payload, filter }) return @@ -57,10 +72,10 @@ export class Broadcast { if (isFn(filter)) { const notification = filter(payload, subscription) if (notification !== undefined) { - subscriber(notification) + subscriber.call(this.context, notification) } } else { - subscriber(payload, subscription) + subscriber.call(this.context, payload, subscription) } }) this.buffer.length = 0 diff --git a/packages/utils/src/case.ts b/packages/shared/src/case.ts similarity index 100% rename from packages/utils/src/case.ts rename to packages/shared/src/case.ts diff --git a/packages/utils/src/clone.ts b/packages/shared/src/clone.ts similarity index 88% rename from packages/utils/src/clone.ts rename to packages/shared/src/clone.ts index ab7c432cd8a..60a61b676c6 100644 --- a/packages/utils/src/clone.ts +++ b/packages/shared/src/clone.ts @@ -1,5 +1,5 @@ -import { isFn } from '@uform/types' -import { globalThisPolyfill } from './globalThis' +import { isFn } from './types' +import { globalThisPolyfill } from './global' type Filter = (value: any, key: string) => boolean @@ -55,6 +55,15 @@ export const clone = (values: any, filter?: Filter) => { if (values._isAMomentObject) { return values } + if (values._isJSONSchemaObject) { + return values + } + if (isFn(values.toJS)) { + return values + } + if (isFn(values.toJSON)) { + return values + } if (Object.getOwnPropertySymbols(values || {}).length) { return values } diff --git a/packages/utils/src/compare.ts b/packages/shared/src/compare.ts similarity index 60% rename from packages/utils/src/compare.ts rename to packages/shared/src/compare.ts index 6f7b25f688e..b295f41e299 100644 --- a/packages/utils/src/compare.ts +++ b/packages/shared/src/compare.ts @@ -1,4 +1,4 @@ -import { isFn, isArr } from '@uform/types' +import { isFn, isArr } from './types' const isArray = isArr const keyList = Object.keys const hasProp = Object.prototype.hasOwnProperty @@ -6,9 +6,11 @@ const hasProp = Object.prototype.hasOwnProperty type Filter = (comparies: { a: any; b: any }, key: string) => boolean /* eslint-disable */ -function equal(a: any, b: any, filter: Filter) { +function equal(a: any, b: any, filter?: Filter) { // fast-deep-equal index.js 2.0.1 - if (a === b) { return true } + if (a === b) { + return true + } if (a && b && typeof a === 'object' && typeof b === 'object') { const arrA = isArray(a) @@ -19,36 +21,73 @@ function equal(a: any, b: any, filter: Filter) { if (arrA && arrB) { length = a.length - if (length !== b.length) { return false } - for (i = length; i-- !== 0;) { if (!equal(a[i], b[i], filter)) { return false } } + if (length !== b.length) { + return false + } + for (i = length; i-- !== 0; ) { + if (!equal(a[i], b[i], filter)) { + return false + } + } return true } - if (arrA !== arrB) { return false } + if (arrA !== arrB) { + return false + } + const momentA = a && a._isAMomentObject + const momentB = b && b._isAMomentObject + if (momentA !== momentB) return false + if (momentA && momentB) return a.isSame(b) + const immutableA = a && a.toJS + const immutableB = b && b.toJS + if (immutableA !== immutableB) return false + if (immutableA) return a.is ? a.is(b) : a === b + const schemaA = a && a.toJSON + const schemaB = b && b.toJSON + if (schemaA !== schemaB) return false + if (schemaA && schemaB) return equal(a.toJSON(), b.toJSN(), filter) const dateA = a instanceof Date const dateB = b instanceof Date - if (dateA !== dateB) { return false } - if (dateA && dateB) { return a.getTime() === b.getTime() } + if (dateA !== dateB) { + return false + } + if (dateA && dateB) { + return a.getTime() === b.getTime() + } const regexpA = a instanceof RegExp const regexpB = b instanceof RegExp - if (regexpA !== regexpB) { return false } - if (regexpA && regexpB) { return a.toString() === b.toString() } + if (regexpA !== regexpB) { + return false + } + if (regexpA && regexpB) { + return a.toString() === b.toString() + } const urlA = a instanceof URL const urlB = b instanceof URL - if (urlA && urlB) { return a.href === b.href } + if (urlA && urlB) { + return a.href === b.href + } const keys = keyList(a) length = keys.length - if (length !== keyList(b).length) { return false } + if (length !== keyList(b).length) { + return false + } - for (i = length; i-- !== 0;) { if (!hasProp.call(b, keys[i])) { return false } } + for (i = length; i-- !== 0; ) { + if (!hasProp.call(b, keys[i])) { + return false + } + } // end fast-deep-equal // Custom handling for React - for (i = length; i-- !== 0;) { + for (i = length; i-- !== 0; ) { key = keys[i] + if (key === '_owner' && a.$$typeof) { // React-specific: avoid traversing React elements' _owner. // _owner contains circular references @@ -58,11 +97,15 @@ function equal(a: any, b: any, filter: Filter) { } else { if (isFn(filter)) { if (filter({ a: a[key], b: b[key] }, key)) { - if (!equal(a[key], b[key], filter)) { return false } + if (!equal(a[key], b[key], filter)) { + return false + } } } else { // all other properties should be traversed as usual - if (!equal(a[key], b[key], filter)) { return false } + if (!equal(a[key], b[key], filter)) { + return false + } } } } diff --git a/packages/shared/src/deprecate.ts b/packages/shared/src/deprecate.ts new file mode 100644 index 00000000000..f3aefb8a102 --- /dev/null +++ b/packages/shared/src/deprecate.ts @@ -0,0 +1,22 @@ +import { isFn, isStr } from './types' + +export const deprecate = ( + method: any, + message?: string, + help?: string +) => { + if (isFn(method)) { + return function(p1?: P1, p2?: P2, p3?: P3, p4?: P4, p5?: P5) { + deprecate(message, help) + method.apply(this, arguments) + } + } + if (isStr(method)) { + console.error( + new Error( + `${method} has been deprecated. Do not continue to use this api.${message || + ''}` + ) + ) + } +} diff --git a/packages/shared/src/global.ts b/packages/shared/src/global.ts new file mode 100644 index 00000000000..40b3d949c98 --- /dev/null +++ b/packages/shared/src/global.ts @@ -0,0 +1,19 @@ +function globalThis() { + try { + if (typeof self !== 'undefined') { + return self + } + } catch (e) {} + try { + if (typeof window !== 'undefined') { + return window + } + } catch (e) {} + try { + if (typeof global !== 'undefined') { + return global + } + } catch (e) {} + return Function('return this')() +} +export const globalThisPolyfill = globalThis() diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 00000000000..79e1ef38ad6 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,12 @@ +export * from './array' +export * from './compare' +export * from './types' +export * from './clone' +export * from './isEmpty' +export * from './case' +export * from './string' +export * from './global' +export * from './path' +export * from './deprecate' +export * from './broadcast' +export * from './merge' diff --git a/packages/utils/src/isEmpty.ts b/packages/shared/src/isEmpty.ts similarity index 96% rename from packages/utils/src/isEmpty.ts rename to packages/shared/src/isEmpty.ts index 173787c9787..6d4ee498a3c 100644 --- a/packages/utils/src/isEmpty.ts +++ b/packages/shared/src/isEmpty.ts @@ -2,6 +2,8 @@ const has = Object.prototype.hasOwnProperty const toString = Object.prototype.toString +export const isValid = (val: any) => val !== undefined + export function isEmpty(val: any): boolean { // Null and Undefined... if (val == null) { diff --git a/packages/shared/src/merge.ts b/packages/shared/src/merge.ts new file mode 100644 index 00000000000..c7924efb947 --- /dev/null +++ b/packages/shared/src/merge.ts @@ -0,0 +1,2 @@ +import deeepmerge from 'deepmerge' +export const merge = deeepmerge diff --git a/packages/shared/src/path.ts b/packages/shared/src/path.ts new file mode 100644 index 00000000000..1a4a1d55d93 --- /dev/null +++ b/packages/shared/src/path.ts @@ -0,0 +1,3 @@ +import FormPath, { Pattern as FormPathPattern } from 'cool-path' + +export { FormPath, FormPathPattern } diff --git a/packages/utils/src/stringLength.ts b/packages/shared/src/string.ts similarity index 100% rename from packages/utils/src/stringLength.ts rename to packages/shared/src/string.ts diff --git a/packages/types/src/types.ts b/packages/shared/src/types.ts similarity index 100% rename from packages/types/src/types.ts rename to packages/shared/src/types.ts diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 00000000000..1d669c29c46 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./lib" + }, + "include": ["./src/**/*.ts", "./src/**/*.tsx"], + "exclude": ["./src/__tests__/*"] +} diff --git a/packages/types/README.md b/packages/types/README.md deleted file mode 100644 index fcb94f48de0..00000000000 --- a/packages/types/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# @uform/validator -> UForm数据校验工具 \ No newline at end of file diff --git a/packages/types/src/effects.ts b/packages/types/src/effects.ts deleted file mode 100644 index 1f30157e479..00000000000 --- a/packages/types/src/effects.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IFormActions, IFormPathMatcher } from './form' -import { Observable } from 'rxjs/internal/Observable' -export type Dispatcher = (eventName: string, payload: any) => void -export type IEffects = (selector: ISelector, actions: IFormActions) => void - -export type ISelector = ( - eventName: string, - formPathPattern?: string | IFormPathMatcher -) => Observable diff --git a/packages/types/src/field.ts b/packages/types/src/field.ts deleted file mode 100644 index 7741058ac89..00000000000 --- a/packages/types/src/field.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { IRuleDescription } from './rule' -import { ISchema } from './schema' -import { Path } from './path' -import { IFormPathMatcher } from './form' - -export interface IField { - value: V - valid: boolean - dirty: boolean - invalid: boolean - visible: boolean - display: boolean - hiddenFromParent: boolean - shownFromParent: boolean - required: boolean - editable: boolean - loading: boolean - errors: string[] - effectErrors: string[] - pristine: boolean - initialValue: V - name: string - path: string[] - props: ISchema - rules: IRuleDescription[] - dirtyType: string - lastValidateValue: V - notify: (forceUpdate?: boolean) => void - changeEditable: (editable: boolean | ((name: string) => boolean)) => void - match: (path: Path | IFormPathMatcher) => boolean - initialize: (options: IFieldOptions) => void - publishState: () => IFieldState - onChange: (fn: () => void) => void - updateState: (fn: (state: IFieldState) => void) => void - destructor: () => void - syncContextValue: () => void - pathEqual: (path: Path | IFormPathMatcher) => boolean -} - -export interface IFieldOptions { - path: Path - name?: string - props: any - value?: any - initialValue?: any - onChange?: (...args: any[]) => void -} - -export interface IFieldState { - value: any - valid: boolean - invalid: boolean - visible: boolean - required: boolean - editable: boolean - loading: boolean - errors: string[] - pristine: boolean - initialValue: any - name: string - path: string[] - props: ISchema - rules: IRuleDescription[] -} - -export interface IFieldMap { - [name: string]: IField -} diff --git a/packages/types/src/form.ts b/packages/types/src/form.ts deleted file mode 100644 index 40057cc6425..00000000000 --- a/packages/types/src/form.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { Subject } from 'rxjs/internal/Subject' -import { Path } from './path' -import { IFieldState, IField } from './field' -import { ISchema } from './schema' -import { IEffects } from './effects' - -export interface IFormPayload { - formState: IFormState -} - -export interface IFieldPayload { - fieldState: IFieldState - formState: IFormState -} - -export interface IFieldError { - name: string - errors: string[] -} - -export interface IFormState { - values: V // 表单数据 - initialValues: V // 初始化数据 - valid: boolean // 是否合法 - invalid: boolean // 是否不合法 - errors: IFieldError[] // 错误提示集合 - pristine: boolean // 是否是原始态 - dirty: boolean // 是否存在变化 -} - -export interface ISubscribers { - [eventName: string]: Subject -} - -export interface IFormOptions { - editable: boolean | ((name: string) => boolean) - effects: IEffects - defaultValue?: V - values?: V - initialValues?: V - schema: ISchema - subscribes: ISubscribers - onFormChange: (payload: IFormPayload) => void - onFieldChange: (payload: IFieldPayload) => void - onValidateFailed: (fieldErrors: IFieldError[]) => void - onFormWillInit?: (form: any) => void - onReset: (payload: IFormPayload) => void - onSubmit: (values: any) => Promise | void - traverse?: (schema: ISchema) => ISchema -} - -// 通过 createActions 创建出来的 actions 接口 -export interface IFormActions { - setFieldState: ( - name: Path | IFormPathMatcher, - callback: (fieldState: IFieldState) => void - ) => Promise - - getFieldState: { - ( - name: Path | IFormPathMatcher, - callback: (fieldState: IFieldState) => void - ): void - (name: Path | IFormPathMatcher): IFieldState - } - - getFormState: { - (): IFormState - (callback: (formState: IFormState) => void): void - } - - setFormState: (callback: (formState: IFormState) => void) => Promise - getSchema: (path: Path) => ISchema - reset: ( - forceClear?: boolean | { forceClear?: boolean; validate?: boolean }, - validate?: boolean - ) => void - submit: () => Promise - validate: () => Promise // error will be IFormState['errors'] - dispatch: (type: string, payload: T) => void -} - -// 通过 createAsyncActions 创建出来的 actions 接口 -export interface IAsyncFormActions { - setFieldState: ( - name: Path | IFormPathMatcher, - callback: (fieldState: IFieldState) => void - ) => Promise - - getFieldState: { - ( - name: Path | IFormPathMatcher, - callback: (fieldState: IFieldState) => void - ): Promise - (name: Path | IFormPathMatcher): Promise - } - - getFormState: { - (): Promise - (callback: (formState: IFormState) => void): Promise - } - - setFormState: (callback: (fieldState: IFormState) => void) => Promise - getSchema: (path: Path) => Promise - reset: ( - forceClear?: boolean | { forceClear?: boolean; validate?: boolean }, - validate?: boolean - ) => Promise - submit: () => Promise - validate: () => Promise //reject err will be IFormState['errors'] - dispatch: (type: string, payload: T) => Promise -} - -export interface IFormPathMatcher { - (payload: IField | Path | { fieldState: IFieldState }): boolean - hasWildcard: boolean - pattern: string -} - -export type TextAlign = 'left' | 'right' -export type Size = 'small' | 'medium' | 'large' -export type Layout = 'horizontal' | 'vertical' | 'inline' -export type TextEl = string | JSX.Element | null -export type LabelAlign = 'left' | 'top' | 'inset' - -type ColSpanType = number | string -export interface ColSize { - span?: ColSpanType - order?: ColSpanType - offset?: ColSpanType - push?: ColSpanType - pull?: ColSpanType -} - -export interface ColProps extends React.HTMLAttributes { - span?: ColSpanType - order?: ColSpanType - offset?: ColSpanType - push?: ColSpanType - pull?: ColSpanType - xs?: ColSpanType | ColSize - sm?: ColSpanType | ColSize - md?: ColSpanType | ColSize - lg?: ColSpanType | ColSize - xl?: ColSpanType | ColSize - xxl?: ColSpanType | ColSize - prefixCls?: string -} -// export type ColProps = { span: number; offset?: number } | number - -export interface IFormItemGridProps { - name?: string - help?: React.ReactNode - extra?: React.ReactNode - description?: string - title?: string - cols?: any -} - -interface IFormSharedProps { - labelCol?: ColProps | number - wrapperCol?: ColProps | number - autoAddColon?: boolean - size?: Size - inline?: boolean - labelAlign?: LabelAlign - labelTextAlign?: TextAlign - className?: string - style?: React.CSSProperties - prefix?: string - maxTipsNum?: number -} - -export interface IFormProps extends IFormSharedProps { - layout?: string - children?: React.ReactNode - component?: string - onValidateFailed?: () => void -} - -export interface IFormItemProps extends IFormSharedProps { - id?: string - required?: boolean - label?: React.ReactNode - extra?: React.ReactNode - validateState?: any - isTableColItem?: boolean - help?: React.ReactNode - noMinHeight?: boolean - children?: React.ReactElement - type?: string - schema?: ISchema -} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts deleted file mode 100644 index 1438c5e65b8..00000000000 --- a/packages/types/src/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './schema' -export * from './types' -export * from './validator' -export * from './effects' -export * from './path' -export * from './field' -export * from './form' -export * from './rule' diff --git a/packages/types/src/path.ts b/packages/types/src/path.ts deleted file mode 100644 index cdb40e73793..00000000000 --- a/packages/types/src/path.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type Path = PathNode[] | PathNode | null -export type PathNode = string | number -export type ArrayPath = PathNode[] diff --git a/packages/types/src/rule.ts b/packages/types/src/rule.ts deleted file mode 100644 index 9a949239501..00000000000 --- a/packages/types/src/rule.ts +++ /dev/null @@ -1,36 +0,0 @@ -export interface IRuleDescription { - required?: boolean - message?: string - pattern?: RegExp | string - validator?: Validator - format?: DefaultPatternRule -} - -export type Validator = ( - value: any, - rule: IRuleDescription, - values: any, - name: string -) => string | Promise - -export type DefaultPatternRule = - | 'url' - | 'email' - | 'ipv6' - | 'ipv4' - | 'number' - | 'integer' - | 'qq' - | 'phone' - | 'idcard' - | 'taodomain' - | 'money' - | 'zh' - | 'date' - | 'zip' - -export type Rule = - | Validator - | Array - | DefaultPatternRule - | IRuleDescription diff --git a/packages/types/src/schema.ts b/packages/types/src/schema.ts deleted file mode 100644 index 2bbed5703d3..00000000000 --- a/packages/types/src/schema.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Rule } from './rule' -import { Dispatcher } from './effects' - -type MayBeArray = V | (V[]) - -export interface ISchema { - type?: string - title?: string | JSX.Element - description?: string | JSX.Element - default?: MayBeArray - required?: boolean - enum?: Array<{ label: string | JSX.Element; value: V } | string | number> - enumNames?: string[] - properties?: { - [key: string]: ISchema - } - items?: ISchema - minItems?: number - maxItems?: number - ['x-props']?: { [name: string]: any } - ['x-index']?: number - ['x-rules']?: Rule - ['x-component']?: string - ['x-effect']?: ( - dispatch: Dispatcher, - option?: object - ) => { [key: string]: any } -} diff --git a/packages/types/src/validator.ts b/packages/types/src/validator.ts deleted file mode 100644 index 76225c1bb33..00000000000 --- a/packages/types/src/validator.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IFieldState } from './field' -export interface IValidateResponse { - name: string - value: any - field: IFieldState - invalid: boolean - valid: boolean - errors: string[] -} - -export type ValidateHandler = (response: IValidateResponse[]) => void diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json deleted file mode 100644 index 704cba57071..00000000000 --- a/packages/types/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./lib" - }, - "include": [ - "./src/**/*.ts", - "./src/**/*.tsx" - ], - "exclude": [ - "./src/__tests__/*" - ] -} diff --git a/packages/utils/src/__tests__/index.spec.js b/packages/utils/src/__tests__/index.spec.js deleted file mode 100644 index c0fa3d45fd3..00000000000 --- a/packages/utils/src/__tests__/index.spec.js +++ /dev/null @@ -1,325 +0,0 @@ -import { getIn, setIn, getPathSegments } from '../accessor' -import { Broadcast } from '../broadcast' -import { isEqual } from '../compare' -import { toArr, every, some, findIndex, find, includes } from '../array' -import { clone } from '../clone' -import { calculateSchemaInitialValues } from '../schema' - -test('test accessor', () => { - const value = { a: { b: { c: 2, d: 333 } } } - expect(getIn(value, 'a.b.c') === 2).toBeTruthy() - setIn(value, 'a.b.c', 1111) - expect(getIn(value, 'a.b.c') === 1111).toBeTruthy() -}) - -test('test accessor with large path', () => { - const value = { array: [{ aa: 123, bb: 321 }] } - expect(isEqual(getIn(value, 'array.0.[aa,bb]'), [123, 321])).toBeTruthy() -}) - -test('test setIn auto create array', () => { - const value = {} - setIn(value, 'array.0.bb.2', 'hello world') - expect( - isEqual(value, { - array: [ - { - bb: [undefined, undefined, 'hello world'] - } - ] - }) - ).toBeTruthy() -}) - -test('getSchema return undefined', () => { - const value = {} - setIn(value, 'array.0.bb.2', 'hello world', () => {}) - - expect( - isEqual(value, { - array: [ - { - bb: [undefined, undefined, 'hello world'] - } - ] - }) - ).toBeTruthy() -}) - -test('test setIn dose not affect other items', () => { - const value = { - aa: [ - { - dd: [ - { - ee: '是' - } - ], - cc: '1111' - } - ] - } - - setIn(value, 'aa.1.dd.0.ee', '否') - expect( - isEqual(value.aa[0], { - dd: [ - { - ee: '是' - } - ], - cc: '1111' - }) - ).toBeTruthy() -}) - -test('destruct getIn', () => { - // getIn 通过解构表达式从扁平数据转为复合嵌套数据 - const value = { a: { b: { c: 2, d: 333 } } } - expect( - isEqual(getIn({ a: { b: { kk: 2, mm: 333 } } }, 'a.b.{c:kk,d:mm}'), { - c: 2, - d: 333 - }) - ).toBeTruthy() - expect( - isEqual( - getIn( - { kk: 2, mm: 333 }, - `{ - a : { - b : { - c : kk, - d : mm - } - } - }` - ), - value - ) - ).toBeTruthy() -}) - -test('destruct setIn', () => { - const value = { a: { b: { c: 2, d: 333 } } } - // setIn 从复杂嵌套结构中解构数据出来对其做赋值处理 - expect( - isEqual( - setIn( - {}, - `{ - a : { - b : { - c, - d - } - } - }`, - value - ), - { c: 2, d: 333 } - ) - ).toBeTruthy() - expect( - isEqual( - setIn( - {}, - ` - [aa,bb] - `, - [123, 444] - ), - { aa: 123, bb: 444 } - ) - ).toBeTruthy() - expect( - isEqual(setIn({}, 'aa.bb.ddd.[aa,bb]', [123, 444]), { - aa: { bb: { ddd: { aa: 123, bb: 444 } } } - }) - ).toBeTruthy() - - expect( - isEqual(setIn({}, 'aa.bb.ddd.[{cc:aa,bb}]', [{ cc: 123, bb: 444 }]), { - aa: { bb: { ddd: { aa: 123, bb: 444 } } } - }) - ).toBeTruthy() -}) - -test('broadcast', () => { - const centerHub = new Broadcast() - const unsubscribe = centerHub.subscribe(payload => { - expect(payload === 111).toBeTruthy() - }) - return new Promise(resolve => { - setTimeout(() => { - centerHub.notify(111) - unsubscribe() - centerHub.notify(222) - resolve() - }, 1000) - }) -}) - -test('toArr', () => { - expect(isEqual(toArr([123]), [123])).toBeTruthy() - expect(isEqual(toArr(123), [123])).toBeTruthy() - expect(isEqual(toArr(null), [])).toBeTruthy() -}) - -test('clone form data', () => { - var dd = new Map() - dd.set('aaa', { bb: 123 }) - var a = { - aa: 123123, - bb: [{ bb: 111 }, { bb: 222 }], - cc: () => { - // eslint-disable-next-line no-console - console.log('123') - }, - dd - } - var 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).toBeFalsy() - expect(a.dd.get('aaa') === cloned.dd.get('aaa')).toBeTruthy() - expect(a.cc === cloned.cc).toBeTruthy() -}) - -test('filter equal', () => { - var a = { - aa: { - bb: 123 - } - } - var b = { - aa: { - bb: 123 - } - } - - expect(isEqual(a, b)).toBeTruthy() - expect(isEqual(a, b, (_, key) => key !== 'aa')).toBeTruthy() -}) - -test('filter clone', () => { - var a = { - aa: { - bb: 123 - }, - cc: { - dd: [1, 3, 4, 5] - } - } - - var b = clone(a, (_, key) => key !== 'aa') - - expect(a.aa === b.aa).toBeTruthy() - expect(a.cc === b.cc).toBeFalsy() - expect(isEqual(a.cc, b.cc)).toBeTruthy() -}) - -test('setIn', () => { - var values = {} - setIn(values, 'a', '123232323') - expect(isEqual(values.a, '123232323')).toBeTruthy() -}) - -test('calculateSchemaInitialValues', () => { - var values1 = JSON.parse( - '{"type":"object","properties":{"[startDate,endDate]":{"type":"daterange","default":["2019-01-24","2019-01-30"],"z-index":0,"id":"[startDate,endDate]","x-index":0}}}' - ) - var values2 = JSON.parse( - '{"type":"object","properties":{"[startDate,endDate]":{"type":"daterange","default":["2019-01-24",""],"z-index":0,"id":"[startDate,endDate]","x-index":0}}}' - ) - var values3 = JSON.parse( - '{"type":"object","properties":{"[startDate,endDate]":{"type":"daterange","default":["","2019-01-30"],"z-index":0,"id":"[startDate,endDate]","x-index":0}}}' - ) - var values4 = JSON.parse( - '{"type":"object","properties":{"[startDate,endDate]":{"type":"daterange","z-index":0,"id":"[startDate,endDate]","x-index":0}}}' - ) - var result1 = calculateSchemaInitialValues(values1) - var result2 = calculateSchemaInitialValues(values2) - var result3 = calculateSchemaInitialValues(values3) - var result4 = calculateSchemaInitialValues(values4) - expect( - isEqual( - JSON.stringify(result1), - JSON.stringify({ startDate: '2019-01-24', endDate: '2019-01-30' }) - ) - ).toBeTruthy() - expect( - isEqual( - JSON.stringify(result2), - JSON.stringify({ startDate: '2019-01-24', endDate: '' }) - ) - ).toBeTruthy() - expect( - isEqual( - JSON.stringify(result3), - JSON.stringify({ startDate: '', endDate: '2019-01-30' }) - ) - ).toBeTruthy() - expect( - isEqual( - JSON.stringify(result4), - JSON.stringify({ startDate: undefined, endDate: undefined }) - ) - ).toBeTruthy() -}) - -test('getPathSegments', () => { - expect(isEqual(getPathSegments(0), [0])).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() -}) diff --git a/packages/utils/src/accessor.ts b/packages/utils/src/accessor.ts deleted file mode 100644 index bcf862d9cee..00000000000 --- a/packages/utils/src/accessor.ts +++ /dev/null @@ -1,594 +0,0 @@ -import { - Path, - PathNode, - ArrayPath, - isStr, - isNum, - isPlainObj, - isArr, - isObj -} from '@uform/types' -import { map, each, every } from './array' -import { LRUMap } from './lru' - -interface ITokenizerHandlers { - name(str: string): void - destructObjectStart(): void - destructObjectEnd(): void - destructArrayStart(): void - destructArrayEnd(): void - destructKey(str: string, isColon?: boolean): void -} - -type Destruct = - | { - [key: string]: string - } - | Path - -type Getter = (obj: any, path: Path, value?: any) => any - -type Setter = ( - obj: any, - path: Path, - value?: any, - getSchema?: (path: string[] | number[]) => any -) => any - -type HasIn = (obj: any, path: Path) => boolean - -function whitespace(c: string) { - return c === ' ' || c === '\n' || c === '\t' || c === '\f' || c === '\r' -} - -function toString(val: Path | null) { - if (!val) { - return '' - } - if (isArr(val)) { - return (val as string[]).join('.') - } - return isStr(val) ? val : '' -} - -const PathCache = new LRUMap(1000) - -export function getPathSegments(path: Path): ArrayPath { - if (isArr(path)) { - return path as string[] - } - if (isStr(path) && path) { - const cached = PathCache.get(path) - if (cached) { - return cached - } - const pathArr = (path as string).split('.') - const parts = [] - - for (let i = 0; i < pathArr.length; i++) { - let p = pathArr[i] - - while (p[p.length - 1] === '\\' && pathArr[i + 1] !== undefined) { - p = p.slice(0, -1) + '.' - p += pathArr[++i] - } - - parts.push(p) - } - PathCache.set(path, parts) - return parts - } - if (isNum(path)) { - return [path as number] - } - return [] -} - -class DestructTokenizer { - private text: string - - private index: number - - private handlers: ITokenizerHandlers - - private state: (char: string, prev?: string) => void - - private declareNameStart: number - - private declareNameEnd: number - - private nbraceCount: number - - private nbracketCount: number - - private EOF: boolean - - private destructKeyStart: number - - private destructKey: string - - constructor(text: string, handlers: ITokenizerHandlers) { - this.text = text - this.index = 0 - this.handlers = handlers - this.state = this.processNameStart - this.declareNameStart = 0 - this.declareNameEnd = 0 - this.nbraceCount = 0 - this.nbracketCount = 0 - } - - public parse() { - let char = '' - let prev = '' - const l = this.text.length - for (; this.index < l; this.index++) { - char = this.text.charAt(this.index) - this.EOF = l - 1 === this.index - this.state(char, prev) - prev = char - } - } - - private processNameStart(char: string) { - if (char === '{' || char === '[') { - this.state = this.processDestructStart - this.index-- - } else if (!whitespace(char)) { - this.declareNameStart = this.index - this.state = this.processName - } - } - - private processName(char: string, prev: string) { - if (whitespace(char)) { - this.declareNameEnd = this.index - this.handlers.name(this.getName()) - } else if (this.EOF) { - this.declareNameEnd = this.index + 1 - this.handlers.name(this.getName()) - } - } - - private processDestructStart(char) { - if (char === '{') { - this.nbraceCount++ - this.handlers.destructObjectStart() - } else if (char === '[') { - this.nbracketCount++ - this.handlers.destructArrayStart() - } else if (!whitespace(char)) { - this.state = this.processDestructKey - this.destructKeyStart = this.index - this.index-- - } - } - - private processDestructKey(char: string, prev: string) { - if (char === '}') { - this.nbraceCount-- - - if (this.nbraceCount || this.nbracketCount) { - this.state = this.processDestructStart - } - if (!whitespace(prev)) { - this.destructKey = this.text.substring( - this.destructKeyStart, - this.index - ) - } - - this.handlers.destructKey(this.destructKey) - this.handlers.destructObjectEnd() - if (!this.nbraceCount && !this.nbracketCount) { - this.index = this.text.length - } - } else if (char === ']') { - this.nbracketCount-- - - if (this.nbraceCount || this.nbracketCount) { - this.state = this.processDestructStart - } - if (!whitespace(prev)) { - this.destructKey = this.text.substring( - this.destructKeyStart, - this.index - ) - } - this.handlers.destructKey(this.destructKey) - this.handlers.destructArrayEnd() - if (!this.nbraceCount && !this.nbracketCount) { - this.index = this.text.length - } - } else if (whitespace(char) || char === ':' || char === ',') { - if (!whitespace(prev)) { - this.destructKey = this.text.substring( - this.destructKeyStart, - this.index - ) - } - if (!whitespace(char)) { - this.state = this.processDestructStart - this.handlers.destructKey(this.destructKey, char === ':') - } - } - } - - private getName() { - return this.text.substring(this.declareNameStart, this.declareNameEnd) - } -} - -const parseDestruct = (str: PathNode) => { - if (!isStr(str)) { - return str - } - - let destruct: Destruct - const stack = [] - let token = '' - let realKey = '' - let lastDestruct: Destruct - let root: Destruct - - new DestructTokenizer(str as string, { - name(key: string) { - root = key - }, - destructKey(key, readyReplace) { - if (!key) { - return - } - token = key - if (readyReplace) { - realKey = key - lastDestruct = destruct - return - } - if (isArr(destruct)) { - ;(destruct as string[]).push(key) - } else if (isPlainObj(destruct)) { - destruct[realKey && lastDestruct === destruct ? realKey : key] = key - } - realKey = '' - lastDestruct = destruct - }, - destructArrayStart() { - if (!destruct) { - root = [] - destruct = root - } else { - destruct = [] - } - const tail = stack[stack.length - 1] - if (isPlainObj(tail)) { - tail[token] = destruct - } else if (isArr(tail)) { - tail.push(destruct) - } - stack.push(destruct) - }, - destructObjectStart() { - if (!destruct) { - root = {} - destruct = root - } else { - destruct = {} - } - const tail = stack[stack.length - 1] - if (isPlainObj(tail)) { - tail[token] = destruct - } else if (isArr(tail)) { - tail.push(destruct) - } - stack.push(destruct) - }, - destructArrayEnd() { - stack.pop() - destruct = stack[stack.length - 1] - }, - destructObjectEnd() { - stack.pop() - destruct = stack[stack.length - 1] - } - }).parse() - return root -} - -const traverse = (obj: any, callback: any) => { - const internalTraverse = (internalObj: any, path: string[]) => { - if (isStr(internalObj)) { - return callback(internalObj, internalObj) - } - each(internalObj, (item: any, key: string) => { - const newPath = path.concat(key) - if (isArr(item) || isPlainObj(item)) { - internalTraverse(item, newPath) - } else { - callback(newPath, item) - } - }) - } - - return internalTraverse(obj, []) -} - -const mapReduce = (obj: any, callback: any) => { - const internalTraverse = (internalObj: any, path: string[]) => { - return map(internalObj, (item: any, key: string) => { - const newPath = path.concat(key) - if (isArr(item) || isPlainObj(item)) { - return internalTraverse(item, newPath) - } else { - return callback( - newPath, - newPath.slice(0, newPath.length - 1).concat(item) - ) - } - }) - } - - return internalTraverse(obj, []) -} - -const parseDesturctPath = (path: Path): any => { - const newPath = getPathSegments(path) - const lastKey = newPath[newPath.length - 1] - const startPath = newPath.slice(0, newPath.length - 1) - const destruct = parseDestruct(lastKey) - return { - path: newPath, - lastKey, - startPath, - destruct - } -} - -const parsePaths = (path: Path): any => { - const result = [] - const parsed = parseDesturctPath(path) - if (isStr(parsed.destruct)) { - return path - } else if (parsed.destruct) { - traverse(parsed.destruct, (internalPath, key) => { - result.push({ - path: parsed.startPath.concat(internalPath), - startPath: parsed.startPath, - endPath: internalPath, - key - }) - }) - return result - } else { - return path - } -} - -const resolveGetIn = (get: Getter) => { - const cache = new Map() - return (obj: any, path: Path, value?: any): any => { - let ast = null - - if (!cache.get(path)) { - ast = parseDesturctPath(path) - cache.set(path, ast) - } else { - ast = cache.get(path) - } - if (!isArr(ast.destruct) && !isPlainObj(ast.destruct)) { - return get(obj, path, value) - } - return mapReduce(ast.destruct, (mapPath, key) => { - return get(obj, ast.startPath.concat(key[key.length - 1])) - }) - } -} - -const resolveUpdateIn = (update: Setter, internalGetIn: Getter) => { - const cache = new Map() - return ( - obj: any, - path: Path, - value?: any, - getSchema?: (path: string[] | number[]) => any - ) => { - let paths: any = [] - if (!cache.get(path)) { - paths = parsePaths(path) - cache.set(path, paths) - } else { - paths = cache.get(path) - } - if (!isArr(paths)) { - return update(obj, path, value, getSchema) - } - if (paths && paths.length) { - each(paths, ({ mapPath, key, startPath, endPath }) => { - update( - obj, - startPath.concat(key), - internalGetIn(value, endPath), - getSchema - ) - }) - } - return obj - } -} - -const resolveExistIn = (has: HasIn) => { - const cache = new Map() - return (obj: any, path: Path) => { - let paths: any = [] - if (!cache.get(path)) { - paths = parsePaths(path) - cache.set(path, paths) - } else { - paths = cache.get(path) - } - if (!isArr(paths)) { - return has(obj, path) - } - if (paths && paths.length) { - return every(paths, ({ startPath, key }) => { - return has(obj, startPath.concat(key)) - }) - } - - return false - } -} - -function _getIn(obj: any, path: Path, value: any) { - if (!isObj(obj) || !path) { - return obj - } - - path = toString(path) - - if (path in obj) { - return obj[path as string] - } - - const pathArr = getPathSegments(path) - - for (let i = 0; i < pathArr.length; i++) { - if (!Object.prototype.propertyIsEnumerable.call(obj, pathArr[i])) { - return value - } - - obj = obj[pathArr[i]] - - if (obj === undefined || obj === null) { - // `obj` is either `undefined` or `null` so we want to stop the loop, and - // if this is not the last bit of the path, and - // if it did't return `undefined` - // it would return `null` if `obj` is `null` - // but we want `get({foo: null}, 'foo.bar')` to equal `undefined`, or the supplied value, not `null` - if (i !== pathArr.length - 1) { - return value - } - - break - } - } - - return obj -} - -function _setIn( - obj: any, - path: Path, - value: any, - getSchema?: (path: string[] | number[]) => any -) { - if (!isObj(obj) || !path) { - return - } - - path = toString(path) - - if (path in obj) { - obj[path as string] = value - return - } - - const pathArr = getPathSegments(path) - - for (let i = 0; i < pathArr.length; i++) { - const p = pathArr[i] - if (!isObj(obj[p])) { - if (obj[p] === undefined && value === undefined) { - return - } - if (/^\d+$/.test(pathArr[i + 1 + ''])) { - if (getSchema) { - const schema = getSchema(pathArr.slice(0, i) as string[]) - - if (!schema || schema.type === 'array') { - obj[p] = [] - } else { - obj[p] = {} - } - } else { - obj[p] = [] - } - } else { - obj[p] = {} - } - } - - if (i === pathArr.length - 1) { - obj[p] = value - } - - obj = obj[p] - } -} - -function _deleteIn(obj: any, path: Path) { - if (!isObj(obj) || !path) { - return - } - - path = toString(path) - - if (path in obj) { - delete obj[path as string] - return - } - - const pathArr = getPathSegments(path) - - for (let i = 0; i < pathArr.length; i++) { - const p = pathArr[i] - - if (i === pathArr.length - 1) { - if (isArr(obj)) { - obj.splice(p as number, 1) - } else { - delete obj[p] - } - return - } - - obj = obj[p] - - if (!isObj(obj)) { - return - } - } -} - -function _existIn(obj: any, path: Path) { - if (!isObj(obj) || !path) { - return false - } - - path = toString(path) - - if (path in obj) { - return true - } - - const pathArr = getPathSegments(path) - - for (let i = 0; i < pathArr.length; i++) { - if (isObj(obj)) { - if (!(pathArr[i] in obj)) { - return false - } - - obj = obj[pathArr[i]] - } else { - return false - } - } - - return true -} -export const getIn = resolveGetIn(_getIn) -export const setIn = resolveUpdateIn(_setIn, getIn) -export const deleteIn = resolveUpdateIn(_deleteIn, getIn) -export const existIn = resolveExistIn(_existIn) -export { parseDesturctPath, parseDestruct, parsePaths } diff --git a/packages/utils/src/defer.ts b/packages/utils/src/defer.ts deleted file mode 100644 index e47fbfd8a44..00000000000 --- a/packages/utils/src/defer.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const defer = () => { - let internalResolve: (payload: P) => void - let internalReject: (error: E) => void - const promise = new Promise((resolve, reject) => { - internalResolve = resolve - internalReject = reject - }) - return { - promise, - resolve: internalResolve, - reject: internalReject - } -} diff --git a/packages/utils/src/globalThis.ts b/packages/utils/src/globalThis.ts deleted file mode 100644 index 99af5ea3b72..00000000000 --- a/packages/utils/src/globalThis.ts +++ /dev/null @@ -1,13 +0,0 @@ -function globalThis() { - if (typeof self !== 'undefined') { - return self - } - if (typeof window !== 'undefined') { - return window - } - if (typeof global !== 'undefined') { - return global - } - return Function('return this')() -} -export const globalThisPolyfill = globalThis() diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts deleted file mode 100644 index 43661128d1d..00000000000 --- a/packages/utils/src/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export * from './accessor' -export * from './array' -export * from './compare' -export * from './broadcast' -export * from '@uform/types' -export * from './clone' -export * from './schema' -export * from './lru' -export * from './isEmpty' -export * from './case' -export * from './defer' -export * from './stringLength' -export * from './globalThis' diff --git a/packages/utils/src/lru.ts b/packages/utils/src/lru.ts deleted file mode 100644 index ddeefacf677..00000000000 --- a/packages/utils/src/lru.ts +++ /dev/null @@ -1,310 +0,0 @@ -/** - * A doubly linked list-based Least Recently Used (LRU) cache. Will keep most - * recently used items while discarding least recently used items when its limit - * is reached. - * - * Licensed under MIT. Copyright (c) 2010 Rasmus Andersson - * See README.md for details. - * - * Illustration of the design: - * - * entry entry entry entry - * ______ ______ ______ ______ - * | head |.newer => | |.newer => | |.newer => | tail | - * | A | | B | | C | | D | - * |______| <= older.|______| <= older.|______| <= older.|______| - * - * removed <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- added - */ -/* eslint-disable */ - -const NEWER = Symbol('newer') -const OLDER = Symbol('older') - -export function LRUMap(limit: number, entries?: any) { - if (typeof limit !== 'number') { - // called as (entries) - entries = limit - limit = 0 - } - - this.size = 0 - this.limit = limit - this.oldest = this.newest = undefined - this._keymap = new Map() - - if (entries) { - this.assign(entries) - if (limit < 1) { - this.limit = this.size - } - } -} - -function Entry(key: any, value: any) { - this.key = key - this.value = value - this[NEWER] = undefined - this[OLDER] = undefined -} - -LRUMap.prototype._markEntryAsUsed = function(entry: any) { - if (entry === this.newest) { - // Already the most recenlty used entry, so no need to update the list - return - } - // HEAD--------------TAIL - // <.older .newer> - // <--- add direction -- - // A B C E - if (entry[NEWER]) { - if (entry === this.oldest) { - this.oldest = entry[NEWER] - } - entry[NEWER][OLDER] = entry[OLDER] // C <-- E. - } - if (entry[OLDER]) { - entry[OLDER][NEWER] = entry[NEWER] // C. --> E - } - entry[NEWER] = undefined // D --x - entry[OLDER] = this.newest // D. --> E - if (this.newest) { - this.newest[NEWER] = entry // E. <-- D - } - this.newest = entry -} - -LRUMap.prototype.assign = function(entries: any) { - let entry: any - let limit = this.limit || Number.MAX_VALUE - this._keymap.clear() - const it = entries[Symbol.iterator]() - for (let itv = it.next(); !itv.done; itv = it.next()) { - const e = new Entry(itv.value[0], itv.value[1]) - this._keymap.set(e.key, e) - if (!entry) { - this.oldest = e - } else { - entry[NEWER] = e - e[OLDER] = entry - } - entry = e - if (limit-- === 0) { - throw new Error('overflow') - } - } - this.newest = entry - this.size = this._keymap.size -} - -LRUMap.prototype.get = function(key: any) { - // First, find our cache entry - const entry = this._keymap.get(key) - if (!entry) { return } // Not cached. Sorry. - // As was found in the cache, register it as being requested recently - this._markEntryAsUsed(entry) - return entry.value -} - -LRUMap.prototype.set = function(key: any, value: any) { - let entry = this._keymap.get(key) - - if (entry) { - // update existing - entry.value = value - this._markEntryAsUsed(entry) - return this - } - - // new entry - this._keymap.set(key, (entry = new Entry(key, value))) - - if (this.newest) { - // link previous tail to the new tail (entry) - this.newest[NEWER] = entry - entry[OLDER] = this.newest - } else { - // we're first in -- yay - this.oldest = entry - } - - // add new entry to the end of the linked list -- it's now the freshest entry. - this.newest = entry - ++this.size - if (this.size > this.limit) { - // we hit the limit -- remove the head - this.shift() - } - - return this -} - -LRUMap.prototype.shift = function() { - // todo: handle special case when limit == 1 - const entry = this.oldest - if (entry) { - if (this.oldest[NEWER]) { - // advance the list - this.oldest = this.oldest[NEWER] - this.oldest[OLDER] = undefined - } else { - // the cache is exhausted - this.oldest = undefined - this.newest = undefined - } - // Remove last strong reference to and remove links from the purged - // entry being returned: - entry[NEWER] = entry[OLDER] = undefined - this._keymap.delete(entry.key) - --this.size - return [entry.key, entry.value] - } -} - -// ---------------------------------------------------------------------------- -// Following code is optional and can be removed without breaking the core -// functionality. - -LRUMap.prototype.find = function(key: any) { - const e = this._keymap.get(key) - return e ? e.value : undefined -} - -LRUMap.prototype.has = function(key: any) { - return this._keymap.has(key) -} - -LRUMap.prototype.delete = function(key: any) { - const entry = this._keymap.get(key) - if (!entry) { return } - this._keymap.delete(entry.key) - if (entry[NEWER] && entry[OLDER]) { - // relink the older entry with the newer entry - entry[OLDER][NEWER] = entry[NEWER] - entry[NEWER][OLDER] = entry[OLDER] - } else if (entry[NEWER]) { - // remove the link to us - entry[NEWER][OLDER] = undefined - // link the newer entry to head - this.oldest = entry[NEWER] - } else if (entry[OLDER]) { - // remove the link to us - entry[OLDER][NEWER] = undefined - // link the newer entry to head - this.newest = entry[OLDER] - } else { - // if(entry[OLDER] === undefined && entry.newer === undefined) { - this.oldest = this.newest = undefined - } - - this.size-- - return entry.value -} - -LRUMap.prototype.clear = function() { - // Not clearing links should be safe, as we don't expose live links to user - this.oldest = this.newest = undefined - this.size = 0 - this._keymap.clear() -} - -function EntryIterator(oldestEntry: any) { - this.entry = oldestEntry -} -EntryIterator.prototype[Symbol.iterator] = function() { - return this -} -EntryIterator.prototype.next = function() { - const ent = this.entry - if (ent) { - this.entry = ent[NEWER] - return { done: false, value: [ent.key, ent.value] } - } else { - return { done: true, value: undefined } - } -} - -function KeyIterator(oldestEntry) { - this.entry = oldestEntry -} -KeyIterator.prototype[Symbol.iterator] = function() { - return this -} -KeyIterator.prototype.next = function() { - const ent = this.entry - if (ent) { - this.entry = ent[NEWER] - return { done: false, value: ent.key } - } else { - return { done: true, value: undefined } - } -} - -function ValueIterator(oldestEntry) { - this.entry = oldestEntry -} -ValueIterator.prototype[Symbol.iterator] = function() { - return this -} -ValueIterator.prototype.next = function() { - const ent = this.entry - if (ent) { - this.entry = ent[NEWER] - return { done: false, value: ent.value } - } else { - return { done: true, value: undefined } - } -} - -LRUMap.prototype.keys = function() { - return new KeyIterator(this.oldest) -} - -LRUMap.prototype.values = function() { - return new ValueIterator(this.oldest) -} - -LRUMap.prototype.entries = function() { - return this -} - -LRUMap.prototype[Symbol.iterator] = function() { - return new EntryIterator(this.oldest) -} - -LRUMap.prototype.forEach = function(fun: (value: any, key: any, ctx: object) => void, thisObj: any) { - if (typeof thisObj !== 'object') { - thisObj = this - } - let entry = this.oldest - while (entry) { - fun.call(thisObj, entry.value, entry.key, this) - entry = entry[NEWER] - } -} - -/** Returns a JSON (array) representation */ -LRUMap.prototype.toJSON = function() { - const s = new Array(this.size) - let i = 0 - let entry = this.oldest - while (entry) { - s[i++] = { key: entry.key, value: entry.value } - entry = entry[NEWER] - } - return s -} - -/** Returns a String representation */ -LRUMap.prototype.toString = function() { - let s = '' - let entry = this.oldest - while (entry) { - s += String(entry.key) + ':' + entry.value - entry = entry[NEWER] - if (entry) { - s += ' < ' - } - } - return s -} diff --git a/packages/utils/src/schema.ts b/packages/utils/src/schema.ts deleted file mode 100644 index cf59e611816..00000000000 --- a/packages/utils/src/schema.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { each, toArr } from './array' -import { getIn, setIn } from './accessor' -import { isFn, Path, ISchema, ArrayPath } from '@uform/types' -import { isEmpty } from './isEmpty' -const numberRE = /^\d+$/ -const VIRTUAL_BOXES = {} - -interface IRuleDescription { - required?: boolean - message?: string - pattern?: RegExp | string - validator?: RuleHandler -} - -interface IPathInfo { - name: string - path: string[] - schemaPath: string[] -} - -type RuleHandler = ( - value: any, - rule: IRuleDescription, - values: object, - name: string -) => string | null - -export const getSchemaNodeFromPath = (schema: ISchema, path: Path) => { - let res = schema - let suc = 0 - path = toArr(path) - for (let i = 0; i < path.length; i++) { - const key = path[i] - if (res && !isEmpty(res.properties)) { - res = res.properties[key] - suc++ - } else if (res && !isEmpty(res.items) && numberRE.test(key as string)) { - res = res.items - suc++ - } - } - return suc === path.length ? res : undefined -} - -export const schemaIs = (schema: ISchema, type: string) => { - return schema && schema.type === type -} - -export const isVirtualBox = (name: string) => { - return !!VIRTUAL_BOXES[name] -} - -export const registerVirtualboxFlag = (name: string) => { - VIRTUAL_BOXES[name] = true -} - -const isVirtualBoxSchema = (schema: ISchema) => { - return isVirtualBox(schema.type) || isVirtualBox(schema['x-component']) -} - -const schemaTraverse = ( - schema: ISchema, - callback: any, - path: ArrayPath = [], - schemaPath = [] -) => { - if (schema) { - if (isVirtualBoxSchema(schema)) { - path = path.slice(0, path.length - 1) - } - callback(schema, { path, schemaPath }) - if (schemaIs(schema, 'object') || schema.properties) { - each(schema.properties, (subSchema, key) => { - schemaTraverse( - subSchema, - callback, - path.concat(key), - schemaPath.concat(key) - ) - }) - } else if (schemaIs(schema, 'array') || schema.items) { - if (schema.items) { - callback( - schema.items, - key => { - schemaTraverse( - schema.items, - callback, - path.concat(key), - schemaPath.concat(key) - ) - }, - path - ) - } - } - } -} - -export const calculateSchemaInitialValues = ( - schema: ISchema, - initialValues: any, - callback?: (pathInfo: IPathInfo, schema: ISchema, value: any) => void -) => { - initialValues = initialValues || schema.default || {} - schemaTraverse(schema, (subSchema, $path, parentPath) => { - const defaultValue = subSchema.default - if (isFn($path) && parentPath) { - each(toArr(getIn(initialValues, parentPath)), (value, index) => { - $path(index) - }) - } else if ($path) { - const isVirtualBoxInstance = isVirtualBoxSchema(subSchema) - const name = isVirtualBoxInstance - ? $path.schemaPath.join('.') - : $path.path.join('.') - const path = isVirtualBoxInstance ? $path.schemaPath : $path.path - const schemaPath = $path.schemaPath - const initialValue = getIn(initialValues, name) - const value = !isEmpty(initialValue) ? initialValue : defaultValue - if (!isEmpty(value)) { - setIn(initialValues, name, value) - } - if (callback && isFn(callback)) { - const newPath = { - name, - path, - schemaPath - } - callback(newPath, subSchema, value) - } - } - }) - return initialValues -} diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json deleted file mode 100644 index 704cba57071..00000000000 --- a/packages/utils/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./lib" - }, - "include": [ - "./src/**/*.ts", - "./src/**/*.tsx" - ], - "exclude": [ - "./src/__tests__/*" - ] -} diff --git a/packages/validator/package.json b/packages/validator/package.json index 59a84fbe377..d3298e901bb 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -1,6 +1,6 @@ { "name": "@uform/validator", - "version": "0.4.3", + "version": "0.4.0", "license": "MIT", "main": "lib", "repository": { @@ -25,8 +25,8 @@ "@babel/runtime": "^7.4.4" }, "dependencies": { - "@uform/types": "^0.4.3", - "@uform/utils": "^0.4.3" + "@uform/shared": "^0.4.0", + "@uform/types": "^0.4.0" }, "publishConfig": { "access": "public" diff --git a/packages/validator/src/__tests__/index.spec.js b/packages/validator/src/__tests__/index.spec.js deleted file mode 100644 index 37f9c1ef391..00000000000 --- a/packages/validator/src/__tests__/index.spec.js +++ /dev/null @@ -1,46 +0,0 @@ -import { runValidation } from '../index' - -const testResponse = (response, tester) => { - response.forEach(item => { - expect(tester[item.name] === item.valid).toBeTruthy() - }) -} - -test('sample validation', () => { - const values = { - a: 123.333, - b: '123123', - c: '123aa', - d: [null, null, null] - } - - const fieldMap = { - a: { - rules: 'integer' - }, - b: { - rules: 'url' - }, - c: { - rules(value) { - return new Promise(resolve => { - setTimeout(() => { - resolve() - }, 2000) - }) - } - }, - d: { - rules: { required: true } - } - } - - return runValidation(values, fieldMap).then(response => { - testResponse(response, { - a: false, - b: false, - c: true, - d: false - }) - }) -}) diff --git a/packages/validator/src/__tests__/index.spec.ts b/packages/validator/src/__tests__/index.spec.ts new file mode 100644 index 00000000000..cf541d6dc44 --- /dev/null +++ b/packages/validator/src/__tests__/index.spec.ts @@ -0,0 +1,185 @@ +import { FormValidator } from '../index' + +const batchTestRules = async (rules: any[], options?: any) => { + const validator = new FormValidator(options) + rules.forEach(({ value, rules, errors = [], warnings = [] }, index) => { + validator.register(`$${index}`, validate => + validate(value, rules).then(t => { + expect(t.errors).toEqual(errors) + expect(t.warnings).toEqual(warnings) + }) + ) + }) + await validator.validate() +} + +test('register', async () => { + const validator = new FormValidator() + let errors1: string[] + let errors2: string[] + validator.register('a.b.c.e', validate => { + validate('123', { required: true }).then(({ errors }) => { + errors1 = errors + }) + }) + validator.register('a.b.c', validate => { + validate('', { required: true }).then(({ errors }) => { + errors2 = errors + }) + }) + const validateResponse = await validator.validate() + expect(errors1).toEqual([]) + expect(errors2).toEqual(['This field is required']) + expect(validateResponse).toEqual({ + errors: [{ path: 'a.b.c', messages: ['This field is required'] }], + warnings: [] + }) +}) + +test('required', async () => { + await batchTestRules([ + { + value: '', + rules: { + required: true + }, + errors: ['This field is required'] + }, + { + value: '', + rules: { + required: true, + message: '该字段不能为空' + }, + errors: ['该字段不能为空'] + }, + { + value: [], + rules: { + required: true, + message: '该字段不能为空' + }, + errors: ['该字段不能为空'] + }, + { + value: false, + rules: { + required: true, + message: '该字段不能为空' + }, + errors: [] + }, + { + value: [], + rules: { + validator: (value: any) => { + return value.length == 0 ? '数组不能为空' : '' + } + }, + errors: ['数组不能为空'] + }, + { + value: [], + rules: { + validator: (value: any) => { + return new Promise(resolve => { + setTimeout(() => { + resolve(value.length == 0 ? 'async validate failed' : '') + }, 1000) + }) + } + }, + errors: ['async validate failed'] + } + ]) +}) + +test('pattern', async () => { + await batchTestRules([ + { + value: '123', + rules: { + pattern: /^[A-Z]+$/, + message: 'must be upper case letters' + }, + errors: ['must be upper case letters'] + }, + { + value: 'HUYUY', + rules: { + pattern: /^[A-Z]+$/, + message: 'must be upper case letters' + }, + errors: [] + } + ]) +}) + +test('max', async () => { + await batchTestRules([ + { + value: '123', + rules: { + max: 2 + }, + errors: ['The length of 123 must be at most 2'] + }, + { + value: '123', + rules: { + max: 3, + message: 'The length of 123 must be at most 3' + }, + errors: [] + } + ]) +}) + +test('whitespace', async () => { + await batchTestRules([ + { + value: ' ', + rules: { + whitespace: true + }, + errors: ['This field cannot be empty'] + } + ]) +}) + +test('validateFirst', async () => { + const secondValidator = jest.fn() + await batchTestRules( + [ + { + value: ' ', + rules: [ + { + whitespace: true + }, + secondValidator + ], + errors: ['This field cannot be empty'] + } + ], + { + validateFirst: true + } + ) + expect(secondValidator).toBeCalledTimes(0) +}) + +//内置正则库测试 +test('formats', async () => { + //todo +}) + +//模板引擎测试 +test('template', async () => { + //todo +}) + +//自定义规则测试 +test('custom rules', async () => { + //todo +}) diff --git a/packages/validator/src/validators/regexp.ts b/packages/validator/src/formats.ts similarity index 100% rename from packages/validator/src/validators/regexp.ts rename to packages/validator/src/formats.ts diff --git a/packages/validator/src/index.ts b/packages/validator/src/index.ts index 3cfbd1f7569..9c92a315a6f 100644 --- a/packages/validator/src/index.ts +++ b/packages/validator/src/index.ts @@ -1,139 +1,3 @@ -import { - each, - reduce, - isFn, - toArr, - getIn, - isArr, - isEqual, - clone, - format, - isEmpty -} from './utils' -import { validate } from './validators' -import { ValidateHandler, IValidateResponse, IFieldMap } from '@uform/types' export * from './message' - -const flatArr = (arr: any[]) => { - return reduce( - arr, - (buf: any, item: any) => { - return isArr(item) - ? buf.concat(flatArr(item)) - : item - ? buf.concat(item) - : buf - }, - [] - ) -} - -export { format } - -export const runValidation = async ( - values: object, - fieldMap: IFieldMap, - forceUpdate?: boolean | ValidateHandler, - callback?: ValidateHandler -): Promise => { - const queue = [] - if (isFn(forceUpdate)) { - callback = forceUpdate as ValidateHandler - forceUpdate = false - } - each(fieldMap, (field, name) => { - const value = getIn(values, name) - if ( - field.visible === false || - field.display === false || - field.editable === false - ) { - return - } - if (!forceUpdate) { - if (field.pristine) return - if (isEmpty(field.lastValidateValue) && isEmpty(value)) return - if (isEqual(field.lastValidateValue, value)) { - return - } - } - - const title = field.props && field.props.title - const rafId = setTimeout(() => { - field.loading = true - field.dirty = true - if (field.notify) { - field.notify() - } - }, 100) - queue.push( - Promise.all( - toArr(field.rules).map(rule => { - return validate(value, rule, values, (title as string) || name) - }) - ).then(errors => { - clearTimeout(rafId) - const lastFieldErrors = toArr(field.errors) - const lastValid = field.valid - const lastLoading = field.loading - const newErrors = flatArr(toArr(errors)) - const effectErrors = flatArr(toArr(field.effectErrors)) - field.loading = false - field.errors = newErrors - field.effectErrors = effectErrors - if (forceUpdate) { - if (newErrors.length || effectErrors.length) { - field.valid = false - field.invalid = true - } else { - field.valid = true - field.invalid = false - } - field.dirty = true - } else { - if (!field.pristine) { - if (newErrors.length || effectErrors.length) { - field.valid = false - field.invalid = true - } else { - field.valid = true - field.invalid = false - } - if ( - !isEqual(lastValid, field.valid) || - !isEqual(lastFieldErrors, field.errors) - ) { - field.dirty = true - } - } - } - - if (field.loading !== lastLoading) { - field.dirty = true - } - - if (field.dirty && field.notify) { - field.notify() - } - field.lastValidateValue = clone(value) - return { - name, - value, - field, - invalid: field.invalid, - valid: field.valid, - errors: newErrors.concat(effectErrors) - } - }) - ) - }) - - return Promise.all(queue).then(response => { - if (isFn(callback)) { - callback(response) - } - return response - }) -} - -export default runValidation +export * from './validator' +export * from './types' diff --git a/packages/validator/src/locale.ts b/packages/validator/src/locale.ts new file mode 100644 index 00000000000..fe62480e453 --- /dev/null +++ b/packages/validator/src/locale.ts @@ -0,0 +1,54 @@ +export default { + en: { + pattern: 'This field does not match any pattern', + required: 'This field is required', + number: 'This field is not a number', + integer: 'This field is not an integer number', + url: 'This field is a invalid url', + email: 'This field is not a email format', + ipv6: 'This field is not a ipv6 format', + ipv4: 'This field is not a ipv4 format', + idcard: 'This field is not an idcard format', + taodomain: 'This field is not a taobao domain format', + qq: 'This field is not a qq number format', + phone: 'This field is not a phone number format', + money: 'This field is not a currency format', + zh: 'This field is not a chinese string', + date: 'This field is not a valid date format', + zip: 'This field is not a zip format', + len: 'The length or number of entries must be {{len}}', + min: 'The length or number of entries must be at least {{min}}', + maximum: 'The value cannot be greater than {{maximum}}', + exclusiveMaximum: 'The value must be less than {{exclusiveMaximum}}', + minimum: 'The value cannot be less than {{minimum}}', + exclusiveMinimum: 'The value must be greater than {{exclusiveMinimum}}', + max: 'The length or number of entries must be at most {{max}}', + whitespace: 'This field cannot be empty' + }, + zh: { + pattern: '该字段不是一个合法的字段', + required: '该字段是必填字段', + number: '该字段不是合法的数字', + integer: '该字段不是合法的整型数字', + url: '该字段不是合法的url', + email: '该字段不是合法的邮箱格式', + ipv6: '该字段不是合法的ipv6格式', + ipv4: '该字段不是合法的ipv4格式', + idcard: '该字段不是合法的身份证格式', + taodomain: '该字段不符合淘系域名规则', + qq: '该字段不符合QQ号格式', + phone: '该字段不是有效的手机号', + money: '该字段不是有效货币格式', + zh: '该字段不是合法的中文字符串', + date: '该字段不是合法的日期格式', + zip: '该字段不是合法的邮编格式', + len: '长度或条目数必须为{{len}}', + min: '长度或条目数不能小于{{min}}', + max: '长度或条目数不能大于{{max}}', + maximum: '数值不能大于{{maximum}}', + exclusiveMaximum: '数值必须小于{{exclusiveMaximum}}', + minimum: '数值不能小于{{minimum}}', + exclusiveMinimum: '数值必须大于{{exclusiveMinimum}}', + whitespace: 'This field cannot be empty' + } +} diff --git a/packages/validator/src/locale/index.ts b/packages/validator/src/locale/index.ts deleted file mode 100644 index 8c88dd4a724..00000000000 --- a/packages/validator/src/locale/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -export default { - en: { - pattern: '%s value %s does not match pattern %s', - required: '%s is required', - number: '%s is not a number', - integer: '%s is not an integer number', - url: '%s is a invalid url', - email: '%s is not a email format', - ipv6: '%s is not a ipv6 format', - ipv4: '%s is not a ipv4 format', - idcard: '%s is not an idcard format', - taodomain: '%s is not a taobao domain format', - qq: '%s is not a qq number format', - phone: '%s is not a phone number format', - money: '%s is not a currency format', - zh: '%s is not a chinese string', - date: '%s is not a valid date format', - zip: '%s is not a zip format' - }, - zh: { - pattern: '该字段不是一个合法的字段', - required: '该字段是必填字段', - number: '该字段不是合法的数字', - integer: '该字段不是合法的整型数字', - url: '该字段不是合法的url', - email: '该字段不是合法的邮箱格式', - ipv6: '该字段不是合法的ipv6格式', - ipv4: '该字段不是合法的ipv4格式', - idcard: '该字段不是合法的身份证格式', - taodomain: '该字段不符合淘系域名规则', - qq: '该字段不符合QQ号格式', - phone: '该字段不是有效的手机号', - money: '该字段不是有效货币格式', - zh: '该字段不是合法的中文字符串', - date: '该字段不是合法的日期格式', - zip: '该字段不是合法的邮编格式' - } -} diff --git a/packages/validator/src/message.ts b/packages/validator/src/message.ts index cb5388f2d76..d36b70a5e76 100644 --- a/packages/validator/src/message.ts +++ b/packages/validator/src/message.ts @@ -1,6 +1,13 @@ -import { getIn, each, globalThisPolyfill } from './utils' +import { + FormPath, + each, + globalThisPolyfill, + merge as deepmerge +} from '@uform/shared' import locales from './locale' +const getIn = FormPath.getIn + const self: any = globalThisPolyfill export interface ILocaleMessages { @@ -34,19 +41,26 @@ const getMatchLang = (lang: string) => { return find } -export const setLocale = (locale: ILocales) => { - Object.assign(LOCALE.messages, locale) +export const setValidationLocale = (locale: ILocales) => { + LOCALE.messages = deepmerge(LOCALE.messages, locale) } -export const setLanguage = (lang: string) => { +export const setLocale = setValidationLocale + +export const setValidationLanguage = (lang: string) => { LOCALE.lang = lang } +export const setLanguage = setValidationLanguage + export const getMessage = (path: string) => { - return ( - getIn(LOCALE.messages, `${getMatchLang(LOCALE.lang)}.${path}`) || - 'field is not valid,but not found error message.' - ) + const message = getIn(LOCALE.messages, `${getMatchLang(LOCALE.lang)}.${path}`) + if (!message && console && console.error) { + console.error( + `field is not valid,but not found ${path} error message. Please set the language pack first through setValidationLocale` + ) + } + return message || 'Field is invalid' } -setLocale(locales) +setValidationLocale(locales) diff --git a/packages/validator/src/rules.ts b/packages/validator/src/rules.ts new file mode 100644 index 00000000000..e4f04740831 --- /dev/null +++ b/packages/validator/src/rules.ts @@ -0,0 +1,87 @@ +import { getMessage } from './message' +import { + isEmpty, + stringLength, + isStr, + isFn, + toArr, + isBool +} from '@uform/shared' +import { ValidateDescription } from './types' +const isValidateEmpty = (value: any) => { + if (typeof value === 'object') { + for (let key in value) { + if (value.hasOwnProperty(key)) { + if (!isValidateEmpty(value[key])) return false + } + } + return true + } else { + return isEmpty(value) + } +} + +const getLength = (value: any) => + isStr(value) ? stringLength(value) : value ? value.length : 0 + +export default { + required(value: any, rule: ValidateDescription) { + return isValidateEmpty(value) ? getMessage('required') : '' + }, + max(value: any, rule: ValidateDescription) { + const length = getLength(value) + const max = Number(rule.max) + return length > max ? getMessage('max') : '' + }, + maximum(value: any, rule: ValidateDescription) { + return Number(value) > Number(rule.maximum) ? getMessage('maximum') : '' + }, + exclusiveMaximum(value: any, rule: ValidateDescription) { + return Number(value) >= Number(rule.maximum) + ? getMessage('exclusiveMaximum') + : '' + }, + minimum(value: any, rule: ValidateDescription) { + return Number(value) < Number(rule.minimum) ? getMessage('minimum') : '' + }, + exclusiveMinimum(value: any, rule: ValidateDescription) { + return Number(value) <= Number(rule.minimum) + ? getMessage('exclusiveMinimum') + : '' + }, + len(value: any, rule: ValidateDescription) { + const length = getLength(value) + const len = Number(rule.len) + return length !== len ? getMessage('len') : '' + }, + min(value: any, rule: ValidateDescription) { + const length = getLength(value) + const min = Number(rule.len) + return length < min ? getMessage('min') : '' + }, + pattern(value: any, rule: ValidateDescription) { + return !new RegExp(rule.pattern).test(value) + ? rule.message || getMessage('pattern') + : '' + }, + async validator(value: any, rule: ValidateDescription) { + if (isFn(rule.validator)) { + const response = await Promise.resolve(rule.validator(value, rule)) + if (isBool(response)) { + return response ? rule.message : '' + } else { + return response + } + } + throw new Error("The rule's validator property must be a function.") + }, + whitespace(value: any, rule: ValidateDescription) { + if (rule.whitespace) { + return /^\s+$/.test(value) || value === '' ? getMessage('whitespace') : '' + } + }, + enum(value: any, rule: ValidateDescription) { + const enums = toArr(rule.enum) + return enums.indexOf(value) === -1 ? getMessage('enum') : '' + } +} diff --git a/packages/validator/src/types.ts b/packages/validator/src/types.ts new file mode 100644 index 00000000000..788d03ac6d3 --- /dev/null +++ b/packages/validator/src/types.ts @@ -0,0 +1,94 @@ +export interface ValidatorOptions { + validateFirst?: boolean +} + +export type ValidateNode = ( + options: ValidateFieldOptions +) => Promise<{ + errors: string[] + warnings: string[] +}> + +export type ValidateNodeMap = { + [key in string]: ValidateNode +} + +export type ValidateFormatsMap = { + [key in string]: RegExp +} + +export interface ValidateDescription { + format?: string + validator?: CustomValidator + required?: boolean + pattern?: RegExp | string + max?: number + maximum?: number + exclusiveMaximum?: number + exclusiveMinimum?: number + minimum?: number + min?: number + len?: number + whitespace?: boolean + enum?: any[] + message?: string +} + +export type ValidateRules = ValidateDescription[] + +export type ValidateArrayRules = Array< + string | CustomValidator | ValidateDescription +> + +export type ValidatePatternRules = + | string + | CustomValidator + | ValidateDescription + | ValidateArrayRules + +export type CustomValidator = ( + value: any, + rescription: ValidateDescription +) => ValidateResponse + +export type ValidateResponse = + | null + | string + | boolean + | { + type?: 'error' | 'warning' + message: string + } + +export type ValidateRulesMap = { + [key in string]: ( + value: any, + description: ValidateDescription + ) => ValidateResponse | Promise +} + +export interface ValidateFieldOptions { + first?: boolean + key?: string +} + +export type ValidateCalculator = ( + validate: ( + value: any, + rules: ValidatePatternRules + ) => Promise<{ + errors: string[] + warnings: string[] + }> +) => void + +export interface ValidateNodeResult { + errors: Array<{ + path: string + messages: string[] + }> + warnings: Array<{ + path: string + messages: string[] + }> +} diff --git a/packages/validator/src/utils.ts b/packages/validator/src/utils.ts deleted file mode 100644 index 67e16b7ae3e..00000000000 --- a/packages/validator/src/utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -export * from '@uform/utils' - -const formatRegExp = /%[sdj%]/g - -export function format(...args: any[]) { - let i = 1 - const f = args[0] - const len = args.length - if (typeof f === 'function') { - return f.apply(null, args.slice(1)) - } - if (typeof f === 'string') { - const str = String(f).replace(formatRegExp, (x: string) => { - if (x === '%%') { - return '%' - } - if (i >= len) { - return x - } - switch (x) { - case '%s': - return String(args[i++]) - case '%d': - return Number(args[i++]) + '' - case '%j': - try { - return JSON.stringify(args[i++]) - } catch (_) { - return '[Circular]' - } - default: - return x - } - }) - return str - } - return f -} diff --git a/packages/validator/src/validator.ts b/packages/validator/src/validator.ts new file mode 100644 index 00000000000..e32c6267f6a --- /dev/null +++ b/packages/validator/src/validator.ts @@ -0,0 +1,277 @@ +import { + ValidatorOptions, + ValidateNodeMap, + ValidatePatternRules, + ValidateRules, + ValidateFormatsMap, + ValidateRulesMap, + ValidateResponse, + ValidateDescription, + ValidateFieldOptions, + ValidateCalculator, + ValidateNode, + ValidateNodeResult +} from './types' +import { + isFn, + isStr, + isArr, + isObj, + each, + FormPath, + FormPathPattern +} from '@uform/shared' +import { getMessage } from './message' +import defaultFormats from './formats' +import defaultRules from './rules' + +//校验规则集合 +const ValidatorRules: ValidateRulesMap = {} + +//校验格式集合 +const ValidatorFormators: ValidateFormatsMap = {} + +//模板引擎 +const template = (message: ValidateResponse, context: any): string => { + if (isStr(message)) { + if (isFn(FormValidator.template)) { + return FormValidator.template(message, context) + } + return message.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, $0) => { + return FormPath.getIn(context, $0) + }) + } else if (isObj(message)) { + return template(message.message, context) + } else { + return '' + } +} + +class FormValidator { + private validateFirst: boolean + private nodes: ValidateNodeMap + + constructor(options: ValidatorOptions = {}) { + this.validateFirst = options.validateFirst + this.nodes = {} + } + + transformRules(rules: ValidatePatternRules) { + if (isStr(rules)) { + if (!ValidatorFormators[rules]) { + throw new Error('Can not found validator pattern') + } + return [ + { + pattern: ValidatorFormators[rules], + message: getMessage(rules) || 'Can not found validator message.' + } + ] + } else if (isFn(rules)) { + return [ + { + validator: rules + } + ] + } else if (isArr(rules)) { + return rules.reduce((buf, rule) => { + return buf.concat(this.transformRules(rule)) + }, []) + } else if (isObj(rules)) { + if (rules.format) { + if (!ValidatorFormators[rules.format]) { + throw new Error('Can not found validator pattern') + } + rules.pattern = ValidatorFormators[rules.format] + rules.message = rules.message || getMessage(rules.format) + } + return [rules] + } + return [] + } + + async internalValidate( + value: any, + rules: ValidateRules, + options: ValidateFieldOptions = {} + ): Promise<{ + errors: string[] + warnings: string[] + }> { + const first = + options.first !== undefined ? !!options.first : !!this.validateFirst + const errors: string[] = [] + const warnings = [] + try { + for (let i = 0; i < rules.length; i++) { + const ruleObj = rules[i] + const keys = Object.keys(ruleObj).sort(key => + key === 'validator' ? 1 : -1 + ) + for (let l = 0; l < keys.length; l++) { + let key = keys[l] + if (ruleObj.hasOwnProperty(key) && ruleObj[key] !== undefined) { + const rule = ValidatorRules[key] + if (rule) { + const payload = await rule(value, ruleObj) + const message = template(payload, { + ...ruleObj, + value, + key: options.key + }) + if (isStr(payload)) { + if (first) { + if (message) { + errors.push(message) + throw new Error(message) + } + } + if (message) errors.push(message) + } else if (isObj(payload)) { + if (payload.type === 'warning') { + if (message) warnings.push(message) + } else { + if (first) { + if (message) { + errors.push(message) + throw new Error(message) + } + } + if (message) errors.push(message) + } + } + } + } + } + } + return { + errors, + warnings + } + } catch (e) { + return { + errors, + warnings + } + } + } + + async validateNodes( + pattern: FormPath, + options: ValidateFieldOptions + ): Promise { + const errors = [] + const warnings = [] + let promise = Promise.resolve({ errors, warnings }) + each(this.nodes, (validator, path) => { + if (pattern.match(path)) { + promise = promise.then(async ({ errors, warnings }) => { + const result = await validator(options) + return { + errors: result.errors.length + ? errors.concat({ + path: path.toString(), + messages: result.errors + }) + : errors, + warnings: result.warnings.length + ? warnings.concat({ + path: path.toString(), + messages: result.warnings + }) + : warnings + } + }) + } + }) + return promise.catch(error => { + console.error(error) + return { + errors: [], + warnings: [] + } + }) + } + + validate = ( + path?: FormPathPattern, + options?: ValidateFieldOptions + ): Promise => { + const pattern = FormPath.getPath(path || '*') + return this.validateNodes(pattern, options) + } + + register = (path: FormPathPattern, calculator: ValidateCalculator) => { + const newPath = FormPath.getPath(path) + this.nodes[newPath.toString()] = (options: ValidateFieldOptions) => { + return new Promise((resolve, reject) => { + const validate = async (value: any, rules: ValidatePatternRules) => { + const data = { + ...options, + key: newPath.toString() + } + return this.internalValidate( + value, + this.transformRules(rules), + data + ).then( + payload => { + resolve(payload) + return payload + }, + payload => { + reject(payload) + return Promise.reject(payload) + } + ) + } + calculator(validate) + }) + } + } + + unregister = (path: FormPathPattern) => { + const newPath = FormPath.getPath(path) + delete this.nodes[newPath.toString()] + } + + static template: ( + message: ValidateResponse, + data: ValidateDescription & { value: any; key: string } + ) => string + + //注册通用规则 + static registerRules(rules: ValidateRulesMap) { + each(rules, (rule, key) => { + if (isFn(rule)) { + ValidatorRules[key] = rule + } + }) + } + /** + * https://github.com/alibaba/uform/issues/215 + * + * @static + * @param {ValidateFormatsMap} formats + * @memberof FormValidator + */ + static registerFormats(formats: ValidateFormatsMap) { + each(formats, (pattern, key) => { + if (isStr(pattern) || pattern instanceof RegExp) { + ValidatorFormators[key] = new RegExp(pattern) + } + }) + } + + //注册校验消息模板引擎 + static registerMTEngine = template => { + if (isFn(template)) { + FormValidator.template = template + } + } +} + +FormValidator.registerFormats(defaultFormats) +FormValidator.registerRules(defaultRules) + +export { FormValidator } diff --git a/packages/validator/src/validators/custom.ts b/packages/validator/src/validators/custom.ts deleted file mode 100644 index a458b4991b8..00000000000 --- a/packages/validator/src/validators/custom.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { isFn } from '../utils' -import { IRuleDescription } from '@uform/types' -export default ( - value: any, - rule: IRuleDescription, - values: any, - name: string -) => { - if (isFn(rule.validator)) { - return rule.validator(value, rule, values, name) - } -} diff --git a/packages/validator/src/validators/format.ts b/packages/validator/src/validators/format.ts deleted file mode 100644 index 79cb200a1ef..00000000000 --- a/packages/validator/src/validators/format.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { format } from '../utils' -import { getMessage } from '../message' -import { patternValidate } from './pattern' -import { IRuleDescription } from '@uform/types' -import RegExpPatterns from './regexp' - -const PatternKeys = Object.keys(RegExpPatterns) - -const batchValidate = ( - value: any, - rule: IRuleDescription, - values: any, - name: string -) => { - for (let i = 0; i < PatternKeys.length; i++) { - if (PatternKeys[i] === rule.format) { - return patternValidate( - RegExpPatterns[PatternKeys[i]], - value, - format(rule.message || getMessage(rule.format), name, value) - ) - } - } -} - -export default ( - value: any, - rule: IRuleDescription, - values: any, - name: string -) => { - return batchValidate(value, rule, values, name) -} diff --git a/packages/validator/src/validators/index.ts b/packages/validator/src/validators/index.ts deleted file mode 100644 index aa6fe36880d..00000000000 --- a/packages/validator/src/validators/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { IRuleDescription, Rule } from '@uform/types' -import { isObj, isStr, isFn } from '../utils' -import formatValidate from './format' -import requiredValidate from './required' -import patternValidate from './pattern' -import customValidate from './custom' -/* - * rule : { - format:"", - * required:true, - * message:"", - * pattern:"", - * validator(value,rule,callback,values){ - * } - * } - * -**/ - -const batchInvoke = (...fns: Array<(...args: any[]) => void>) => { - return (...args: any[]) => { - return fns.map(fn => Promise.resolve(fn(...args))) - } -} - -const batchValidate = ( - value: any, - rule: IRuleDescription, - values: any, - name: string -) => { - return Promise.all( - batchInvoke( - formatValidate, - requiredValidate, - patternValidate, - customValidate - )(value, rule, values, name) - ) -} - -export const validate = (value: any, rule: Rule, values: any, name: string) => { - const newRule = isObj(rule) - ? rule - : isStr(rule) - ? { format: rule } - : isFn(rule) - ? { validator: rule } - : {} - return batchValidate(value, newRule as IRuleDescription, values, name) -} diff --git a/packages/validator/src/validators/pattern.ts b/packages/validator/src/validators/pattern.ts deleted file mode 100644 index a1501a78770..00000000000 --- a/packages/validator/src/validators/pattern.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { isRegExp, format, isFn, isEmpty } from '../utils' -import { getMessage } from '../message' -import { IRuleDescription } from '@uform/types' - -export const patternValidate = ( - pattern: string | RegExp | ((...args: any[]) => boolean), - value: any, - message: string -) => { - if (isEmpty(value)) { - return '' - } - if (isRegExp(pattern)) { - pattern.lastIndex = 0 - } - const valid = isFn(pattern) - ? pattern(value) - : isRegExp(pattern) - ? pattern.test(String(value)) - : new RegExp(String(pattern)).test(String(value)) - return !valid ? message : '' -} - -export default ( - value: any, - rule: IRuleDescription, - values: any, - name: string -) => { - if (rule.pattern) { - return patternValidate( - rule.pattern, - value, - format(rule.message || getMessage('pattern'), name, value, rule.pattern) - ) - } -} diff --git a/packages/validator/src/validators/required.ts b/packages/validator/src/validators/required.ts deleted file mode 100644 index 63f0e243e1a..00000000000 --- a/packages/validator/src/validators/required.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { format, isEmpty } from '../utils' -import { getMessage } from '../message' -import { IRuleDescription } from '@uform/types' - -const isValidateEmpty = (value: any) => { - if (typeof value === 'object') { - for (let key in value) { - if (value.hasOwnProperty(key)) { - if (!isValidateEmpty(value[key])) return false - } - } - return true - } else { - return isEmpty(value) - } -} - -export default ( - value: any, - rule: IRuleDescription, - values: any, - name: string -) => { - if (rule.required) { - return isValidateEmpty(value) - ? format(rule.message || getMessage('required'), name) - : '' - } -} diff --git a/scripts/jest.base.js b/scripts/jest.base.js index 2f8e215763a..79e5ea9ce32 100644 --- a/scripts/jest.base.js +++ b/scripts/jest.base.js @@ -22,6 +22,7 @@ module.exports = { '^.+\\.jsx?$': 'babel-jest' }, preset: 'ts-jest', + testMatch: ['**/__tests__/**/*.[jt]s?(x)'], setupFilesAfterEnv: [ require.resolve('jest-dom/extend-expect'), require.resolve('@testing-library/react/cleanup-after-each'), From afa1dcf091343ff638cad5e13a7edd1139621899 Mon Sep 17 00:00:00 2001 From: "ziyi.hzy" Date: Thu, 24 Oct 2019 23:53:32 +0800 Subject: [PATCH 02/99] feat: add schema-editor --- package.json | 1 + packages/schema-editor/README.md | 10 +++ .../src/components/FieldEditor.tsx | 68 ++++++++++++++++ .../src/components/JsonDialog.tsx | 0 .../src/components/SchemaCode.tsx | 24 ++++++ .../src/components/SchemaPreview.tsx | 6 ++ .../src/components/SchemaTree.tsx | 21 +++++ packages/schema-editor/src/index.tsx | 31 +++++++ packages/schema-editor/src/main.scss | 69 ++++++++++++++++ .../schema-editor/src/utils/components.ts | 81 +++++++++++++++++++ packages/schema-editor/src/utils/index.ts | 4 + packages/schema-editor/src/utils/schema.ts | 26 ++++++ packages/schema-editor/src/utils/types.ts | 13 +++ 13 files changed, 354 insertions(+) create mode 100644 packages/schema-editor/README.md create mode 100644 packages/schema-editor/src/components/FieldEditor.tsx create mode 100644 packages/schema-editor/src/components/JsonDialog.tsx create mode 100644 packages/schema-editor/src/components/SchemaCode.tsx create mode 100644 packages/schema-editor/src/components/SchemaPreview.tsx create mode 100644 packages/schema-editor/src/components/SchemaTree.tsx create mode 100644 packages/schema-editor/src/index.tsx create mode 100644 packages/schema-editor/src/main.scss create mode 100644 packages/schema-editor/src/utils/components.ts create mode 100644 packages/schema-editor/src/utils/index.ts create mode 100644 packages/schema-editor/src/utils/schema.ts create mode 100644 packages/schema-editor/src/utils/types.ts diff --git a/package.json b/package.json index 180f92fd0c2..6d3f5e41f75 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "test:prod": "cross-env TEST_ENV=production npm run build && jest", "doc:core": "doc-scripts start -i packages/core", "doc:react": "doc-scripts start -i packages/react", + "doc:schema-editor": "doc-scripts start -i packages/schema-editor", "test:watch": "jest --watch", "coverage": "npm run test -- --coverage", "coverage:upload": "rm -rf ./coverage && npm run coverage && node ./scripts/mapCoverage.js && codecov", diff --git a/packages/schema-editor/README.md b/packages/schema-editor/README.md new file mode 100644 index 00000000000..a2a2ac112fc --- /dev/null +++ b/packages/schema-editor/README.md @@ -0,0 +1,10 @@ +# schema-editor + +## Demo + +```jsx +import React from 'react' +import { SchemaEditor } from './src' + +ReactDOM.render(, document.getElementById('root')) +``` diff --git a/packages/schema-editor/src/components/FieldEditor.tsx b/packages/schema-editor/src/components/FieldEditor.tsx new file mode 100644 index 00000000000..775e3f58ebe --- /dev/null +++ b/packages/schema-editor/src/components/FieldEditor.tsx @@ -0,0 +1,68 @@ +import { Input, Select, Checkbox, Button, Icon } from '@alifd/next' +import React from 'react' + +const typeOptions = [ + { label: '字符串', value: 'string' }, + { label: '布尔值', value: 'boolean' }, + { label: '数字', value: 'number' }, + { label: '数组', value: 'array' }, + { label: '对象', value: 'object' } +] + +export const FieldEditor: React.FC = ({ children }) => { + return ( +
    +
    + + + +
    +
    + + + + + + +
    +
    +
    {children}
    +
    + ) +} diff --git a/packages/schema-editor/src/components/JsonDialog.tsx b/packages/schema-editor/src/components/JsonDialog.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/schema-editor/src/components/SchemaCode.tsx b/packages/schema-editor/src/components/SchemaCode.tsx new file mode 100644 index 00000000000..e817715480c --- /dev/null +++ b/packages/schema-editor/src/components/SchemaCode.tsx @@ -0,0 +1,24 @@ +import React from 'react' +// import MonacoEditor from 'react-monaco-editor' +import { ISchemaCodeProps } from '../utils/types' + +export const SchemaCode: React.FC = ({ + schema, + onChange +}) => { + if (typeof schema === 'object') { + schema = JSON.stringify(schema, null, '\t') + } + + return ( +
    + {/* */} +
    + ) +} diff --git a/packages/schema-editor/src/components/SchemaPreview.tsx b/packages/schema-editor/src/components/SchemaPreview.tsx new file mode 100644 index 00000000000..9b639a41287 --- /dev/null +++ b/packages/schema-editor/src/components/SchemaPreview.tsx @@ -0,0 +1,6 @@ +import * as React from 'react' +import { ISchemaPreviewProps } from '../utils/types' + +export const SchemaPreview: React.FC = ({ schema }) => { + return
    SchemaPreview
    +} diff --git a/packages/schema-editor/src/components/SchemaTree.tsx b/packages/schema-editor/src/components/SchemaTree.tsx new file mode 100644 index 00000000000..62c98bcf7ff --- /dev/null +++ b/packages/schema-editor/src/components/SchemaTree.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { ISchemaTreeProps } from '../utils/types' +import { FieldEditor } from './FieldEditor' + +export const SchemaTree: React.FC = ({ + schema, + onChange +}) => { + return ( +
    + + + + + + + + +
    + ) +} diff --git a/packages/schema-editor/src/index.tsx b/packages/schema-editor/src/index.tsx new file mode 100644 index 00000000000..9002ec9445d --- /dev/null +++ b/packages/schema-editor/src/index.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import { Button, Icon, Grid } from '@alifd/next' +import { jsonToSchema } from './utils' +import { SchemaTree } from './components/SchemaTree' +import { SchemaCode } from './components/SchemaCode' +import json from './utils/schema' +const { Row, Col } = Grid + +export const SchemaEditor: React.FC = () => { + const initialSchema = jsonToSchema(json) + const [schema, setSchema] = React.useState(initialSchema) + + return ( +
    +
    + + +
    + + + + + + + + +
    + ) +} diff --git a/packages/schema-editor/src/main.scss b/packages/schema-editor/src/main.scss new file mode 100644 index 00000000000..b4abed7ba9c --- /dev/null +++ b/packages/schema-editor/src/main.scss @@ -0,0 +1,69 @@ +/* write style here */ +.schema-editor { + padding: 20px; + + /* 操作区 */ + .schema-menus { + padding: 0 20px 20px 20px; + border-bottom: 1px solid #eee; + + .schema-preview-btn { + float: right; + } + } + + .splitter { + border-right: 1px dashed #aaa; + } + + /* 编辑区 */ + .schema-editor-main { + margin-top: 20px; + + .schema-col { + padding: 20px; + min-height: 400px; + } + + /* 树形编辑 */ + .schema-tree {} + + /* 代码编辑 */ + .schema-code {} + } + + .field-editor { + margin-top: 10px; + padding: 5px; + border: 1px solid #eee; + + .field-operate { + padding: 5px; + + .op-btn { + margin-left: 5px; + } + + .op-btn-expand { + float: right !important; + } + } + + .field-group { + background-color: #F2F3F7; + padding: 5px; + + .field-input { + margin-left: 5px; + width: 100px; + } + } + + .field-children { + padding: 10px 0 0 20px; + } + + + } + +} diff --git a/packages/schema-editor/src/utils/components.ts b/packages/schema-editor/src/utils/components.ts new file mode 100644 index 00000000000..1440de4af17 --- /dev/null +++ b/packages/schema-editor/src/utils/components.ts @@ -0,0 +1,81 @@ +export default { + next: [ + { + name: 'Affix', + title: '固钉', + typeId: 6, + props: { + container: { + type: { + name: 'func' + }, + required: false, + description: '设置 Affix 需要监听滚动事件的容器元素', + defaultValue: { + value: '() => window', + computed: false + }, + docblock: + '设置 Affix 需要监听滚动事件的容器元素\n@return {ReactElement} 目标容器元素的实例', + params: [], + returns: { + description: '目标容器元素的实例', + type: { + name: 'ReactElement' + } + } + }, + offsetTop: { + type: { + name: 'number' + }, + required: false, + description: '距离窗口顶部达到指定偏移量后触发', + docblock: '距离窗口顶部达到指定偏移量后触发' + }, + offsetBottom: { + type: { + name: 'number' + }, + required: false, + description: '距离窗口底部达到制定偏移量后触发', + docblock: '距离窗口底部达到制定偏移量后触发' + }, + onAffix: { + type: { + name: 'func' + }, + required: false, + description: '当元素的样式发生固钉样式变化时触发的回调函数', + defaultValue: { + value: 'func.noop', + computed: true + }, + docblock: + '当元素的样式发生固钉样式变化时触发的回调函数\n@param {Boolean} 元素是否被固钉', + params: [ + { + name: '元素是否被固钉', + description: null, + type: { + name: 'Boolean' + } + } + ], + returns: null + }, + useAbsolute: { + type: { + name: 'bool' + }, + required: false, + description: '是否启用绝对布局实现 affix', + docblock: + '是否启用绝对布局实现 affix\n@param {Boolean} 是否启用绝对布局' + } + }, + methods: [], + subComponents: [] + } + ] +} diff --git a/packages/schema-editor/src/utils/index.ts b/packages/schema-editor/src/utils/index.ts new file mode 100644 index 00000000000..cf4f4628bfb --- /dev/null +++ b/packages/schema-editor/src/utils/index.ts @@ -0,0 +1,4 @@ +export function jsonToSchema(json?: object) { + let schema = json || {} + return schema +} diff --git a/packages/schema-editor/src/utils/schema.ts b/packages/schema-editor/src/utils/schema.ts new file mode 100644 index 00000000000..c1ddd95557b --- /dev/null +++ b/packages/schema-editor/src/utils/schema.ts @@ -0,0 +1,26 @@ +export default { + name: { + title: '姓名', + description: '请输入姓名', + default: '淘小宝', + type: 'string', + 'x-props': { + help: '不得超过6个汉字' + }, + 'x-component': 'Input', + 'x-component-props': { + disabled: true + }, + 'x-rules': [ + { + required: true + }, + { + pattern: '[\u4e00-\u9fa5]' + }, + { + validator: 'nameValidator' + } + ] + } +} diff --git a/packages/schema-editor/src/utils/types.ts b/packages/schema-editor/src/utils/types.ts new file mode 100644 index 00000000000..640083c8b35 --- /dev/null +++ b/packages/schema-editor/src/utils/types.ts @@ -0,0 +1,13 @@ +export interface ISchemaCodeProps { + schema: any + onChange?: any +} + +export interface ISchemaTreeProps { + schema: object + onChange?: Function +} + +export interface ISchemaPreviewProps { + schema: object +} From be68d3dfb57f9583cdc7c6e2b05871060d4e2b77 Mon Sep 17 00:00:00 2001 From: "ziyi.hzy" Date: Fri, 25 Oct 2019 10:28:15 +0800 Subject: [PATCH 03/99] feat: rename schema-editor -> react-schema-editor --- package.json | 2 +- packages/{schema-editor => react-schema-editor}/README.md | 0 .../src/components/FieldEditor.tsx | 0 .../src/components/JsonDialog.tsx | 0 .../src/components/SchemaCode.tsx | 0 .../src/components/SchemaPreview.tsx | 0 .../src/components/SchemaTree.tsx | 0 packages/{schema-editor => react-schema-editor}/src/index.tsx | 0 packages/{schema-editor => react-schema-editor}/src/main.scss | 0 .../src/utils/components.ts | 0 .../{schema-editor => react-schema-editor}/src/utils/index.ts | 0 .../{schema-editor => react-schema-editor}/src/utils/schema.ts | 0 .../{schema-editor => react-schema-editor}/src/utils/types.ts | 0 13 files changed, 1 insertion(+), 1 deletion(-) rename packages/{schema-editor => react-schema-editor}/README.md (100%) rename packages/{schema-editor => react-schema-editor}/src/components/FieldEditor.tsx (100%) rename packages/{schema-editor => react-schema-editor}/src/components/JsonDialog.tsx (100%) rename packages/{schema-editor => react-schema-editor}/src/components/SchemaCode.tsx (100%) rename packages/{schema-editor => react-schema-editor}/src/components/SchemaPreview.tsx (100%) rename packages/{schema-editor => react-schema-editor}/src/components/SchemaTree.tsx (100%) rename packages/{schema-editor => react-schema-editor}/src/index.tsx (100%) rename packages/{schema-editor => react-schema-editor}/src/main.scss (100%) rename packages/{schema-editor => react-schema-editor}/src/utils/components.ts (100%) rename packages/{schema-editor => react-schema-editor}/src/utils/index.ts (100%) rename packages/{schema-editor => react-schema-editor}/src/utils/schema.ts (100%) rename packages/{schema-editor => react-schema-editor}/src/utils/types.ts (100%) diff --git a/package.json b/package.json index 6d3f5e41f75..20829c6663f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test:prod": "cross-env TEST_ENV=production npm run build && jest", "doc:core": "doc-scripts start -i packages/core", "doc:react": "doc-scripts start -i packages/react", - "doc:schema-editor": "doc-scripts start -i packages/schema-editor", + "doc:editor": "doc-scripts start -i packages/react-schema-editor", "test:watch": "jest --watch", "coverage": "npm run test -- --coverage", "coverage:upload": "rm -rf ./coverage && npm run coverage && node ./scripts/mapCoverage.js && codecov", diff --git a/packages/schema-editor/README.md b/packages/react-schema-editor/README.md similarity index 100% rename from packages/schema-editor/README.md rename to packages/react-schema-editor/README.md diff --git a/packages/schema-editor/src/components/FieldEditor.tsx b/packages/react-schema-editor/src/components/FieldEditor.tsx similarity index 100% rename from packages/schema-editor/src/components/FieldEditor.tsx rename to packages/react-schema-editor/src/components/FieldEditor.tsx diff --git a/packages/schema-editor/src/components/JsonDialog.tsx b/packages/react-schema-editor/src/components/JsonDialog.tsx similarity index 100% rename from packages/schema-editor/src/components/JsonDialog.tsx rename to packages/react-schema-editor/src/components/JsonDialog.tsx diff --git a/packages/schema-editor/src/components/SchemaCode.tsx b/packages/react-schema-editor/src/components/SchemaCode.tsx similarity index 100% rename from packages/schema-editor/src/components/SchemaCode.tsx rename to packages/react-schema-editor/src/components/SchemaCode.tsx diff --git a/packages/schema-editor/src/components/SchemaPreview.tsx b/packages/react-schema-editor/src/components/SchemaPreview.tsx similarity index 100% rename from packages/schema-editor/src/components/SchemaPreview.tsx rename to packages/react-schema-editor/src/components/SchemaPreview.tsx diff --git a/packages/schema-editor/src/components/SchemaTree.tsx b/packages/react-schema-editor/src/components/SchemaTree.tsx similarity index 100% rename from packages/schema-editor/src/components/SchemaTree.tsx rename to packages/react-schema-editor/src/components/SchemaTree.tsx diff --git a/packages/schema-editor/src/index.tsx b/packages/react-schema-editor/src/index.tsx similarity index 100% rename from packages/schema-editor/src/index.tsx rename to packages/react-schema-editor/src/index.tsx diff --git a/packages/schema-editor/src/main.scss b/packages/react-schema-editor/src/main.scss similarity index 100% rename from packages/schema-editor/src/main.scss rename to packages/react-schema-editor/src/main.scss diff --git a/packages/schema-editor/src/utils/components.ts b/packages/react-schema-editor/src/utils/components.ts similarity index 100% rename from packages/schema-editor/src/utils/components.ts rename to packages/react-schema-editor/src/utils/components.ts diff --git a/packages/schema-editor/src/utils/index.ts b/packages/react-schema-editor/src/utils/index.ts similarity index 100% rename from packages/schema-editor/src/utils/index.ts rename to packages/react-schema-editor/src/utils/index.ts diff --git a/packages/schema-editor/src/utils/schema.ts b/packages/react-schema-editor/src/utils/schema.ts similarity index 100% rename from packages/schema-editor/src/utils/schema.ts rename to packages/react-schema-editor/src/utils/schema.ts diff --git a/packages/schema-editor/src/utils/types.ts b/packages/react-schema-editor/src/utils/types.ts similarity index 100% rename from packages/schema-editor/src/utils/types.ts rename to packages/react-schema-editor/src/utils/types.ts From 1cc755ea073cac08a36392680927e2344e687f94 Mon Sep 17 00:00:00 2001 From: Janry Date: Fri, 25 Oct 2019 14:29:01 +0800 Subject: [PATCH 04/99] Fix CI and some Bugs (#348) --- docs/Examples/next/Relations.md | 2 +- packages/core/src/__tests__/index.spec.ts | 19 +++--- packages/core/src/index.ts | 60 ++++++++++++------- packages/core/src/shared/graph.ts | 19 +++++- packages/core/src/shared/model.ts | 5 +- packages/core/src/shared/subscrible.ts | 19 +++++- packages/core/src/state/field.ts | 4 +- packages/core/src/state/virtual-field.ts | 5 +- packages/core/src/types.ts | 10 ++++ .../__snapshots__/markup.spec.tsx.snap | 54 ++++------------- .../__snapshots__/register.spec.tsx.snap | 49 ++------------- .../src/__tests__/field.spec.tsx | 9 +++ .../src/__tests__/form.spec.tsx | 9 +++ .../src/__tests__/json-schema.spec.tsx | 9 +++ .../src/__tests__/markup.spec.tsx | 6 ++ .../src/__tests__/register.spec.tsx | 6 ++ .../src/components/SchemaField.tsx | 2 +- .../src/components/SchemaMarkup.tsx | 2 +- packages/react/src/__tests__/actions.spec.tsx | 9 +++ .../react/src/__tests__/consumer.spec.tsx | 9 +++ packages/react/src/__tests__/effects.spec.tsx | 9 +++ packages/react/src/__tests__/field.spec.tsx | 9 +++ packages/react/src/__tests__/form.spec.tsx | 6 ++ .../react/src/__tests__/provider.spec.tsx | 9 +++ packages/react/src/__tests__/spy.spec.tsx | 9 +++ .../react/src/__tests__/useDirty.spec.tsx | 9 +++ .../react/src/__tests__/useField.spec.tsx | 9 +++ .../src/__tests__/useForceUpdate.spec.tsx | 9 +++ packages/react/src/__tests__/useForm.spec.tsx | 9 +++ packages/react/src/__tests__/virtual.spec.tsx | 9 +++ packages/react/src/components/Field.tsx | 2 +- packages/react/src/hooks/useField.ts | 24 ++++---- packages/react/src/hooks/useVirtualField.ts | 27 +++++---- .../validator/src/__tests__/index.spec.ts | 6 +- 34 files changed, 296 insertions(+), 157 deletions(-) diff --git a/docs/Examples/next/Relations.md b/docs/Examples/next/Relations.md index 8703d9adf56..a97dbd0334a 100644 --- a/docs/Examples/next/Relations.md +++ b/docs/Examples/next/Relations.md @@ -52,7 +52,7 @@ const App = () => { { + effects={($, { setFieldState, getFieldState,getFormGraph }) => { $('onFormInit').subscribe(() => { setFieldState(FormPath.match('*(gg,hh)'), state => { state.props['x-props'] = state.props['x-props'] || {} diff --git a/packages/core/src/__tests__/index.spec.ts b/packages/core/src/__tests__/index.spec.ts index d8bb50152b0..ba17c10a8c2 100644 --- a/packages/core/src/__tests__/index.spec.ts +++ b/packages/core/src/__tests__/index.spec.ts @@ -781,9 +781,9 @@ describe('getFieldValue', () => { form.registerField({ path: 'a.b.c.d.e' }) expect(form.getFieldValue('a')).toEqual(deepValues.a) - expect(form.getFieldValue('a.b')).toEqual(deepValues.a) + expect(form.getFieldValue('a.b')).toEqual(undefined) expect(form.getFieldValue('a.b.c')).toEqual(deepValues.a.c) - expect(form.getFieldValue('a.b.c.d')).toEqual(deepValues.a.c) + expect(form.getFieldValue('a.b.c.d')).toEqual(undefined) expect(form.getFieldValue('a.b.c.d.e')).toEqual(deepValues.a.c.e) }) @@ -793,7 +793,7 @@ describe('getFieldValue', () => { form.registerField({ path: 'a.b' }) form.registerField({ path: 'a.b.c' }) - expect(form.getFieldValue('a')).toEqual(deepValues) + expect(form.getFieldValue('a')).toEqual(undefined) expect(form.getFieldValue('a.b')).toEqual(deepValues.b) expect(form.getFieldValue('a.b.c')).toEqual(deepValues.b.c) }) @@ -831,14 +831,12 @@ describe('registerField', () => { describe('registerVirtualField', () => { test('basic', async () => { - const onChange = jest.fn() const vprops = { hello: 'world' }; const form = createForm({ values: { a: 1 } }) form.registerVirtualField({ path: 'a' }) - form.registerVirtualField({ path: 'b', onChange }) - expect(onChange).toBeCalledTimes(1) // initialized + form.registerVirtualField({ path: 'b' }) form.registerVirtualField({ path: 'c', props: vprops }) - expect(form.getFieldValue('a')).toEqual({ a: 1 }) // 根据dataPath法则,会拿到根路径的value + expect(form.getFieldValue('a')).toEqual(undefined) expect(form.getFieldState('a', state => state.values)).toEqual(undefined) // 不存在这个属性 expect(form.getFieldState('c', state => state.props)).toEqual(vprops) expect(form.getFieldState('b', state => state.display)).toEqual(true) @@ -847,7 +845,6 @@ describe('registerVirtualField', () => { expect(form.getFieldState('b', state => state.display)).toEqual(false) form.setFieldState('b', state => state.visible = false) expect(form.getFieldState('b', state => state.visible)).toEqual(false) - expect(onChange).toBeCalledTimes(3) }) }) @@ -1012,9 +1009,9 @@ describe('transformDataPath', () => { form.registerField({ path: 'a.b.c.d.e' }) expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a')).toString()).toEqual('a') - expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b')).toString()).toEqual('a') + expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b')).toString()).toEqual('a.b') expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c')).toString()).toEqual('a.c') - expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d')).toString()).toEqual('a.c') + expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d')).toString()).toEqual('a.c.d') expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d.e')).toString()).toEqual('a.c.e') }) @@ -1024,7 +1021,7 @@ describe('transformDataPath', () => { form.registerField({ path: 'a.b' }) form.registerField({ path: 'a.b.c' }) - expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a')).toString()).toEqual('') + expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a')).toString()).toEqual('a') expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b')).toString()).toEqual('b') expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c')).toString()).toEqual('b.c') }) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index cd51939b6ae..9f7550a831c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -146,7 +146,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { } } - function onFieldChange({ onChange, field, path }) { + function onFieldChange({ field, path }) { return (published: IFieldState) => { const valueChanged = field.hasChanged('value') const initialValueChanged = field.hasChanged('initialValue') @@ -169,7 +169,6 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { }, true) } } - if (visibleChanged) { if (!published.visible) { deleteFormValuesIn(path, true) @@ -197,7 +196,6 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { false ) } - if (unmountedChanged) { if (published.unmounted) { deleteFormValuesIn(path, true) @@ -234,11 +232,10 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { if (warningsChanged) { syncFormMessages('warnings', published.name, published.warnings) } - - if (isFn(onChange) && (!env.shadowStage || env.leadingStage)) { - onChange(field) - } heart.notify(LifeCycleTypes.ON_FIELD_CHANGE, field) + if (!(!env.shadowStage || env.leadingStage)) { + return false + } } } @@ -271,7 +268,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { }) } - function onVirtualFieldChange({ onChange, field, path }) { + function onVirtualFieldChange({ field, path }) { return (published: IVirtualFieldState) => { const visibleChanged = field.hasChanged('visible') const displayChanged = field.hasChanged('display') @@ -309,7 +306,6 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { if (mountedChanged && published.mounted) { heart.notify(LifeCycleTypes.ON_FIELD_MOUNT, field) } - if (isFn(onChange)) onChange(field) heart.notify(LifeCycleTypes.ON_FIELD_CHANGE, field) } } @@ -317,18 +313,21 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { function registerVirtualField({ name, path, - props, - onChange + props }: IVirtualFieldStateProps): IVirtualField { let nodePath = FormPath.parse(path || name) + let dataPath = transformDataPath(nodePath) let field: IVirtualField const createField = () => { let field: IVirtualField field = new VirtualFieldState({ nodePath, + dataPath, useDirty: options.useDirty }) - field.subscribe(onVirtualFieldChange({ onChange, field, path: nodePath })) + field.subscription = { + notify: onVirtualFieldChange({ field, path: nodePath }) + } field.batch(() => { batchRunTaskQueue(field, nodePath) field.setState((state: IVirtualFieldState) => { @@ -359,12 +358,11 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { required, rules, editable, - onChange, props }: Exclude): IField { let field: IField let nodePath = FormPath.parse(path || name) - let dataPath = transformDataPath(path) + let dataPath = transformDataPath(nodePath) const createField = () => { let field: IField field = new FieldState({ @@ -372,8 +370,10 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { dataPath, useDirty: options.useDirty }) + field.subscription = { + notify: onFieldChange({ field, path: nodePath }) + } heart.notify(LifeCycleTypes.ON_FIELD_WILL_INIT, field) - field.subscribe(onFieldChange({ onChange, field, path: nodePath })) field.batch(() => { batchRunTaskQueue(field, nodePath) field.setState((state: IFieldState) => { @@ -436,6 +436,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { function transformDataPath(path: FormPathPattern) { const newPath = FormPath.getPath(path) return newPath.reduce((path: FormPath, key: string, index: number) => { + if (index >= newPath.length - 1) return path.concat(key) const realPath = newPath.slice(0, index + 1) const dataPath = path.concat(key) const selected = graph.get(realPath) @@ -779,7 +780,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { function batchRunTaskQueue(field: IField | IVirtualField, path: FormPath) { env.taskQueue.forEach((task, index) => { const { pattern, callbacks } = task - if (pattern.match(path)) { + if (matchStrategy(pattern, field)) { callbacks.forEach(callback => { field.setState(callback) }) @@ -810,6 +811,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { let taskIndex = env.taskIndexes[pattern.toString()] if (isValid(taskIndex)) { if ( + env.taskQueue[taskIndex] && !env.taskQueue[taskIndex].callbacks.some(fn => isEqual(fn, callback)) ) { env.taskQueue[taskIndex].callbacks.push(callback) @@ -831,7 +833,9 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { } function getFieldValue(path?: FormPathPattern) { - return getFormValuesIn(path) + return getFieldState(path, state => { + return state.value + }) } function setFieldInitialValue(path?: FormPathPattern, value?: any) { @@ -841,7 +845,9 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { } function getFieldInitialValue(path?: FormPathPattern) { - return getFormInitialValuesIn(path) + return getFieldState(path, state => { + return state.initialValue + }) } function getFieldState( @@ -905,9 +911,21 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { env.leadingStage = false } + function matchStrategy( + pattern: FormPathPattern, + node: IField | IVirtualField + ) { + const matchPattern = FormPath.parse(pattern) + return node.unsafe_getSourceState( + state => matchPattern.match(state.name) || matchPattern.match(state.path) + ) + } + const state = new FormState(options) const validator = new FormValidator(options) - const graph = new FormGraph() + const graph = new FormGraph({ + matchStrategy + }) const formApi = { submit, reset, @@ -964,7 +982,9 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { submittingTask: undefined } heart.notify(LifeCycleTypes.ON_FORM_WILL_INIT, state) - state.subscribe(onFormChange) + state.subscription = { + notify: onFormChange + } graph.appendNode('', state) state.setState((state: IFormState) => { state.initialized = true diff --git a/packages/core/src/shared/graph.ts b/packages/core/src/shared/graph.ts index 246bec5ad89..bff3405e0a6 100644 --- a/packages/core/src/shared/graph.ts +++ b/packages/core/src/shared/graph.ts @@ -11,6 +11,7 @@ import { FormGraphNodeRef, FormGraphMatcher, FormGraphEacher, + FormGraphProps } from '../types' export class FormGraph extends Subscrible<{ @@ -34,11 +35,14 @@ export class FormGraph extends Subscrible<{ } }[] - constructor() { + private matchStrategy: FormGraphProps['matchStrategy'] + + constructor(props: FormGraphProps = {}) { super() this.refrences = {} this.nodes = {} this.buffer = [] + this.matchStrategy = props.matchStrategy } /** @@ -56,7 +60,11 @@ export class FormGraph extends Subscrible<{ } for (let name in this.nodes) { const node = this.nodes[name] - if (pattern.match(name)) { + if ( + isFn(this.matchStrategy) + ? this.matchStrategy(pattern, node) + : pattern.match(name) + ) { if (isFn(matcher)) { const result = matcher(node, FormPath.parse(name)) if (result === false) { @@ -133,7 +141,12 @@ export class FormGraph extends Subscrible<{ return each(ref.children, path => { if (isFn(eacher)) { const node = this.get(path) - if (node && FormPath.parse(path).match(selector)) { + if ( + node && + (isFn(this.matchStrategy) + ? this.matchStrategy(selector, node) + : FormPath.parse(selector).match(path)) + ) { eacher(node, path) if (recursion) { this.eachChildren(path, selector, eacher, recursion) diff --git a/packages/core/src/shared/model.ts b/packages/core/src/shared/model.ts index 532fd5d8f90..48511f271a7 100644 --- a/packages/core/src/shared/model.ts +++ b/packages/core/src/shared/model.ts @@ -10,7 +10,10 @@ export const createStateModel = ( return class Model extends Subscrible implements IModel { public state: State & { displayName?: string } - public props: Props & DefaultProps & { useDirty?: boolean } + public props: Props & + DefaultProps & { + useDirty?: boolean + } public displayName?: string public dirtyNum: number public dirtyMap: StateDirtyMap diff --git a/packages/core/src/shared/subscrible.ts b/packages/core/src/shared/subscrible.ts index d1acdde036a..c8c49c8daef 100644 --- a/packages/core/src/shared/subscrible.ts +++ b/packages/core/src/shared/subscrible.ts @@ -1,9 +1,11 @@ import { isFn, each } from '@uform/shared' -import { Subscriber } from '../types' +import { Subscriber, Subscription } from '../types' export class Subscrible { subscribers: Subscriber[] = [] + subscription: Subscription + subscribe = (callback?: Subscriber) => { if ( isFn(callback) && @@ -24,6 +26,19 @@ export class Subscrible { } notify = (payload?: Payload) => { - each(this.subscribers, callback => callback(payload)) + if (this.subscription) { + if (this.subscription && isFn(this.subscription.notify)) { + if (this.subscription.notify.call(this, payload) === false) { + return + } + } + } + const filter = (payload: Payload) => { + if (this.subscription && isFn(this.subscription.filter)) { + return this.subscription.filter.call(this, payload) + } + return payload + } + each(this.subscribers, callback => callback(filter(payload))) } } diff --git a/packages/core/src/state/field.ts b/packages/core/src/state/field.ts index 2e4f73ccf64..4c60d4b7dbd 100644 --- a/packages/core/src/state/field.ts +++ b/packages/core/src/state/field.ts @@ -166,10 +166,10 @@ export const FieldState = createStateModel( draft.warnings = [] draft.effectWarnings = [] } - if (draft.mounted === true) { + if (draft.mounted === true && draft.mounted !== prevState.mounted) { draft.unmounted = false } - if (draft.unmounted === true) { + if (draft.unmounted === true && draft.unmounted !== prevState.unmounted) { draft.mounted = false } if (draft.errors.length) { diff --git a/packages/core/src/state/virtual-field.ts b/packages/core/src/state/virtual-field.ts index 0f7028bc52b..4940529d2b4 100644 --- a/packages/core/src/state/virtual-field.ts +++ b/packages/core/src/state/virtual-field.ts @@ -34,11 +34,14 @@ export const VirtualFieldState = createStateModel< private path: FormPath + private dataPath: FormPath + constructor(state: IVirtualFieldState, props: IVirtualFieldStateProps) { this.state = state this.path = FormPath.getPath(props.nodePath) - this.state.name = this.path.entire + this.dataPath = FormPath.getPath(props.dataPath) this.state.path = this.path.entire + this.state.name = this.dataPath.entire } computeState(draft: IVirtualFieldState, prevState: IVirtualFieldState) { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index c1d00804800..3927007b450 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -52,6 +52,10 @@ export enum LifeCycleTypes { ON_FIELD_UNMOUNT = 'onFieldUnmount' } +export interface FormGraphProps { + matchStrategy?: (pattern: FormPathPattern, field: any) => boolean +} + export type FormGraphNodeMap = { [key in string]: T } @@ -221,6 +225,7 @@ export type VirtualFieldStateDirtyMap = StateDirtyMap export interface IVirtualFieldStateProps { path?: FormPathPattern + dataPath?: FormPathPattern nodePath?: FormPathPattern name?: string props?: {} @@ -262,6 +267,11 @@ export interface IMutators { export type Subscriber = (payload: S) => void +export interface Subscription { + notify?: (payload: S) => void | boolean + filter?: (payload: S) => any +} + export interface IModel extends Subscrible { state: S props: P diff --git a/packages/react-schema-renderer/src/__tests__/__snapshots__/markup.spec.tsx.snap b/packages/react-schema-renderer/src/__tests__/__snapshots__/markup.spec.tsx.snap index 8f8529a73aa..ec96f5025c0 100644 --- a/packages/react-schema-renderer/src/__tests__/__snapshots__/markup.spec.tsx.snap +++ b/packages/react-schema-renderer/src/__tests__/__snapshots__/markup.spec.tsx.snap @@ -40,6 +40,7 @@ Object { "modified": true, "mounted": true, "name": "aa", + "path": "aa", "pristine": false, "props": Object { "default": "123", @@ -96,60 +97,23 @@ Object { "display": true, "displayName": "VirtualFieldState", "initialized": true, - "mounted": false, + "mounted": true, "name": "NO_NAME_FIELD_$0", + "path": "NO_NAME_FIELD_$0", "props": Object { "type": "object", "x-component": "card", - "x-props": Object { + "x-component-props": Object { "title": "this is card", }, - }, - "unmounted": false, - "visible": true, - }, - "NO_NAME_FIELD_$0.aa": Object { - "active": false, - "display": true, - "displayName": "FieldState", - "editable": true, - "effectErrors": Array [], - "effectWarnings": Array [], - "errors": Array [], - "formEditable": undefined, - "initialValue": "123", - "initialized": true, - "invalid": false, - "loading": false, - "modified": true, - "mounted": true, - "name": "NO_NAME_FIELD_$0.aa", - "pristine": false, - "props": Object { - "default": "123", - "type": "string", "x-props": Object { - "data-testid": "input", + "title": "this is card", }, }, - "required": false, - "ruleErrors": Array [], - "ruleWarnings": Array [], - "rules": Array [], - "selfEditable": undefined, - "touched": false, "unmounted": false, - "valid": true, - "validating": false, - "value": "aa change", - "values": Array [ - "aa change", - ], "visible": true, - "visited": false, - "warnings": Array [], }, - "aa": Object { + "NO_NAME_FIELD_$0.aa": Object { "active": false, "display": true, "displayName": "FieldState", @@ -164,7 +128,8 @@ Object { "loading": false, "modified": true, "mounted": true, - "name": "NO_NAME_FIELD_$0.aa", + "name": "aa", + "path": "NO_NAME_FIELD_$0.aa", "pristine": false, "props": Object { "default": "123", @@ -208,6 +173,9 @@ Object { }, "type": "object", "x-component": "card", + "x-component-props": Object { + "title": "this is card", + }, "x-props": Object { "title": "this is card", }, diff --git a/packages/react-schema-renderer/src/__tests__/__snapshots__/register.spec.tsx.snap b/packages/react-schema-renderer/src/__tests__/__snapshots__/register.spec.tsx.snap index 52f8a58efbc..4ee8f1fa33c 100644 --- a/packages/react-schema-renderer/src/__tests__/__snapshots__/register.spec.tsx.snap +++ b/packages/react-schema-renderer/src/__tests__/__snapshots__/register.spec.tsx.snap @@ -40,6 +40,7 @@ Object { "modified": true, "mounted": true, "name": "aa", + "path": "aa", "pristine": false, "props": Object { "default": "123", @@ -108,6 +109,7 @@ Object { "modified": true, "mounted": true, "name": "aa", + "path": "aa", "pristine": false, "props": Object { "default": "123", @@ -160,53 +162,13 @@ Object { }, "warnings": Array [], }, - "aa": Object { - "active": false, - "display": true, - "displayName": "FieldState", - "editable": true, - "effectErrors": Array [], - "effectWarnings": Array [], - "errors": Array [], - "formEditable": undefined, - "initialValue": "123", - "initialized": true, - "invalid": false, - "loading": false, - "modified": true, - "mounted": true, - "name": "cc.aa", - "pristine": false, - "props": Object { - "default": "123", - "type": "string", - "x-props": Object { - "data-testid": "input", - }, - }, - "required": false, - "ruleErrors": Array [], - "ruleWarnings": Array [], - "rules": Array [], - "selfEditable": undefined, - "touched": false, - "unmounted": false, - "valid": true, - "validating": false, - "value": "aa change", - "values": Array [ - "aa change", - ], - "visible": true, - "visited": false, - "warnings": Array [], - }, "cc": Object { "display": true, "displayName": "VirtualFieldState", "initialized": true, - "mounted": false, + "mounted": true, "name": "cc", + "path": "cc", "props": Object { "type": "object", "x-component": "box", @@ -229,7 +191,8 @@ Object { "loading": false, "modified": true, "mounted": true, - "name": "cc.aa", + "name": "aa", + "path": "cc.aa", "pristine": false, "props": Object { "default": "123", diff --git a/packages/react-schema-renderer/src/__tests__/field.spec.tsx b/packages/react-schema-renderer/src/__tests__/field.spec.tsx index 3764eab2358..cfbcecec706 100644 --- a/packages/react-schema-renderer/src/__tests__/field.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/field.spec.tsx @@ -1,12 +1,21 @@ describe('test all apis',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('major scenes',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('bugfix',()=>{ //todo + test('basic',()=>{ + //todo + }) }) \ No newline at end of file diff --git a/packages/react-schema-renderer/src/__tests__/form.spec.tsx b/packages/react-schema-renderer/src/__tests__/form.spec.tsx index 3764eab2358..cfbcecec706 100644 --- a/packages/react-schema-renderer/src/__tests__/form.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/form.spec.tsx @@ -1,12 +1,21 @@ describe('test all apis',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('major scenes',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('bugfix',()=>{ //todo + test('basic',()=>{ + //todo + }) }) \ No newline at end of file diff --git a/packages/react-schema-renderer/src/__tests__/json-schema.spec.tsx b/packages/react-schema-renderer/src/__tests__/json-schema.spec.tsx index 3764eab2358..cfbcecec706 100644 --- a/packages/react-schema-renderer/src/__tests__/json-schema.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/json-schema.spec.tsx @@ -1,12 +1,21 @@ describe('test all apis',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('major scenes',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('bugfix',()=>{ //todo + test('basic',()=>{ + //todo + }) }) \ No newline at end of file diff --git a/packages/react-schema-renderer/src/__tests__/markup.spec.tsx b/packages/react-schema-renderer/src/__tests__/markup.spec.tsx index 9013675dd70..3a2ab866730 100644 --- a/packages/react-schema-renderer/src/__tests__/markup.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/markup.spec.tsx @@ -98,8 +98,14 @@ describe('test all apis', () => { describe('major scenes',()=>{ //todo,核心场景回归 + test('basic',()=>{ + //todo + }) }) describe('bugfix',()=>{ //todo,问题修复回归 + test('basic',()=>{ + //todo + }) }) \ No newline at end of file diff --git a/packages/react-schema-renderer/src/__tests__/register.spec.tsx b/packages/react-schema-renderer/src/__tests__/register.spec.tsx index a72934ce55a..f2705867c7d 100644 --- a/packages/react-schema-renderer/src/__tests__/register.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/register.spec.tsx @@ -158,9 +158,15 @@ describe('test all apis', () => { describe('major scenes',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('bugfix',()=>{ //todo + test('basic',()=>{ + //todo + }) }) \ No newline at end of file diff --git a/packages/react-schema-renderer/src/components/SchemaField.tsx b/packages/react-schema-renderer/src/components/SchemaField.tsx index ae1b6110bde..999c7e1d0fe 100644 --- a/packages/react-schema-renderer/src/components/SchemaField.tsx +++ b/packages/react-schema-renderer/src/components/SchemaField.tsx @@ -115,7 +115,7 @@ export const SchemaField: React.FunctionComponent = ( const childPath = path.concat(key) return ( ) diff --git a/packages/react-schema-renderer/src/components/SchemaMarkup.tsx b/packages/react-schema-renderer/src/components/SchemaMarkup.tsx index 684988bb099..a167d68db03 100644 --- a/packages/react-schema-renderer/src/components/SchemaMarkup.tsx +++ b/packages/react-schema-renderer/src/components/SchemaMarkup.tsx @@ -71,7 +71,7 @@ SchemaMarkupForm.displayName = 'SchemaMarkupForm' export function createVirtualBox( key: string, - component?: React.JSXElementConstructor + component?: React.JSXElementConstructor> ) { registerVirtualBox( key, diff --git a/packages/react/src/__tests__/actions.spec.tsx b/packages/react/src/__tests__/actions.spec.tsx index 3764eab2358..cfbcecec706 100644 --- a/packages/react/src/__tests__/actions.spec.tsx +++ b/packages/react/src/__tests__/actions.spec.tsx @@ -1,12 +1,21 @@ describe('test all apis',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('major scenes',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('bugfix',()=>{ //todo + test('basic',()=>{ + //todo + }) }) \ No newline at end of file diff --git a/packages/react/src/__tests__/consumer.spec.tsx b/packages/react/src/__tests__/consumer.spec.tsx index 3764eab2358..cfbcecec706 100644 --- a/packages/react/src/__tests__/consumer.spec.tsx +++ b/packages/react/src/__tests__/consumer.spec.tsx @@ -1,12 +1,21 @@ describe('test all apis',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('major scenes',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('bugfix',()=>{ //todo + test('basic',()=>{ + //todo + }) }) \ No newline at end of file diff --git a/packages/react/src/__tests__/effects.spec.tsx b/packages/react/src/__tests__/effects.spec.tsx index 3764eab2358..cfbcecec706 100644 --- a/packages/react/src/__tests__/effects.spec.tsx +++ b/packages/react/src/__tests__/effects.spec.tsx @@ -1,12 +1,21 @@ describe('test all apis',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('major scenes',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('bugfix',()=>{ //todo + test('basic',()=>{ + //todo + }) }) \ No newline at end of file diff --git a/packages/react/src/__tests__/field.spec.tsx b/packages/react/src/__tests__/field.spec.tsx index 3764eab2358..cfbcecec706 100644 --- a/packages/react/src/__tests__/field.spec.tsx +++ b/packages/react/src/__tests__/field.spec.tsx @@ -1,12 +1,21 @@ describe('test all apis',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('major scenes',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('bugfix',()=>{ //todo + test('basic',()=>{ + //todo + }) }) \ No newline at end of file diff --git a/packages/react/src/__tests__/form.spec.tsx b/packages/react/src/__tests__/form.spec.tsx index 041f97923ce..1e936e8a3d9 100644 --- a/packages/react/src/__tests__/form.spec.tsx +++ b/packages/react/src/__tests__/form.spec.tsx @@ -64,8 +64,14 @@ describe('test all apis', () => { describe('major scenes', () => { //todo + test('basic',()=>{ + //todo + }) }) describe('bugfix', () => { //todo + test('basic',()=>{ + //todo + }) }) diff --git a/packages/react/src/__tests__/provider.spec.tsx b/packages/react/src/__tests__/provider.spec.tsx index 3764eab2358..cfbcecec706 100644 --- a/packages/react/src/__tests__/provider.spec.tsx +++ b/packages/react/src/__tests__/provider.spec.tsx @@ -1,12 +1,21 @@ describe('test all apis',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('major scenes',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('bugfix',()=>{ //todo + test('basic',()=>{ + //todo + }) }) \ No newline at end of file diff --git a/packages/react/src/__tests__/spy.spec.tsx b/packages/react/src/__tests__/spy.spec.tsx index 3764eab2358..cfbcecec706 100644 --- a/packages/react/src/__tests__/spy.spec.tsx +++ b/packages/react/src/__tests__/spy.spec.tsx @@ -1,12 +1,21 @@ describe('test all apis',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('major scenes',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('bugfix',()=>{ //todo + test('basic',()=>{ + //todo + }) }) \ No newline at end of file diff --git a/packages/react/src/__tests__/useDirty.spec.tsx b/packages/react/src/__tests__/useDirty.spec.tsx index 3764eab2358..cfbcecec706 100644 --- a/packages/react/src/__tests__/useDirty.spec.tsx +++ b/packages/react/src/__tests__/useDirty.spec.tsx @@ -1,12 +1,21 @@ describe('test all apis',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('major scenes',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('bugfix',()=>{ //todo + test('basic',()=>{ + //todo + }) }) \ No newline at end of file diff --git a/packages/react/src/__tests__/useField.spec.tsx b/packages/react/src/__tests__/useField.spec.tsx index 3764eab2358..cfbcecec706 100644 --- a/packages/react/src/__tests__/useField.spec.tsx +++ b/packages/react/src/__tests__/useField.spec.tsx @@ -1,12 +1,21 @@ describe('test all apis',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('major scenes',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('bugfix',()=>{ //todo + test('basic',()=>{ + //todo + }) }) \ No newline at end of file diff --git a/packages/react/src/__tests__/useForceUpdate.spec.tsx b/packages/react/src/__tests__/useForceUpdate.spec.tsx index 3764eab2358..cfbcecec706 100644 --- a/packages/react/src/__tests__/useForceUpdate.spec.tsx +++ b/packages/react/src/__tests__/useForceUpdate.spec.tsx @@ -1,12 +1,21 @@ describe('test all apis',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('major scenes',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('bugfix',()=>{ //todo + test('basic',()=>{ + //todo + }) }) \ No newline at end of file diff --git a/packages/react/src/__tests__/useForm.spec.tsx b/packages/react/src/__tests__/useForm.spec.tsx index 3764eab2358..cfbcecec706 100644 --- a/packages/react/src/__tests__/useForm.spec.tsx +++ b/packages/react/src/__tests__/useForm.spec.tsx @@ -1,12 +1,21 @@ describe('test all apis',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('major scenes',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('bugfix',()=>{ //todo + test('basic',()=>{ + //todo + }) }) \ No newline at end of file diff --git a/packages/react/src/__tests__/virtual.spec.tsx b/packages/react/src/__tests__/virtual.spec.tsx index 3764eab2358..cfbcecec706 100644 --- a/packages/react/src/__tests__/virtual.spec.tsx +++ b/packages/react/src/__tests__/virtual.spec.tsx @@ -1,12 +1,21 @@ describe('test all apis',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('major scenes',()=>{ //todo + test('basic',()=>{ + //todo + }) }) describe('bugfix',()=>{ //todo + test('basic',()=>{ + //todo + }) }) \ No newline at end of file diff --git a/packages/react/src/components/Field.tsx b/packages/react/src/components/Field.tsx index 1011f64b6cb..872c65ab120 100644 --- a/packages/react/src/components/Field.tsx +++ b/packages/react/src/components/Field.tsx @@ -30,7 +30,7 @@ const createFieldMutators = ( } } -export const Field: React.FunctionComponent = props => { +export const Field: React.FC = props => { const { state, props: innerProps, mutators, form } = useField(props) if (!state.visible || !state.display) return if (isFn(props.children)) { diff --git a/packages/react/src/hooks/useField.ts b/packages/react/src/hooks/useField.ts index 4d6b653c259..604d3d46579 100644 --- a/packages/react/src/hooks/useField.ts +++ b/packages/react/src/hooks/useField.ts @@ -20,18 +20,16 @@ export const useField = (options: IFieldStateProps): IFieldHook => { } useMemo(() => { let initialized = false - ref.current.field = form.registerField({ - ...options, - onChange() { - if (ref.current.unmounted) return - /** - * 同步Field状态只需要forceUpdate一下触发重新渲染,因为字段状态全部代理在uform core内部 - */ - if (initialized) { - raf(() => { - forceUpdate() - }) - } + ref.current.field = form.registerField(options) + ref.current.field.subscribe(() => { + /** + * 同步Field状态只需要forceUpdate一下触发重新渲染,因为字段状态全部代理在uform core内部 + */ + if (initialized) { + raf(() => { + if (ref.current.unmounted) return + forceUpdate() + }) } }) initialized = true @@ -53,8 +51,10 @@ export const useField = (options: IFieldStateProps): IFieldHook => { ref.current.field.unsafe_setSourceState(state => { state.mounted = true }) + ref.current.unmounted = false return () => { ref.current.unmounted = true + ref.current.field.unsubscribe() ref.current.field.setState((state: IFieldState) => { state.unmounted = true }) diff --git a/packages/react/src/hooks/useVirtualField.ts b/packages/react/src/hooks/useVirtualField.ts index 3f5f21645dc..47f3806e441 100644 --- a/packages/react/src/hooks/useVirtualField.ts +++ b/packages/react/src/hooks/useVirtualField.ts @@ -22,18 +22,16 @@ export const useVirtualField = ( } useMemo(() => { let initialized = false - ref.current.field = form.registerVirtualField({ - ...options, - onChange() { - if (ref.current.unmounted) return - /** - * 同步Field状态只需要forceUpdate一下触发重新渲染,因为字段状态全部代理在uform core内部 - */ - if (initialized) { - raf(() => { - forceUpdate() - }) - } + ref.current.field = form.registerVirtualField(options) + ref.current.field.subscribe(() => { + if (ref.current.unmounted) return + /** + * 同步Field状态只需要forceUpdate一下触发重新渲染,因为字段状态全部代理在uform core内部 + */ + if (initialized) { + raf(() => { + forceUpdate() + }) } }) initialized = true @@ -52,8 +50,13 @@ export const useVirtualField = ( }) useEffect(() => { + ref.current.field.unsafe_setSourceState(state => { + state.mounted = true + }) + ref.current.unmounted = false return () => { ref.current.unmounted = true + ref.current.field.unsubscribe() ref.current.field.setState((state: IVirtualFieldState) => { state.unmounted = true }) diff --git a/packages/validator/src/__tests__/index.spec.ts b/packages/validator/src/__tests__/index.spec.ts index cf541d6dc44..fce52e1fd8b 100644 --- a/packages/validator/src/__tests__/index.spec.ts +++ b/packages/validator/src/__tests__/index.spec.ts @@ -51,7 +51,7 @@ test('required', async () => { required: true, message: '该字段不能为空' }, - errors: ['该字段不能为空'] + errors: ['This field is required'] }, { value: [], @@ -59,7 +59,7 @@ test('required', async () => { required: true, message: '该字段不能为空' }, - errors: ['该字段不能为空'] + errors: ['This field is required'] }, { value: false, @@ -122,7 +122,7 @@ test('max', async () => { rules: { max: 2 }, - errors: ['The length of 123 must be at most 2'] + errors: ['The length or number of entries must be at most 2'] }, { value: '123', From 209a4da200e0d896c7e4a98df90042a84020d38e Mon Sep 17 00:00:00 2001 From: Janry Date: Fri, 25 Oct 2019 17:54:10 +0800 Subject: [PATCH 05/99] fix(@uform/react-shared-components): fix text (#349) * fix(@uform/react-shared-components): fix text * fix(@uform/antd/next): fix file case * fix(@uform/antd/next): fix file case * fix(@uform/core): fix field's dirty check logic * fix(CI): fix test case errors --- packages/antd/src/components/Button.tsx | 87 ++++++++++ .../antd/src/components/FormButtonGroup.tsx | 155 +++++++++++++++++ packages/core/src/index.ts | 58 +++---- packages/next/src/components/Button.tsx | 102 ++++++++++++ .../next/src/components/FormButtonGroup.tsx | 157 ++++++++++++++++++ .../src/PreviewText.tsx | 4 +- packages/react/src/hooks/useField.ts | 4 +- 7 files changed, 535 insertions(+), 32 deletions(-) create mode 100644 packages/antd/src/components/Button.tsx create mode 100644 packages/antd/src/components/FormButtonGroup.tsx create mode 100644 packages/next/src/components/Button.tsx create mode 100644 packages/next/src/components/FormButtonGroup.tsx diff --git a/packages/antd/src/components/Button.tsx b/packages/antd/src/components/Button.tsx new file mode 100644 index 00000000000..5d07468e830 --- /dev/null +++ b/packages/antd/src/components/Button.tsx @@ -0,0 +1,87 @@ +import React from 'react' +import { FormSpy, LifeCycleTypes } from '@uform/react-schema-renderer' +import { Button } from 'antd' +import { ButtonProps } from 'antd/lib/button' +import { ISubmitProps, IResetProps } from '../types' + +export const TextButton: React.FC = props => ( + + ) + }} + + ) +} + +Submit.defaultProps = { + showLoading: true +} + +export const Reset: React.FC = ({ + children, + forceClear, + validate, + ...props +}) => { + return ( + + {({ form }) => { + return ( + + ) + }} + + ) +} diff --git a/packages/antd/src/components/FormButtonGroup.tsx b/packages/antd/src/components/FormButtonGroup.tsx new file mode 100644 index 00000000000..7899c572d23 --- /dev/null +++ b/packages/antd/src/components/FormButtonGroup.tsx @@ -0,0 +1,155 @@ +import React, { useRef } from 'react' +import { Row, Col } from 'antd' +import Sticky from 'react-stikky' +import cls from 'classnames' +import styled from 'styled-components' +import { useFormItem } from '../compat/context' +import { IFormButtonGroupProps } from '../types' + +export interface IOffset { + top: number | string + right: number | string + bottom: number | string + left: number | string +} + +const getAlign = align => { + if (align === 'start' || align === 'end') return align + if (align === 'left' || align === 'top') return 'flex-start' + if (align === 'right' || align === 'bottom') return 'flex-end' + return align +} + +const isElementInViewport = ( + rect: ClientRect, + { + offset = 0, + threshold = 0 + }: { + offset?: IOffset | number + threshold?: number + } = {} +): boolean => { + const { top, right, bottom, left, width, height } = rect + const intersection = { + t: bottom, + r: window.innerWidth - left, + b: window.innerHeight - top, + l: right + } + + const elementThreshold = { + x: threshold * width, + y: threshold * height + } + + return ( + intersection.t >= + ((offset as IOffset).top || (offset as number) + elementThreshold.y) && + intersection.r >= + ((offset as IOffset).right || (offset as number) + elementThreshold.x) && + intersection.b >= + ((offset as IOffset).bottom || (offset as number) + elementThreshold.y) && + intersection.l >= + ((offset as IOffset).left || (offset as number) + elementThreshold.x) + ) +} + +export const FormButtonGroup = styled( + (props: React.PropsWithChildren) => { + const { + span, + zIndex, + sticky, + style, + offset, + className, + children, + triggerDistance, + itemStyle + } = props + const { inline } = useFormItem() + const selfRef = useRef() + const renderChildren = () => { + return ( +
    + + + +
    + {children} +
    + + +
    +
    + ) + } + const getStickyBoundaryHandler = () => { + return () => { + if (selfRef.current && selfRef.current.parentElement) { + const container = selfRef.current.parentElement + return isElementInViewport(container.getBoundingClientRect()) + } + return true + } + } + + const content = ( +
    + {renderChildren()} +
    + ) + + if (sticky) { + return ( +
    + +
    + {content} +
    +
    +
    + ) + } + + return content + } +)` + ${(props: IFormButtonGroupProps) => + props.align ? `display:flex;justify-content: ${getAlign(props.align)}` : ''} + &.is-inline { + display: inline-block; + flex-grow: 3; + } + .button-group { + .inline { + display: inline-block; + .inline-view { + & > * { + margin-right: 10px; + margin-left: 0px; + display: inline-block; + } + & > *:last-child { + margin-right: 0 !important; + } + } + } + } +` diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9f7550a831c..c6256531478 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -239,35 +239,6 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { } } - //实时同步Form Messages - function syncFormMessages(type: string, path: string, messages: string[]) { - state.unsafe_setSourceState(state => { - let foundField = false - state[type] = state[type] || [] - state[type] = state[type].reduce((buf: any, item: any) => { - if (item.path === path) { - foundField = true - return messages.length ? buf.concat({ path, messages }) : buf - } else { - return buf.concat(item) - } - }, []) - if (!foundField && messages.length) { - state[type].push({ - path, - messages - }) - } - if (state.errors.length) { - state.invalid = true - state.valid = false - } else { - state.invalid = false - state.valid = true - } - }) - } - function onVirtualFieldChange({ field, path }) { return (published: IVirtualFieldState) => { const visibleChanged = field.hasChanged('visible') @@ -433,6 +404,35 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { return field } + //实时同步Form Messages + function syncFormMessages(type: string, path: string, messages: string[]) { + state.unsafe_setSourceState(state => { + let foundField = false + state[type] = state[type] || [] + state[type] = state[type].reduce((buf: any, item: any) => { + if (item.path === path) { + foundField = true + return messages.length ? buf.concat({ path, messages }) : buf + } else { + return buf.concat(item) + } + }, []) + if (!foundField && messages.length) { + state[type].push({ + path, + messages + }) + } + if (state.errors.length) { + state.invalid = true + state.valid = false + } else { + state.invalid = false + state.valid = true + } + }) + } + function transformDataPath(path: FormPathPattern) { const newPath = FormPath.getPath(path) return newPath.reduce((path: FormPath, key: string, index: number) => { diff --git a/packages/next/src/components/Button.tsx b/packages/next/src/components/Button.tsx new file mode 100644 index 00000000000..9a3b67750b4 --- /dev/null +++ b/packages/next/src/components/Button.tsx @@ -0,0 +1,102 @@ +import React from 'react' +import { FormSpy, LifeCycleTypes } from '@uform/react-schema-renderer' +import { Button } from '@alifd/next' +import { ButtonProps } from '@alifd/next/types/button' +import { ISubmitProps, IResetProps } from '../types' +import styled from 'styled-components' + +export const TextButton: React.FC = props => ( + + ) +})` + border-radius: 50% !important; + padding: 0 !important; + min-width: 28px; + &.next-large { + min-width: 40px; + } + &.next-small { + min-width: 20px; + } + &.has-text { + .next-icon { + margin-right: 5px; + } + background: none !important; + border: none !important; + } +` + +export const Submit = ({ showLoading, onSubmit, ...props }: ISubmitProps) => { + return ( + { + switch (action.type) { + case LifeCycleTypes.ON_FORM_SUBMIT_START: + return { + ...state, + submitting: true + } + case LifeCycleTypes.ON_FORM_SUBMIT_END: + return { + ...state, + submitting: false + } + default: + return state + } + }} + > + {({ state, form }) => { + return ( + + ) + }} + + ) +} + +Submit.defaultProps = { + showLoading: true +} + +export const Reset: React.FC = ({ + children, + forceClear, + validate, + ...props +}) => { + return ( + + {({ form }) => { + return ( + + ) + }} + + ) +} diff --git a/packages/next/src/components/FormButtonGroup.tsx b/packages/next/src/components/FormButtonGroup.tsx new file mode 100644 index 00000000000..2faea71c2a4 --- /dev/null +++ b/packages/next/src/components/FormButtonGroup.tsx @@ -0,0 +1,157 @@ +import React, { useRef } from 'react' +import { Grid } from '@alifd/next' +import Sticky from 'react-stikky' +import cls from 'classnames' +import styled from 'styled-components' +import { useFormItem } from '../compat/context' +import { IFormButtonGroupProps } from '../types' + +const { Row, Col } = Grid + +export interface IOffset { + top: number | string + right: number | string + bottom: number | string + left: number | string +} + +const getAlign = align => { + if (align === 'start' || align === 'end') return align + if (align === 'left' || align === 'top') return 'flex-start' + if (align === 'right' || align === 'bottom') return 'flex-end' + return align +} + +const isElementInViewport = ( + rect: ClientRect, + { + offset = 0, + threshold = 0 + }: { + offset?: IOffset | number + threshold?: number + } = {} +): boolean => { + const { top, right, bottom, left, width, height } = rect + const intersection = { + t: bottom, + r: window.innerWidth - left, + b: window.innerHeight - top, + l: right + } + + const elementThreshold = { + x: threshold * width, + y: threshold * height + } + + return ( + intersection.t >= + ((offset as IOffset).top || (offset as number) + elementThreshold.y) && + intersection.r >= + ((offset as IOffset).right || (offset as number) + elementThreshold.x) && + intersection.b >= + ((offset as IOffset).bottom || (offset as number) + elementThreshold.y) && + intersection.l >= + ((offset as IOffset).left || (offset as number) + elementThreshold.x) + ) +} + +export const FormButtonGroup = styled( + (props: React.PropsWithChildren) => { + const { + span, + zIndex, + sticky, + style, + offset, + className, + children, + triggerDistance, + itemStyle + } = props + const { inline } = useFormItem() + const selfRef = useRef() + const renderChildren = () => { + return ( +
    + + + +
    + {children} +
    + + +
    +
    + ) + } + const getStickyBoundaryHandler = () => { + return () => { + if (selfRef.current && selfRef.current.parentElement) { + const container = selfRef.current.parentElement + return isElementInViewport(container.getBoundingClientRect()) + } + return true + } + } + + const content = ( +
    + {renderChildren()} +
    + ) + + if (sticky) { + return ( +
    + +
    + {content} +
    +
    +
    + ) + } + + return content + } +)` + ${(props: IFormButtonGroupProps) => + props.align ? `display:flex;justify-content: ${getAlign(props.align)}` : ''} + &.is-inline { + display: inline-block; + flex-grow: 3; + } + .button-group { + .inline { + display: inline-block; + .inline-view { + & > * { + margin-right: 10px; + margin-left: 0px; + display: inline-block; + } + & > *:last-child { + margin-right: 0 !important; + } + } + } + } +` diff --git a/packages/react-shared-components/src/PreviewText.tsx b/packages/react-shared-components/src/PreviewText.tsx index b2ddca4e642..cfc570ea5af 100644 --- a/packages/react-shared-components/src/PreviewText.tsx +++ b/packages/react-shared-components/src/PreviewText.tsx @@ -9,7 +9,9 @@ export const PreviewText: React.FC = props => { ? props.value.some(val => val == value) : props.value == value ) - value = find.map(item => item.label).join(' , ') + value = find.reduce((buf, item, index) => { + return buf.concat(item.label, index < find.length - 1 ? ', ' : '') + }, []) } else { value = Array.isArray(props.value) ? props.value.join(' ~ ') diff --git a/packages/react/src/hooks/useField.ts b/packages/react/src/hooks/useField.ts index 604d3d46579..12df9431f60 100644 --- a/packages/react/src/hooks/useField.ts +++ b/packages/react/src/hooks/useField.ts @@ -48,9 +48,9 @@ export const useField = (options: IFieldStateProps): IFieldHook => { }) useEffect(() => { - ref.current.field.unsafe_setSourceState(state => { + ref.current.field.setState(state => { state.mounted = true - }) + }, true) ref.current.unmounted = false return () => { ref.current.unmounted = true From c0922b26e9349848ca630008df78a243ef8c464b Mon Sep 17 00:00:00 2001 From: Janry Date: Mon, 28 Oct 2019 17:18:46 +0800 Subject: [PATCH 06/99] [UMBRELLA V1] Compat v0 (#351) --- docs/Examples/antd/Deconstruction.md | 2 +- docs/Examples/antd/Relations.md | 20 +- docs/Examples/antd/Validation.md | 2 +- docs/Examples/next/Deconstruction.md | 8 +- docs/Examples/next/Relations.md | 286 +++++---- docs/Examples/next/Validation.md | 2 +- packages/antd/src/fields/cards.tsx | 10 +- packages/antd/src/shared.ts | 2 +- .../__snapshots__/index.spec.ts.snap | 34 +- packages/core/src/__tests__/index.spec.ts | 600 ++++++++++++------ packages/core/src/index.ts | 24 +- packages/core/src/shared/model.ts | 26 +- packages/core/src/state/field.ts | 11 +- packages/next/src/fields/password.tsx | 4 +- packages/next/src/shared.ts | 2 +- .../src/shared/connect.ts | 26 +- packages/react/src/hooks/useField.ts | 14 +- 17 files changed, 650 insertions(+), 423 deletions(-) diff --git a/docs/Examples/antd/Deconstruction.md b/docs/Examples/antd/Deconstruction.md index 2e99cb3a2b7..1d55549379f 100644 --- a/docs/Examples/antd/Deconstruction.md +++ b/docs/Examples/antd/Deconstruction.md @@ -224,7 +224,7 @@ const App = () => ( { - $('onFieldChange', 'wrapper.relation').subscribe(({ value }) => { + $('onFieldValueChange', 'wrapper.relation').subscribe(({ value }) => { setFieldState( FormPath.match( 'wrapper.[[{aa:{bb:{cc:destructor1,dd:\\[destructor2,destructor3\\],ee}}}]]' diff --git a/docs/Examples/antd/Relations.md b/docs/Examples/antd/Relations.md index 976736ef95d..286c2104eb7 100644 --- a/docs/Examples/antd/Relations.md +++ b/docs/Examples/antd/Relations.md @@ -64,14 +64,14 @@ const App = () => { } }) }) - $('onFieldChange', 'aa').subscribe(fieldState => { + $('onFieldValueChange', 'aa').subscribe(fieldState => { console.log(fieldState.value) setFieldState('bb', state => { state.visible = !fieldState.value }) }) - $('onFieldChange', 'cc').subscribe(fieldState => { + $('onFieldValueChange', 'cc').subscribe(fieldState => { setFieldState('dd', state => { state.visible = !fieldState.value }) @@ -89,12 +89,12 @@ const App = () => { } }) }) - $('onFieldChange', 'mm').subscribe(fieldState => { + $('onFieldValueChange', 'mm').subscribe(fieldState => { setFieldState('ff', state => { state.visible = !fieldState.value }) }) - $('onFieldChange', 'gg') + $('onFieldValueChange', 'gg') .pipe( withLatestFrom($('onChangeOption')), map(([fieldState, { payload: option }]) => { @@ -267,7 +267,7 @@ const App = () => ( $('onFormInit').subscribe(() => { hide('bb') }) - $('onFieldChange', 'aa').subscribe(fieldState => { + $('onFieldValueChange', 'aa').subscribe(fieldState => { if (!fieldState.value) return show('bb') loading('bb') @@ -277,7 +277,7 @@ const App = () => ( setValue('bb', '1111') }, 1000) }) - $('onFieldChange', 'bb').subscribe(fieldState => { + $('onFieldValueChange', 'bb').subscribe(fieldState => { console.log(fieldState.loading) if (!fieldState.value) return hide('cc') show('cc') @@ -340,7 +340,7 @@ const App = () => ( { - $('onFieldChange', 'bb').subscribe(state => { + $('onFieldValueChange', 'bb').subscribe(state => { if (state.value) { setFieldState('aa', state => { state.value = '123' @@ -452,7 +452,7 @@ const App = () => { $('onFormInit').subscribe(() => { hide(FormPath.match('aa.*.*(cc,gg,dd.*.ee)')) }) - $('onFieldChange', 'aa.*.bb').subscribe(fieldState => { + $('onFieldValueChange', 'aa.*.bb').subscribe(fieldState => { const cc = FormPath.transform( fieldState.name, /\d+/, @@ -470,7 +470,7 @@ const App = () => { setValue(cc, '1111') }, 1000) }) - $('onFieldChange', 'aa.*.dd.*.ee').subscribe(fieldState => { + $('onFieldValueChange', 'aa.*.dd.*.ee').subscribe(fieldState => { const gg = FormPath.transform( fieldState.name, /\d+/, @@ -482,7 +482,7 @@ const App = () => { } }) }) - $('onFieldChange', 'aa.*.dd.*.ff').subscribe(fieldState => { + $('onFieldValueChange', 'aa.*.dd.*.ff').subscribe(fieldState => { const ee = FormPath.transform( fieldState.name, /\d+/, diff --git a/docs/Examples/antd/Validation.md b/docs/Examples/antd/Validation.md index b25028bdb73..b4e84dccff4 100644 --- a/docs/Examples/antd/Validation.md +++ b/docs/Examples/antd/Validation.md @@ -158,7 +158,7 @@ const App = () => ( labelCol={6} wrapperCol={6} effects={($, { setFieldState, getFieldState }) => { - $('onFieldChange', '*(password,confirm)').subscribe(fieldState => { + $('onFieldValueChange', '*(password,confirm)').subscribe(fieldState => { const selfName = fieldState.name const selfValue = fieldState.value const otherName = selfName == 'password' ? 'confirm' : 'password' diff --git a/docs/Examples/next/Deconstruction.md b/docs/Examples/next/Deconstruction.md index 1fce3ec1bd2..b3a63e65446 100644 --- a/docs/Examples/next/Deconstruction.md +++ b/docs/Examples/next/Deconstruction.md @@ -172,7 +172,7 @@ ReactDOM.render(, document.getElementById('root')) ### 使用 FormPath 路径匹配处理联动 -> 注意使用`[[]]`来包裹带关键字的 name 才能匹配 +> ~~注意使用`[[]]`来包裹带关键字的 name 才能匹配~~ v1.x不再需要包裹 ```jsx import React from 'react' @@ -224,11 +224,9 @@ const App = () => ( { - $('onFieldChange', 'wrapper.relation').subscribe(({ value }) => { + $('onFieldValueChange', 'wrapper.relation').subscribe(({ value }) => { setFieldState( - FormPath.match( - 'wrapper.[[{aa:{bb:{cc:destructor1,dd:\\[destructor2,destructor3\\],ee}}}]]' - ), + 'wrapper.{aa:{bb:{cc:destructor1,dd:[ destructor2, destructor3 ],ee}}}', state => { state.visible = value == 2 } diff --git a/docs/Examples/next/Relations.md b/docs/Examples/next/Relations.md index a97dbd0334a..3e04127cdbc 100644 --- a/docs/Examples/next/Relations.md +++ b/docs/Examples/next/Relations.md @@ -41,7 +41,7 @@ import { FormBlock, FormLayout } from '@uform/next' -import { filter, withLatestFrom, map, debounceTime } from 'rxjs/operators' +import { filter, combineLatest, map, debounceTime } from 'rxjs/operators' import { Button } from '@alifd/next' import Printer from '@uform/printer' import '@alifd/next/dist/next.css' @@ -51,8 +51,7 @@ const App = () => { return ( { + effects={($, { setFieldState, getFieldState, getFormGraph }) => { $('onFormInit').subscribe(() => { setFieldState(FormPath.match('*(gg,hh)'), state => { state.props['x-props'] = state.props['x-props'] || {} @@ -64,12 +63,12 @@ const App = () => { } }) }) - $('onFieldChange', 'aa').subscribe(fieldState => { + $('onFieldValueChange', 'aa').subscribe(fieldState => { setFieldState('bb', state => { state.visible = !fieldState.value }) }) - $('onFieldChange', 'cc').subscribe(fieldState => { + $('onFieldValueChange', 'cc').subscribe(fieldState => { setFieldState('dd', state => { state.visible = !fieldState.value }) @@ -87,9 +86,9 @@ const App = () => { } }) }) - $('onFieldChange', 'gg') + $('onFieldValueChange', 'gg') .pipe( - withLatestFrom($('onChangeOption')), + combineLatest($('onChangeOption')), map(([fieldState, { payload: option }]) => { return { state: fieldState, @@ -160,7 +159,7 @@ const App = () => { name="gg" type="string" x-effect={dispatch => ({ - onChange(value,type, option) { + onChange(value, type, option) { dispatch('onChangeOption', option) }, onSearch(value) { @@ -251,7 +250,7 @@ const App = () => ( $('onFormInit').subscribe(() => { hide('bb') }) - $('onFieldChange', 'aa').subscribe(fieldState => { + $('onFieldValueChange', 'aa').subscribe(fieldState => { if (!fieldState.value) return show('bb') loading('bb') @@ -261,7 +260,7 @@ const App = () => ( setValue('bb', '1111') }, 1000) }) - $('onFieldChange', 'bb').subscribe(fieldState => { + $('onFieldValueChange', 'bb').subscribe(fieldState => { console.log(fieldState.loading) if (!fieldState.value) return hide('cc') show('cc') @@ -323,7 +322,7 @@ const App = () => ( { - $('onFieldChange', 'bb').subscribe(state => { + $('onFieldValueChange', 'bb').subscribe(state => { if (state.value) { setFieldState('aa', state => { state.value = '123' @@ -359,7 +358,7 @@ ReactDOM.render(, document.getElementById('root')) #### Demo 示例 ```jsx -import React,{useState,useEffect} from 'react' +import React, { useState, useEffect } from 'react' import ReactDOM from 'react-dom' import { SchemaForm, @@ -379,9 +378,9 @@ import Printer from '@uform/printer' import '@alifd/next/dist/next.css' const App = () => { - const [values,setValues] = useState({}) - useEffect(()=>{ - setTimeout(()=>{ + const [values, setValues] = useState({}) + useEffect(() => { + setTimeout(() => { setValues({ aa: [ { @@ -394,143 +393,142 @@ const App = () => { } ] }) - },1000) - },[]) + }, 1000) + }, []) return ( - - { - const loading = name => { - setFieldState(name, state => { - state.loading = true - }) - } - const loaded = name => { - setFieldState(name, state => { - state.loading = false - }) - } - const hide = name => { - setFieldState(name, state => { - state.visible = false - }) - } - const show = name => { - setFieldState(name, state => { - state.visible = true + + { + const loading = name => { + setFieldState(name, state => { + state.loading = true + }) + } + const loaded = name => { + setFieldState(name, state => { + state.loading = false + }) + } + const hide = name => { + setFieldState(name, state => { + state.visible = false + }) + } + const show = name => { + setFieldState(name, state => { + state.visible = true + }) + } + const setEnum = (name, value) => { + setFieldState(name, state => { + state.props.enum = value + }) + } + const setValue = (name, value) => { + setFieldState(name, state => { + state.value = value + }) + } + $('onFormInit').subscribe(() => { + hide(FormPath.match('aa.*.*(cc,gg,dd.*.ee)')) }) - } - const setEnum = (name, value) => { - setFieldState(name, state => { - state.props.enum = value + $('onFieldValueChange', 'aa.*.bb').subscribe(fieldState => { + const cc = FormPath.transform( + fieldState.name, + /\d+/, + i => `aa.${i}.cc` + ) + if (!fieldState.value) { + hide(cc) + return + } + show(cc) + loading(cc) + setTimeout(() => { + loaded(cc) + setEnum(cc, ['1111', '2222']) + setValue(cc, '1111') + }, 1000) }) - } - const setValue = (name, value) => { - setFieldState(name, state => { - state.value = value + $('onFieldValueChange', 'aa.*.dd.*.ee').subscribe(fieldState => { + const gg = FormPath.transform( + fieldState.name, + /\d+/, + (i, j) => `aa.${i}.gg` + ) + setFieldState(gg, state => { + if (fieldState.value) { + state.visible = fieldState.value == '是' + } + }) }) - } - $('onFormInit').subscribe(() => { - hide(FormPath.match('aa.*.*(cc,gg,dd.*.ee)')) - }) - $('onFieldChange', 'aa.*.bb').subscribe(fieldState => { - const cc = FormPath.transform( - fieldState.name, - /\d+/, - i => `aa.${i}.cc` - ) - if (!fieldState.value) { - hide(cc) - return - } - show(cc) - loading(cc) - setTimeout(() => { - loaded(cc) - setEnum(cc, ['1111', '2222']) - setValue(cc, '1111') - }, 1000) - }) - $('onFieldChange', 'aa.*.dd.*.ee').subscribe(fieldState => { - const gg = FormPath.transform( - fieldState.name, - /\d+/, - (i, j) => `aa.${i}.gg` - ) - setFieldState(gg, state => { - if (fieldState.value) { + $('onFieldValueChange', 'aa.*.dd.*.ff').subscribe(fieldState => { + const ee = FormPath.transform( + fieldState.name, + /\d+/, + (i, j) => `aa.${i}.dd.${j}.ee` + ) + setFieldState(ee, state => { state.visible = fieldState.value == '是' - } - }) - }) - $('onFieldChange', 'aa.*.dd.*.ff').subscribe(fieldState => { - const ee = FormPath.transform( - fieldState.name, - /\d+/, - (i, j) => `aa.${i}.dd.${j}.ee` - ) - setFieldState(ee, state => { - state.visible = fieldState.value == '是' + }) }) - }) - }} - onSubmit={v => console.log(v)} - initialValues={values} - > - - - - - - - - - - - - - - - - - + }} + onSubmit={v => console.log(v)} + initialValues={values} + > + + + + + + + + + + + + + + + + + + - - + + - - - - - - - - -) + + + + + + + + ) } ReactDOM.render(, document.getElementById('root')) ``` - diff --git a/docs/Examples/next/Validation.md b/docs/Examples/next/Validation.md index 795fc11d056..0b4ab17ef85 100644 --- a/docs/Examples/next/Validation.md +++ b/docs/Examples/next/Validation.md @@ -146,7 +146,7 @@ const App = () => ( labelCol={6} wrapperCol={6} effects={($, { setFieldState, getFieldState }) => { - $('onFieldChange', '*(password,confirm)').subscribe(fieldState => { + $('onFieldValueChange', '*(password,confirm)').subscribe(fieldState => { const selfName = fieldState.name const selfValue = fieldState.value const otherName = selfName == 'password' ? 'confirm' : 'password' diff --git a/packages/antd/src/fields/cards.tsx b/packages/antd/src/fields/cards.tsx index 1b13bb6ba83..c1a29afb2c0 100644 --- a/packages/antd/src/fields/cards.tsx +++ b/packages/antd/src/fields/cards.tsx @@ -4,7 +4,7 @@ import { ISchemaFieldComponentProps, SchemaField } from '@uform/react-schema-renderer' -import { toArr, isFn } from '@uform/shared' +import { toArr, isFn, FormPath } from '@uform/shared' import { ArrayList } from '@uform/react-shared-components' import { CircleButton, TextButton } from '../components/Button' import { Card, Icon } from 'antd' @@ -85,7 +85,7 @@ const FormCardsField = styled(
    } > - + ) })} @@ -123,8 +123,8 @@ const FormCardsField = styled( .ant-card { box-shadow: none; } - .ant-card-body{ - padding:20px 10px 0 10px; + .ant-card-body { + padding: 20px 10px 0 10px; } .array-cards-addition { box-shadow: none; @@ -138,7 +138,7 @@ const FormCardsField = styled( display: flex; flex-direction: column; align-items: center; - margin-bottom:10px; + margin-bottom: 10px; img { height: 85px; } diff --git a/packages/antd/src/shared.ts b/packages/antd/src/shared.ts index 64e0363713a..0a7b22e9b74 100644 --- a/packages/antd/src/shared.ts +++ b/packages/antd/src/shared.ts @@ -45,7 +45,7 @@ export const transformDataSourceKey = (component, dataSourceKey) => { export const normalizeCol = ( col: { span: number; offset?: number } | number, - defaultValue: { span: number } + defaultValue?: { span: number } ): { span: number; offset?: number } => { if (!col) { return defaultValue diff --git a/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap b/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap index 09f00018cff..a38a1040f8d 100644 --- a/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap +++ b/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap @@ -893,7 +893,7 @@ Object { "path": "aa.bb", "props": Object {}, "unmounted": false, - "visible": false, + "visible": true, }, "aa.bb.cc": Object { "active": false, @@ -927,7 +927,7 @@ Object { "values": Array [ 123, ], - "visible": false, + "visible": true, "visited": false, "warnings": Array [], }, @@ -1072,7 +1072,7 @@ Object { "values": Array [ 123, ], - "visible": false, + "visible": true, "visited": false, "warnings": Array [], }, @@ -1179,7 +1179,7 @@ Object { "cc": 123, }, ], - "visible": false, + "visible": true, "visited": false, "warnings": Array [], }, @@ -1215,7 +1215,7 @@ Object { "values": Array [ 123, ], - "visible": false, + "visible": true, "visited": false, "warnings": Array [], }, @@ -2670,7 +2670,7 @@ Object { "values": Array [ 123, ], - "visible": false, + "visible": true, "visited": false, "warnings": Array [], }, @@ -2706,7 +2706,7 @@ Object { "values": Array [ 222, ], - "visible": false, + "visible": true, "visited": false, "warnings": Array [], }, @@ -2735,7 +2735,11 @@ Object { "unmounted": false, "valid": true, "validating": false, - "values": Object {}, + "values": Object { + "aa": Object { + "bb": "123", + }, + }, "warnings": Array [], }, "aa": Object { @@ -2796,11 +2800,11 @@ Object { "initialized": true, "invalid": false, "loading": false, - "modified": false, + "modified": true, "mounted": false, "name": "aa.bb", "path": "aa.bb", - "pristine": true, + "pristine": false, "props": Object {}, "required": false, "ruleErrors": Array [], @@ -2811,11 +2815,11 @@ Object { "unmounted": false, "valid": true, "validating": false, - "value": 123, + "value": "123", "values": Array [ - 123, + "123", ], - "visible": false, + "visible": true, "visited": false, "warnings": Array [], }, @@ -2851,7 +2855,7 @@ Object { "values": Array [ 222, ], - "visible": false, + "visible": true, "visited": false, "warnings": Array [], }, @@ -2946,7 +2950,7 @@ Object { "initialized": true, "invalid": false, "loading": false, - "modified": false, + "modified": true, "mounted": false, "name": "aa.bb", "path": "aa.bb", diff --git a/packages/core/src/__tests__/index.spec.ts b/packages/core/src/__tests__/index.spec.ts index ba17c10a8c2..c3d44c6e409 100644 --- a/packages/core/src/__tests__/index.spec.ts +++ b/packages/core/src/__tests__/index.spec.ts @@ -13,7 +13,7 @@ const resetInitValues = { } const deepValues = { a: { - b: { c: { d: { e: 1}}}, + b: { c: { d: { e: 1 } } }, c: { e: 2 } @@ -23,7 +23,6 @@ const deepValues = { } } - describe('createForm', () => { test('values', () => { const form = createForm({ @@ -109,8 +108,8 @@ describe('createForm', () => { expect(form.getFormState(state => state.values)).toEqual(testValues2) // change field's value - aa.setState(state => state.value = changeValues.aa) - bb.setState(state => state.value = changeValues.bb) + aa.setState(state => (state.value = changeValues.aa)) + bb.setState(state => (state.value = changeValues.bb)) expect(onFieldChange).toBeCalledTimes(4) expect(form.getFormState(state => state.values)).toEqual(changeValues) expect(aa.getState(state => state.value)).toEqual(changeValues.aa) @@ -138,7 +137,7 @@ describe('graph', () => { form.registerField({ path: 'aa', value: changeValues.aa }) form.registerField({ path: 'bb', value: changeValues.bb }) const snapshot = form.getFormGraph() - form.setFieldState('aa', state => state.visible = false) + form.setFieldState('aa', state => (state.visible = false)) expect(form.getFormGraph()).toMatchSnapshot() // change graph or you can also call it 'time travel' @@ -148,10 +147,10 @@ describe('graph', () => { }) describe('submit', () => { - test('onSubmit', async() => { + test('onSubmit', async () => { const onSubmitContructor = jest.fn() const onSubmitFn = jest.fn() - const changeSubmitPayload = (values) => ({ hello: 'world' }) + const changeSubmitPayload = values => ({ hello: 'world' }) const form1 = createForm({ onSubmit: onSubmitContructor }) const form2 = createForm() @@ -168,30 +167,47 @@ describe('submit', () => { expect(onSubmitContructor).toBeCalledTimes(1) expect(onSubmitFn).toBeCalledTimes(2) const result = await form2.submit(changeSubmitPayload) - expect(result).toEqual({ validated: { - errors: [], - warnings: [], - }, payload: { hello: 'world'} }) + expect(result).toEqual({ + validated: { + errors: [], + warnings: [] + }, + payload: { hello: 'world' } + }) }) test('submitResult', async () => { const sunmitFailed = jest.fn() - const form = createForm() - form.registerField({ path: 'a', rules: [(value) => { - return value === undefined ? { type: 'error', message: 'a is required' } : null - }] }) - form.registerField({ path: 'b', rules: [(value) => { - return value === undefined ? { type: 'warning', message: 'b is required' } : null - }] }) + const form = createForm() + form.registerField({ + path: 'a', + rules: [ + value => { + return value === undefined + ? { type: 'error', message: 'a is required' } + : null + } + ] + }) + form.registerField({ + path: 'b', + rules: [ + value => { + return value === undefined + ? { type: 'warning', message: 'b is required' } + : null + } + ] + }) // error failed try { await form.submit() } catch (errors) { sunmitFailed() - expect(errors).toEqual([{ path: 'a', messages: ['a is required']}]) + expect(errors).toEqual([{ path: 'a', messages: ['a is required'] }]) } - + // warning pass form.setFieldValue('a', 1) let validated @@ -202,7 +218,9 @@ describe('submit', () => { sunmitFailed() } - expect(validated.warnings).toEqual([{ path: 'b', messages: ['b is required'] }]) + expect(validated.warnings).toEqual([ + { path: 'b', messages: ['b is required'] } + ]) expect(validated.errors).toEqual([]) expect(sunmitFailed).toHaveBeenCalledTimes(1) }) @@ -221,7 +239,7 @@ describe('submit', () => { const form = createForm({ lifecycles: [ new FormLifeCycle(LifeCycleTypes.ON_FORM_SUBMIT_START, onSubmitStart), - new FormLifeCycle(LifeCycleTypes.ON_FORM_SUBMIT_END, onSubmitEnd), + new FormLifeCycle(LifeCycleTypes.ON_FORM_SUBMIT_END, onSubmitEnd) ], onSubmit: () => { expect(form.getFormState(state => state.submitting)).toEqual(true) @@ -257,7 +275,9 @@ describe('reset', () => { await form.reset({ forceClear: true }) expect(form.getFormGraph()).toMatchSnapshot() expect(form.getFormState(state => state.values)).toEqual({ aa: { bb: [] } }) - expect(form.getFormState(state => state.initialValues)).toEqual(resetInitValues) + expect(form.getFormState(state => state.initialValues)).toEqual( + resetInitValues + ) }) test('array reset no forceclear(initial values)', async () => { @@ -278,7 +298,9 @@ describe('reset', () => { await form.reset() expect(form.getFormGraph()).toMatchSnapshot() expect(form.getFormState(state => state.values)).toEqual(resetInitValues) - expect(form.getFormState(state => state.initialValues)).toEqual(resetInitValues) + expect(form.getFormState(state => state.initialValues)).toEqual( + resetInitValues + ) }) test('array reset no forceclear(values)', async () => { @@ -298,7 +320,7 @@ describe('reset', () => { expect(form.getFormGraph()).toMatchSnapshot() await form.reset() expect(form.getFormGraph()).toMatchSnapshot() - expect(form.getFormState(state => state.values)).toEqual({ aa: { bb: [] }}) + expect(form.getFormState(state => state.values)).toEqual({ aa: { bb: [] } }) expect(form.getFormState(state => state.initialValues)).toEqual({}) }) }) @@ -308,15 +330,31 @@ describe('clearErrors', () => { const form = createForm() const warnMsg = ['warning msg'] const errMsg = ['error msg'] - form.registerField({ path: 'b', rules: [(v) => v === undefined ? ({ type: 'warning', message: 'warning msg' }) : undefined] }) // CustomValidator warning - form.registerField({ path: 'c', rules: [(v) => v === undefined ? ({ type: 'error', message: 'error msg' }) : undefined] }) // CustomValidator error + form.registerField({ + path: 'b', + rules: [ + v => + v === undefined + ? { type: 'warning', message: 'warning msg' } + : undefined + ] + }) // CustomValidator warning + form.registerField({ + path: 'c', + rules: [ + v => + v === undefined ? { type: 'error', message: 'error msg' } : undefined + ] + }) // CustomValidator error const result1 = await form.validate() expect(result1.warnings).toEqual([{ path: 'b', messages: warnMsg }]) - expect(result1.errors).toEqual([{ path: 'c', messages: errMsg}]) + expect(result1.errors).toEqual([{ path: 'c', messages: errMsg }]) form.clearErrors('b') expect(form.getFormState(state => state.warnings)).toEqual([]) - expect(form.getFormState(state => state.errors)).toEqual([{ path: 'c', messages: errMsg}]) + expect(form.getFormState(state => state.errors)).toEqual([ + { path: 'c', messages: errMsg } + ]) form.clearErrors('c') expect(form.getFormState(state => state.warnings)).toEqual([]) @@ -324,20 +362,16 @@ describe('clearErrors', () => { const result2 = await form.validate() expect(result2.warnings).toEqual([{ path: 'b', messages: warnMsg }]) - expect(result2.errors).toEqual([{ path: 'c', messages: errMsg}]) + expect(result2.errors).toEqual([{ path: 'c', messages: errMsg }]) form.clearErrors() expect(form.getFormState(state => state.warnings)).toEqual([]) expect(form.getFormState(state => state.errors)).toEqual([]) }) - test('wildcard path', async () => { + test('wildcard path', async () => {}) - }) - - test('effect', async () => { - - }) + test('effect', async () => {}) }) describe('validate', () => { @@ -352,14 +386,20 @@ describe('validate', () => { const onValidateFailedTrigger = jest.fn() const onValidateFailed = ({ warnings, errors }) => { expect(warnings).toEqual([{ path: 'b', messages: ['warning msg'] }]) - expect(errors).toEqual([{ path: 'c', messages: ['error msg']}]) - onValidateFailedTrigger(); - }; + expect(errors).toEqual([{ path: 'c', messages: ['error msg'] }]) + onValidateFailedTrigger() + } const form = createForm({ onValidateFailed }) - form.registerField({ path: 'b', rules: [() => ({ type: 'warning', message: 'warning msg' })] }) // CustomValidator warning - form.registerField({ path: 'c', rules: [() => ({ type: 'error', message: 'error msg' })] }) // CustomValidator error + form.registerField({ + path: 'b', + rules: [() => ({ type: 'warning', message: 'warning msg' })] + }) // CustomValidator warning + form.registerField({ + path: 'c', + rules: [() => ({ type: 'error', message: 'error msg' })] + }) // CustomValidator error await form.validate() expect(onValidateFailedTrigger).toBeCalledTimes(1) }) @@ -369,15 +409,27 @@ describe('validate', () => { const onValidateEnd = jest.fn() const form = createForm({ lifecycles: [ - new FormLifeCycle(LifeCycleTypes.ON_FORM_VALIDATE_START, onValidateStart), - new FormLifeCycle(LifeCycleTypes.ON_FORM_VALIDATE_END, onValidateEnd), - ], + new FormLifeCycle( + LifeCycleTypes.ON_FORM_VALIDATE_START, + onValidateStart + ), + new FormLifeCycle(LifeCycleTypes.ON_FORM_VALIDATE_END, onValidateEnd) + ] }) form.registerField({ path: 'a', rules: ['number'] }) // string - form.registerField({ path: 'b', rules: [() => ({ type: 'warning', message: 'warning msg' })] }) // CustomValidator warning - form.registerField({ path: 'c', rules: [() => ({ type: 'error', message: 'warning msg' })] }) // CustomValidator error + form.registerField({ + path: 'b', + rules: [() => ({ type: 'warning', message: 'warning msg' })] + }) // CustomValidator warning + form.registerField({ + path: 'c', + rules: [() => ({ type: 'error', message: 'warning msg' })] + }) // CustomValidator error form.registerField({ path: 'd', rules: [() => 'straight error msg'] }) // CustomValidator string - form.registerField({ path: 'e', rules: [{ required: true, message: 'desc msg' }] }) // ValidateDescription + form.registerField({ + path: 'e', + rules: [{ required: true, message: 'desc msg' }] + }) // ValidateDescription expect(onValidateStart).toBeCalledTimes(0) expect(onValidateEnd).toBeCalledTimes(0) @@ -385,10 +437,10 @@ describe('validate', () => { const validatePromise = form.validate() expect(form.getFormState(state => state.validating)).toEqual(true) expect(onValidateStart).toBeCalledTimes(1) - validatePromise.then((validated) => { + validatePromise.then(validated => { expect(form.getFormState(state => state.validating)).toEqual(false) expect(onValidateEnd).toBeCalledTimes(1) - const { warnings, errors } = validated; + const { warnings, errors } = validated expect(warnings.length).toEqual(1) expect(errors.length).toEqual(4) }) @@ -396,30 +448,56 @@ describe('validate', () => { test('path', async () => { const form = createForm() - form.registerField({ path: 'b', rules: [(v) => v === undefined ? ({ type: 'warning', message: 'warning msg' }) : undefined] }) // CustomValidator warning - form.registerField({ path: 'c', rules: [(v) => v === undefined ? ({ type: 'error', message: 'error msg' }) : undefined] }) // CustomValidator error + form.registerField({ + path: 'b', + rules: [ + v => + v === undefined + ? { type: 'warning', message: 'warning msg' } + : undefined + ] + }) // CustomValidator warning + form.registerField({ + path: 'c', + rules: [ + v => + v === undefined ? { type: 'error', message: 'error msg' } : undefined + ] + }) // CustomValidator error const bResult = await form.validate('b') expect(bResult.warnings).toEqual([{ path: 'b', messages: ['warning msg'] }]) - expect(bResult.errors).toEqual([]) - expect(form.getFieldState('b', state => state.warnings)).toEqual(['warning msg']) + expect(bResult.errors).toEqual([]) + expect(form.getFieldState('b', state => state.warnings)).toEqual([ + 'warning msg' + ]) expect(form.getFieldState('c', state => state.errors)).toEqual([]) - expect(form.getFormState(state => state.warnings)).toEqual([{ path: 'b', messages: ['warning msg'] }]) + expect(form.getFormState(state => state.warnings)).toEqual([ + { path: 'b', messages: ['warning msg'] } + ]) expect(form.getFormState(state => state.errors)).toEqual([]) const cResult = await form.validate('c') expect(cResult.warnings).toEqual([]) - expect(cResult.errors).toEqual([{ path: 'c', messages: ['error msg']}]) - expect(form.getFieldState('b', state => state.warnings)).toEqual(['warning msg']) - expect(form.getFieldState('c', state => state.errors)).toEqual(['error msg']) - expect(form.getFormState(state => state.warnings)).toEqual([{ path: 'b', messages: ['warning msg'] }]) - expect(form.getFormState(state => state.errors)).toEqual([{ path: 'c', messages: ['error msg']}]) + expect(cResult.errors).toEqual([{ path: 'c', messages: ['error msg'] }]) + expect(form.getFieldState('b', state => state.warnings)).toEqual([ + 'warning msg' + ]) + expect(form.getFieldState('c', state => state.errors)).toEqual([ + 'error msg' + ]) + expect(form.getFormState(state => state.warnings)).toEqual([ + { path: 'b', messages: ['warning msg'] } + ]) + expect(form.getFormState(state => state.errors)).toEqual([ + { path: 'c', messages: ['error msg'] } + ]) form.setFieldValue('b', 1) form.setFieldValue('c', 1) const bResult2 = await form.validate('b') const cResult2 = await form.validate('c') expect(bResult2.warnings).toEqual([]) - expect(bResult2.errors).toEqual([]) + expect(bResult2.errors).toEqual([]) expect(cResult2.warnings).toEqual([]) expect(cResult2.errors).toEqual([]) expect(form.getFieldState('b', state => state.warnings)).toEqual([]) @@ -437,7 +515,7 @@ describe('setFormState', () => { expect(form.getFormState()).toEqual(state) }) - test('set', async() => { + test('set', async () => { const form = createForm() // pristine 依赖 draft.values === draft.initialValues // invalid 依赖 errors.length === 0 @@ -445,15 +523,29 @@ describe('setFormState', () => { // loading 取决于validating // mounted 和 unmounted 互为取反,先读mounted // errors, warnings 无法被设置,会从最新的state中获取 - form.registerField({ path: 'b', rules: [(v) => v === undefined ? ({ type: 'warning', message: 'warning msg' }) : undefined] }) // CustomValidator warning - form.registerField({ path: 'c', rules: [(v) => v === undefined ? ({ type: 'error', message: 'error msg' }) : undefined] }) // CustomValidator error + form.registerField({ + path: 'b', + rules: [ + v => + v === undefined + ? { type: 'warning', message: 'warning msg' } + : undefined + ] + }) // CustomValidator warning + form.registerField({ + path: 'c', + rules: [ + v => + v === undefined ? { type: 'error', message: 'error msg' } : undefined + ] + }) // CustomValidator error const { errors, warnings } = await form.validate() const invalid = errors.length > 0 const values = { b: '2' } const initialValues = { a: '1' } const validating = true - form.setFormState((state) => { + form.setFormState(state => { state.pristine = false state.valid = false state.invalid = false @@ -484,32 +576,40 @@ describe('setFormState', () => { initialValues, mounted: true, unmounted: false, - props: { hello: 'world' }, + props: { hello: 'world' } }) - }) + }) }) describe('getFormState', () => { test('basic', async () => { const form = createForm() const state = form.getFormState() - expect(state).toEqual(form.getFormState((state) => state)) + expect(state).toEqual(form.getFormState(state => state)) expect(form.getFormState(state => state.pristine)).toEqual(state.pristine) expect(form.getFormState(state => state.valid)).toEqual(state.valid) expect(form.getFormState(state => state.invalid)).toEqual(state.invalid) expect(form.getFormState(state => state.loading)).toEqual(state.loading) - expect(form.getFormState(state => state.validating)).toEqual(state.validating) - expect(form.getFormState(state => state.submitting)).toEqual(state.submitting) - expect(form.getFormState(state => state.initialized)).toEqual(state.initialized) + expect(form.getFormState(state => state.validating)).toEqual( + state.validating + ) + expect(form.getFormState(state => state.submitting)).toEqual( + state.submitting + ) + expect(form.getFormState(state => state.initialized)).toEqual( + state.initialized + ) expect(form.getFormState(state => state.editable)).toEqual(state.editable) expect(form.getFormState(state => state.errors)).toEqual(state.errors) expect(form.getFormState(state => state.warnings)).toEqual(state.warnings) expect(form.getFormState(state => state.values)).toEqual(state.values) - expect(form.getFormState(state => state.initialValues)).toEqual(state.initialValues) + expect(form.getFormState(state => state.initialValues)).toEqual( + state.initialValues + ) expect(form.getFormState(state => state.mounted)).toEqual(state.mounted) expect(form.getFormState(state => state.unmounted)).toEqual(state.unmounted) expect(form.getFormState(state => state.props)).toEqual(state.props) - }) + }) }) describe('setFieldState', () => { @@ -523,17 +623,24 @@ describe('setFieldState', () => { test('validating and loading', () => { const form = createForm() - form.registerField({ path: 'a', rules: [ - (v) => v === undefined ? ({ type: 'warning', message: 'warning msg' }) : undefined, - (v) => v === undefined ? ({ type: 'error', message: 'error msg' }) : undefined - ] }) + form.registerField({ + path: 'a', + rules: [ + v => + v === undefined + ? { type: 'warning', message: 'warning msg' } + : undefined, + v => + v === undefined ? { type: 'error', message: 'error msg' } : undefined + ] + }) expect(form.getFieldState('a', state => state.validating)).toEqual(false) expect(form.getFieldState('a', state => state.loading)).toEqual(false) - form.setFieldState('a', state => state.validating = true) + form.setFieldState('a', state => (state.validating = true)) expect(form.getFieldState('a', state => state.loading)).toEqual(true) - form.setFieldState('a', state => state.validating = false) + form.setFieldState('a', state => (state.validating = false)) expect(form.getFieldState('a', state => state.loading)).toEqual(false) - }); + }) test('value, values and parseValues', () => { const form = createForm() @@ -541,79 +648,94 @@ describe('setFieldState', () => { expect(form.getFieldState('a', state => state.modified)).toEqual(false) expect(form.getFieldState('a', state => state.value)).toEqual(undefined) expect(form.getFieldState('a', state => state.values)).toEqual([undefined]) - const arr = [1,2,3] - form.setFieldState('a', state => state.value = arr) + const arr = [1, 2, 3] + form.setFieldState('a', state => (state.value = arr)) expect(form.getFieldState('a', state => state.modified)).toEqual(true) expect(form.getFieldState('a', state => state.value)).toEqual(arr) expect(form.getFieldState('a', state => state.values)).toEqual([arr]) - form.setFieldState('a', state => state.values = ['e', 'context']) + form.setFieldState('a', state => (state.values = ['e', 'context'])) // values 第一个参数会是value, 处理onChange的多参数一般会和values[0]同步,这里value测试极端情况 expect(form.getFieldState('a', state => state.value)).toEqual(arr) - expect(form.getFieldState('a', state => state.values)).toEqual([arr, 'context']) + expect(form.getFieldState('a', state => state.values)).toEqual([ + arr, + 'context' + ]) // visible为false或者已卸载的组件无法修改value - form.setFieldState('a', state => state.visible = false) - form.setFieldState('a', state => state.value = [4,5,6]) + form.setFieldState('a', state => (state.visible = false)) + form.setFieldState('a', state => (state.value = [4, 5, 6])) expect(form.getFieldState('a', state => state.value)).toEqual(arr) form.setFieldState('a', state => { - state.visible = true; - state.unmounted = true; + state.visible = true + state.unmounted = true }) - form.setFieldState('a', state => state.value = [4,5,6]) + form.setFieldState('a', state => (state.value = [4, 5, 6])) expect(form.getFieldState('a', state => state.value)).toEqual(arr) - }); + }) test('mount and unmount', () => { const form = createForm() form.registerField({ path: 'a' }) expect(form.getFieldState('a', state => state.mounted)).toEqual(false) expect(form.getFieldState('a', state => state.unmounted)).toEqual(false) - form.setFieldState('a', state => state.mounted = true) + form.setFieldState('a', state => (state.mounted = true)) expect(form.getFieldState('a', state => state.mounted)).toEqual(true) expect(form.getFieldState('a', state => state.unmounted)).toEqual(false) - }); + }) test('rules, required and parseRules', () => { const form = createForm() form.registerField({ path: 'a' }) expect(form.getFieldState('a', state => state.required)).toEqual(false) expect(form.getFieldState('a', state => state.rules)).toEqual([]) - form.setFieldState('a', state => state.required = true) + form.setFieldState('a', state => (state.required = true)) expect(form.getFieldState('a', state => state.required)).toEqual(true) - const customValidator: ValidatePatternRules[] = [(v, _: ValidateDescription) => v === undefined ? ({ type: 'warning', message: 'warning msg' }) : null]; - form.setFieldState('a', state => state.rules = customValidator) - const rules = form.getFieldState('a', state => state.rules); - expect(rules).toEqual([...customValidator, { required: true }]) - }); + const customValidator: ValidatePatternRules[] = [ + (v, _: ValidateDescription) => + v === undefined ? { type: 'warning', message: 'warning msg' } : null + ] + form.setFieldState('a', state => (state.rules = customValidator)) + const rules = form.getFieldState('a', state => state.rules) + expect(rules).toEqual([...customValidator, { required: true }]) + }) - test('pristine', () => { // 无法被修改,依赖value和initialValue的差别 + test('pristine', () => { + // 无法被修改,依赖value和initialValue的差别 const form = createForm() form.registerField({ path: 'a' }) expect(form.getFieldState('a', state => state.pristine)).toEqual(true) - form.setFieldState('a', state => state.pristine = false) + form.setFieldState('a', state => (state.pristine = false)) expect(form.getFieldState('a', state => state.pristine)).toEqual(true) - form.setFieldState('a', state => state.value = '1') + form.setFieldState('a', state => (state.value = '1')) expect(form.getFieldState('a', state => state.pristine)).toEqual(false) - form.setFieldState('a', state => state.initialValue = '1') + form.setFieldState('a', state => (state.initialValue = '1')) expect(form.getFieldState('a', state => state.pristine)).toEqual(true) - }); + }) - test('invalid 和 valid', () => { // 无法被修改,依赖错误信息和告警信息 + test('invalid 和 valid', () => { + // 无法被修改,依赖错误信息和告警信息 const form = createForm() form.registerField({ path: 'a' }) expect(form.getFieldState('a', state => state.invalid)).toEqual(false) expect(form.getFieldState('a', state => state.valid)).toEqual(true) - form.setFieldState('a', state => state.invalid = true) - form.setFieldState('a', state => state.valid = false) + form.setFieldState('a', state => (state.invalid = true)) + form.setFieldState('a', state => (state.valid = false)) expect(form.getFieldState('a', state => state.invalid)).toEqual(false) expect(form.getFieldState('a', state => state.valid)).toEqual(true) - }); - - test('set errors and warnings', async() => { - const form = createForm() - const a = form.registerField({ path: 'a', rules: [ - (v) => v === undefined ? ({ type: 'warning', message: 'warning msg' }) : undefined, - (v) => v === undefined ? ({ type: 'error', message: 'error msg' }) : undefined - ] }) + }) + + test('set errors and warnings', async () => { + const form = createForm() + const a = form.registerField({ + path: 'a', + rules: [ + v => + v === undefined + ? { type: 'warning', message: 'warning msg' } + : undefined, + v => + v === undefined ? { type: 'error', message: 'error msg' } : undefined + ] + }) const state = form.getFieldState('a') expect(state.effectErrors).toEqual([]) expect(state.effectWarnings).toEqual([]) @@ -637,8 +759,8 @@ describe('setFieldState', () => { // 看起来有问题,但是对于开发者并不透出effectErrors的概念,只让他感知这是errors // errors = effectErrors + ruleErrors // warnings = effectWarnings + ruleWarnings - form.setFieldState('a', state => state.errors = ['effect errors msg']) - form.setFieldState('a', state => state.warnings = ['effect warning msg']) + form.setFieldState('a', state => (state.errors = ['effect errors msg'])) + form.setFieldState('a', state => (state.warnings = ['effect warning msg'])) const state3 = form.getFieldState('a') expect(state3.effectErrors).toEqual(['effect errors msg']) expect(state3.effectWarnings).toEqual(['effect warning msg']) @@ -646,19 +768,19 @@ describe('setFieldState', () => { expect(state3.warnings).toEqual(['warning msg', 'effect warning msg']) // 不可编辑,清空所有错误和警告信息 - form.setFieldState('a', state => state.editable = false) + form.setFieldState('a', state => (state.editable = false)) const state4 = form.getFieldState('a') expect(state4.effectErrors).toEqual([]) expect(state4.effectWarnings).toEqual([]) expect(state4.errors).toEqual([]) expect(state4.warnings).toEqual([]) - form.setFieldState('a', state => state.editable = true) + form.setFieldState('a', state => (state.editable = true)) // 隐藏,清空所有错误和警告信息 await mutators.validate() - form.setFieldState('a', state => state.errors = ['effect errors msg']) - form.setFieldState('a', state => state.warnings = ['effect warning msg']) - form.setFieldState('a', state => state.visible = false) + form.setFieldState('a', state => (state.errors = ['effect errors msg'])) + form.setFieldState('a', state => (state.warnings = ['effect warning msg'])) + form.setFieldState('a', state => (state.visible = false)) const state6 = form.getFieldState('a') expect(state6.effectErrors).toEqual([]) expect(state6.effectWarnings).toEqual([]) @@ -667,16 +789,16 @@ describe('setFieldState', () => { // 卸载组件,清空所有错误和警告信息 await mutators.validate() - form.setFieldState('a', state => state.errors = ['effect errors msg']) - form.setFieldState('a', state => state.warnings = ['effect warning msg']) + form.setFieldState('a', state => (state.errors = ['effect errors msg'])) + form.setFieldState('a', state => (state.warnings = ['effect warning msg'])) const state7 = form.getFieldState('a') expect(state7.effectErrors).toEqual([]) expect(state7.effectWarnings).toEqual([]) expect(state7.errors).toEqual([]) expect(state7.warnings).toEqual([]) - }); + }) - test('set editable', async() => { + test('set editable', async () => { const form = createForm() form.registerField({ path: 'a' }) const state = form.getFieldState('a') @@ -687,59 +809,85 @@ describe('setFieldState', () => { expect(state.formEditable).toEqual(undefined) expect(form.getFieldState('a', state => state.editable)).toEqual(true) // 简单设置 (editable会影响selfEditable) - form.setFieldState('a', state => state.editable = false) + form.setFieldState('a', state => (state.editable = false)) expect(form.getFieldState('a', state => state.editable)).toEqual(false) expect(form.getFieldState('a', state => state.selfEditable)).toEqual(false) // 设置影响计算的值selfEditable - form.setFieldState('a', state => state.selfEditable = true) + form.setFieldState('a', state => (state.selfEditable = true)) expect(form.getFieldState('a', state => state.editable)).toEqual(true) // 设置影响计算的值formEditable(selfEditable优先级高于formEditable) - form.setFieldState('a', state => state.selfEditable = undefined) - form.setFieldState('a', state => state.formEditable = false) + form.setFieldState('a', state => (state.selfEditable = undefined)) + form.setFieldState('a', state => (state.formEditable = false)) expect(form.getFieldState('a', state => state.editable)).toEqual(false) // 支持函数(UI层传入) - form.setFieldState('a', state => state.formEditable = () => true) + form.setFieldState('a', state => (state.formEditable = () => true)) expect(form.getFieldState('a', state => state.editable)).toEqual(true) // editable会影响selfEditable, 设置顺序又因为 editable > selfEditable > formEditable // 前两者都无效时,最终返回formEditable的值 - form.setFieldState('a', state => state.editable = undefined) - form.setFieldState('a', state => state.formEditable = () => false) - expect(form.getFieldState('a', state => state.selfEditable)).toEqual(undefined) + form.setFieldState('a', state => (state.editable = undefined)) + form.setFieldState('a', state => (state.formEditable = () => false)) + expect(form.getFieldState('a', state => state.selfEditable)).toEqual( + undefined + ) expect(form.getFieldState('a', state => state.editable)).toEqual(false) - }); + }) }) describe('getFieldState', () => { const form = createForm() form.registerField({ path: 'a' }) const state = form.getFieldState('a') - expect(state).toEqual(form.getFieldState('a', (state) => state)) + expect(state).toEqual(form.getFieldState('a', state => state)) expect(form.getFieldState('a', state => state.name)).toEqual(state.name) - expect(form.getFieldState('a', state => state.initialized)).toEqual(state.initialized) - expect(form.getFieldState('a', state => state.pristine)).toEqual(state.pristine) + expect(form.getFieldState('a', state => state.initialized)).toEqual( + state.initialized + ) + expect(form.getFieldState('a', state => state.pristine)).toEqual( + state.pristine + ) expect(form.getFieldState('a', state => state.valid)).toEqual(state.valid) expect(form.getFieldState('a', state => state.touched)).toEqual(state.touched) expect(form.getFieldState('a', state => state.invalid)).toEqual(state.invalid) expect(form.getFieldState('a', state => state.visible)).toEqual(state.visible) expect(form.getFieldState('a', state => state.display)).toEqual(state.display) - expect(form.getFieldState('a', state => state.editable)).toEqual(state.editable) - expect(form.getFieldState('a', state => state.formEditable)).toEqual(state.formEditable) + expect(form.getFieldState('a', state => state.editable)).toEqual( + state.editable + ) + expect(form.getFieldState('a', state => state.formEditable)).toEqual( + state.formEditable + ) expect(form.getFieldState('a', state => state.loading)).toEqual(state.loading) - expect(form.getFieldState('a', state => state.modified)).toEqual(state.modified) + expect(form.getFieldState('a', state => state.modified)).toEqual( + state.modified + ) expect(form.getFieldState('a', state => state.active)).toEqual(state.active) expect(form.getFieldState('a', state => state.visited)).toEqual(state.visited) - expect(form.getFieldState('a', state => state.validating)).toEqual(state.validating) + expect(form.getFieldState('a', state => state.validating)).toEqual( + state.validating + ) expect(form.getFieldState('a', state => state.errors)).toEqual(state.errors) expect(form.getFieldState('a', state => state.values)).toEqual(state.values) - expect(form.getFieldState('a', state => state.effectErrors)).toEqual(state.effectErrors) - expect(form.getFieldState('a', state => state.warnings)).toEqual(state.warnings) - expect(form.getFieldState('a', state => state.effectWarnings)).toEqual(state.effectWarnings) + expect(form.getFieldState('a', state => state.effectErrors)).toEqual( + state.effectErrors + ) + expect(form.getFieldState('a', state => state.warnings)).toEqual( + state.warnings + ) + expect(form.getFieldState('a', state => state.effectWarnings)).toEqual( + state.effectWarnings + ) expect(form.getFieldState('a', state => state.value)).toEqual(state.value) - expect(form.getFieldState('a', state => state.initialValue)).toEqual(state.initialValue) + expect(form.getFieldState('a', state => state.initialValue)).toEqual( + state.initialValue + ) expect(form.getFieldState('a', state => state.rules)).toEqual(state.rules) - expect(form.getFieldState('a', state => state.required)).toEqual(state.required) + expect(form.getFieldState('a', state => state.required)).toEqual( + state.required + ) expect(form.getFieldState('a', state => state.mounted)).toEqual(state.mounted) - expect(form.getFieldState('a', state => state.unmounted)).toEqual(state.unmounted) + expect(form.getFieldState('a', state => state.unmounted)).toEqual( + state.unmounted + ) expect(form.getFieldState('a', state => state.props)).toEqual(state.props) }) @@ -807,7 +955,7 @@ describe('registerField', () => { form.registerField({ path: 'c' }) form.registerField({ path: 'd', editable: false }) form.registerField({ path: 'e', editable: true }) - expect(form.getFieldValue('a')).toEqual(1) + expect(form.getFieldValue('a')).toEqual(1) expect(form.getFieldValue('b')).toEqual(undefined) expect(form.getFieldState('a', state => state.values)).toEqual([1]) expect(form.getFieldState('b', state => state.values)).toEqual([undefined]) @@ -817,7 +965,7 @@ describe('registerField', () => { }) test('merge', async () => { - const form = createForm({ values: { a: 1, b: 2, c: 3, d: 4 }}) + const form = createForm({ values: { a: 1, b: 2, c: 3, d: 4 } }) form.registerField({ path: 'a' }) form.registerField({ path: 'b', value: 'x' }) form.registerField({ path: 'c', initialValue: 'y' }) @@ -831,7 +979,7 @@ describe('registerField', () => { describe('registerVirtualField', () => { test('basic', async () => { - const vprops = { hello: 'world' }; + const vprops = { hello: 'world' } const form = createForm({ values: { a: 1 } }) form.registerVirtualField({ path: 'a' }) form.registerVirtualField({ path: 'b' }) @@ -841,9 +989,9 @@ describe('registerVirtualField', () => { expect(form.getFieldState('c', state => state.props)).toEqual(vprops) expect(form.getFieldState('b', state => state.display)).toEqual(true) expect(form.getFieldState('b', state => state.visible)).toEqual(true) - form.setFieldState('b', state => state.display = false) + form.setFieldState('b', state => (state.display = false)) expect(form.getFieldState('b', state => state.display)).toEqual(false) - form.setFieldState('b', state => state.visible = false) + form.setFieldState('b', state => (state.visible = false)) expect(form.getFieldState('b', state => state.visible)).toEqual(false) }) }) @@ -854,14 +1002,24 @@ describe('createMutators', () => { const form = createForm() const a = form.registerField({ path: 'a' }) const mutators = form.createMutators(a) - expect(form.getFieldState('a', (state => ({ values: state.values, value: state.value })))).toEqual({ + expect( + form.getFieldState('a', state => ({ + values: state.values, + value: state.value + })) + ).toEqual({ value: undefined, - values: [undefined], + values: [undefined] }) - mutators.change(1,2,3,4) - expect(form.getFieldState('a', (state => ({ values: state.values, value: state.value })))).toEqual({ + mutators.change(1, 2, 3, 4) + expect( + form.getFieldState('a', state => ({ + values: state.values, + value: state.value + })) + ).toEqual({ value: 1, - values: [1,2,3,4], + values: [1, 2, 3, 4] }) }) @@ -870,15 +1028,25 @@ describe('createMutators', () => { const a = form.registerField({ path: 'a' }) const mutators = form.createMutators(a) expect(form.getFormGraph()).toMatchSnapshot() - expect(form.getFieldState('a', (state => ({ active: state.active, visited: state.visited })))).toEqual({ + expect( + form.getFieldState('a', state => ({ + active: state.active, + visited: state.visited + })) + ).toEqual({ active: false, - visited: false, + visited: false }) mutators.focus() expect(form.getFormGraph()).toMatchSnapshot() - expect(form.getFieldState('a', (state => ({ active: state.active, visited: state.visited })))).toEqual({ + expect( + form.getFieldState('a', state => ({ + active: state.active, + visited: state.visited + })) + ).toEqual({ active: true, - visited: true, + visited: true }) }) @@ -887,16 +1055,26 @@ describe('createMutators', () => { const a = form.registerField({ path: 'a' }) const mutators = form.createMutators(a) expect(form.getFormGraph()).toMatchSnapshot() - expect(form.getFieldState('a', (state => ({ active: state.active, visited: state.visited })))).toEqual({ + expect( + form.getFieldState('a', state => ({ + active: state.active, + visited: state.visited + })) + ).toEqual({ active: false, - visited: false, + visited: false }) mutators.focus() mutators.blur() expect(form.getFormGraph()).toMatchSnapshot() - expect(form.getFieldState('a', (state => ({ active: state.active, visited: state.visited })))).toEqual({ + expect( + form.getFieldState('a', state => ({ + active: state.active, + visited: state.visited + })) + ).toEqual({ active: false, - visited: true, + visited: true }) }) @@ -924,7 +1102,7 @@ describe('createMutators', () => { const mutators = form.createMutators(mm) expect(form.getFieldValue('mm')).toEqual(arr) mutators.insert(1, 'x') - expect(form.getFieldValue('mm')).toEqual(['a','x','b']) + expect(form.getFieldValue('mm')).toEqual(['a', 'x', 'b']) }) test('remove', async () => { @@ -972,7 +1150,15 @@ describe('createMutators', () => { test('validate', async () => { const form = createForm() - const mm = form.registerField({ path: 'mm', rules: [(v) => v === undefined ? ({ type: 'warning', message: 'warning msg' }) : undefined] }) + const mm = form.registerField({ + path: 'mm', + rules: [ + v => + v === undefined + ? { type: 'warning', message: 'warning msg' } + : undefined + ] + }) const mutators = form.createMutators(mm) const result = await mutators.validate() expect(result.errors).toEqual([]) @@ -993,11 +1179,27 @@ describe('transformDataPath', () => { form.registerField({ path: 'a.b.c.d' }) form.registerField({ path: 'a.b.c.d.e' }) - expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a')).toString()).toEqual('a') - expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b')).toString()).toEqual('a.b') - expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c')).toString()).toEqual('a.b.c') - expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d')).toString()).toEqual('a.b.c.d') - expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d.e')).toString()).toEqual('a.b.c.d.e') + expect( + form.unsafe_do_not_use_transform_data_path(new FormPath('a')).toString() + ).toEqual('a') + expect( + form.unsafe_do_not_use_transform_data_path(new FormPath('a.b')).toString() + ).toEqual('a.b') + expect( + form + .unsafe_do_not_use_transform_data_path(new FormPath('a.b.c')) + .toString() + ).toEqual('a.b.c') + expect( + form + .unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d')) + .toString() + ).toEqual('a.b.c.d') + expect( + form + .unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d.e')) + .toString() + ).toEqual('a.b.c.d.e') }) test('virtual path', async () => { @@ -1008,11 +1210,27 @@ describe('transformDataPath', () => { form.registerVirtualField({ path: 'a.b.c.d' }) form.registerField({ path: 'a.b.c.d.e' }) - expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a')).toString()).toEqual('a') - expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b')).toString()).toEqual('a.b') - expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c')).toString()).toEqual('a.c') - expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d')).toString()).toEqual('a.c.d') - expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d.e')).toString()).toEqual('a.c.e') + expect( + form.unsafe_do_not_use_transform_data_path(new FormPath('a')).toString() + ).toEqual('a') + expect( + form.unsafe_do_not_use_transform_data_path(new FormPath('a.b')).toString() + ).toEqual('a.b') + expect( + form + .unsafe_do_not_use_transform_data_path(new FormPath('a.b.c')) + .toString() + ).toEqual('a.c') + expect( + form + .unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d')) + .toString() + ).toEqual('a.c.d') + expect( + form + .unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d.e')) + .toString() + ).toEqual('a.c.e') }) test('virtual path(head)', async () => { @@ -1021,9 +1239,17 @@ describe('transformDataPath', () => { form.registerField({ path: 'a.b' }) form.registerField({ path: 'a.b.c' }) - expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a')).toString()).toEqual('a') - expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b')).toString()).toEqual('b') - expect(form.unsafe_do_not_use_transform_data_path(new FormPath('a.b.c')).toString()).toEqual('b.c') + expect( + form.unsafe_do_not_use_transform_data_path(new FormPath('a')).toString() + ).toEqual('a') + expect( + form.unsafe_do_not_use_transform_data_path(new FormPath('a.b')).toString() + ).toEqual('b') + expect( + form + .unsafe_do_not_use_transform_data_path(new FormPath('a.b.c')) + .toString() + ).toEqual('b.c') }) }) @@ -1083,14 +1309,18 @@ describe('major sences', () => { form.registerField({ path: 'aa', value: {} }) form.registerField({ path: 'aa.bb', initialValue: 123 }) form.registerField({ path: 'aa.cc', initialValue: 222 }) - form.setFieldState('aa', state => state.visible = false) + form.setFieldState('aa', state => (state.visible = false)) expect(form.getFormState(state => state.values)).toEqual({}) - + expect(form.getFormGraph()).toMatchSnapshot() - form.setFieldState('aa.bb', state => state.value = '123') + form.setFieldState('aa.bb', state => (state.value = '123')) expect(form.getFormGraph()).toMatchSnapshot() - form.setFieldState('aa', state => state.visible = true) + form.setFieldState('aa', state => (state.visible = true)) + expect(form.getFieldValue('aa')).toEqual({ bb: 123, cc: 222 }) + expect(form.getFormState(state => state.values)).toEqual({ + aa: { bb: 123, cc: 222 } + }) expect(form.getFormGraph()).toMatchSnapshot() }) @@ -1099,7 +1329,7 @@ describe('major sences', () => { form.registerField({ path: 'aa', value: {} }) form.registerField({ path: 'aa.bb' }) form.registerField({ path: 'aa.bb.cc', value: 123 }) - form.setFieldState('aa', state => state.visible = false) + form.setFieldState('aa', state => (state.visible = false)) expect(form.getFormGraph()).toMatchSnapshot() expect(form.getFormState(state => state.values)).toEqual({}) }) @@ -1109,7 +1339,7 @@ describe('major sences', () => { form.registerField({ path: 'aa', value: {} }) form.registerField({ path: 'aa.bb' }) form.registerField({ path: 'aa.bb.cc', value: 123 }) - form.setFieldState('aa.bb', state => state.visible = false) + form.setFieldState('aa.bb', state => (state.visible = false)) expect(form.getFormGraph()).toMatchSnapshot() expect(form.getFormState(state => state.values)).toEqual({ aa: {} }) }) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c6256531478..d599b539e56 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -170,20 +170,12 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { } } if (visibleChanged) { + //visible不能遍历子节点控制其visible,visible只对当前节点生效 if (!published.visible) { deleteFormValuesIn(path, true) } else { setFormValuesIn(path, published.value) } - graph.eachChildren( - path, - childState => { - childState.setState((state: IFieldState) => { - state.visible = published.visible - }) - }, - false - ) } if (displayChanged) { graph.eachChildren( @@ -202,20 +194,10 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { } else { setFormValuesIn(path, published.value) } - graph.eachChildren( - path, - childState => { - childState.setState((state: IFieldState) => { - state.unmounted = published.unmounted - }) - }, - false - ) } if (mountedChanged && published.mounted) { heart.notify(LifeCycleTypes.ON_FIELD_MOUNT, field) } - if (valueChanged) { setFormValuesIn(path, published.value) heart.notify(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, field) @@ -300,11 +282,11 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { notify: onVirtualFieldChange({ field, path: nodePath }) } field.batch(() => { - batchRunTaskQueue(field, nodePath) field.setState((state: IVirtualFieldState) => { state.initialized = true state.props = props }) + batchRunTaskQueue(field, nodePath) }) return field } @@ -346,7 +328,6 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { } heart.notify(LifeCycleTypes.ON_FIELD_WILL_INIT, field) field.batch(() => { - batchRunTaskQueue(field, nodePath) field.setState((state: IFieldState) => { const formValue = getFormValuesIn(dataPath) const formInitialValue = getFormInitialValuesIn(dataPath) @@ -368,6 +349,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { state.editable = editable state.formEditable = options.editable }) + batchRunTaskQueue(field, nodePath) }) validator.register(nodePath, validate => { const { value, rules, editable, visible, unmounted } = field.getState() diff --git a/packages/core/src/shared/model.ts b/packages/core/src/shared/model.ts index 48511f271a7..939e7535876 100644 --- a/packages/core/src/shared/model.ts +++ b/packages/core/src/shared/model.ts @@ -19,6 +19,7 @@ export const createStateModel = ( public dirtyMap: StateDirtyMap public batching: boolean public controller: StateModel + constructor(defaultProps: DefaultProps) { super() this.state = { ...Factory.defaultState } @@ -87,8 +88,10 @@ export const createStateModel = ( if (isFn(callback)) { if (!hasProxy || this.props.useDirty) { const draft = this.getState() - this.dirtyNum = 0 - this.dirtyMap = {} + if (!this.batching) { + this.dirtyNum = 0 + this.dirtyMap = {} + } callback(draft) if (isFn(this.controller.computeState)) { this.controller.computeState(draft, this.state) @@ -114,12 +117,15 @@ export const createStateModel = ( if (this.dirtyNum > 0 && !silent) { if (this.batching) return this.notify(this.getState()) + this.dirtyMap = {} this.dirtyNum = 0 } } else { - this.dirtyNum = 0 - this.dirtyMap = {} + if (!this.batching) { + this.dirtyNum = 0 + this.dirtyMap = {} + } //用proxy解决脏检查计算属性问题 this.state = produce( this.state, @@ -131,16 +137,14 @@ export const createStateModel = ( }, patches => { patches.forEach(({ path, op, value }) => { - if (!this.dirtyMap[path[0]]) { - if (op === 'replace') { - if (!isEqual(this.state[path[0]], value)) { - this.dirtyMap[path[0]] = true - this.dirtyNum++ - } - } else { + if (op === 'replace') { + if (!isEqual(this.state[path[0]], value)) { this.dirtyMap[path[0]] = true this.dirtyNum++ } + } else { + this.dirtyMap[path[0]] = true + this.dirtyNum++ } }) } diff --git a/packages/core/src/state/field.ts b/packages/core/src/state/field.ts index 4c60d4b7dbd..8af02bc6d15 100644 --- a/packages/core/src/state/field.ts +++ b/packages/core/src/state/field.ts @@ -145,11 +145,12 @@ export const FieldState = createStateModel( if (!isValid(draft.props)) { draft.props = prevState.props } - - if (draft.validating === true) { - draft.loading = true - } else if (draft.validating === false) { - draft.loading = false + if (draft.validating !== prevState.validating) { + if (draft.validating === true) { + draft.loading = true + } else if (draft.validating === false) { + draft.loading = false + } } // 以下几种情况清理错误和警告信息 // 1. 字段设置为不可编辑 diff --git a/packages/next/src/fields/password.tsx b/packages/next/src/fields/password.tsx index 018287bd43b..496e08c7e5b 100644 --- a/packages/next/src/fields/password.tsx +++ b/packages/next/src/fields/password.tsx @@ -28,14 +28,14 @@ const Password = styled( } } - onChangeHandler = (value, e) => { + onChangeHandler = (value) => { this.setState( { value }, () => { if (this.props.onChange) { - this.props.onChange(value, e) + this.props.onChange(value,value) } } ) diff --git a/packages/next/src/shared.ts b/packages/next/src/shared.ts index 5d7761ae55a..ad04287e8e8 100644 --- a/packages/next/src/shared.ts +++ b/packages/next/src/shared.ts @@ -36,7 +36,7 @@ export const acceptEnum = (component: React.JSXElementConstructor) => { export const normalizeCol = ( col: { span: number; offset?: number } | number, - defaultValue: { span: number } + defaultValue?: { span: number } ): { span: number; offset?: number } => { if (!col) { return defaultValue diff --git a/packages/react-schema-renderer/src/shared/connect.ts b/packages/react-schema-renderer/src/shared/connect.ts index 159fcac127c..625ab95f0c9 100644 --- a/packages/react-schema-renderer/src/shared/connect.ts +++ b/packages/react-schema-renderer/src/shared/connect.ts @@ -32,17 +32,20 @@ const bindEffects = ( effect: ISchema['x-effect'], notify: (type: string, payload?: any) => void ): any => { - each(effect(notify, { ...props }), (event, key) => { - const prevEvent = key === 'onChange' ? props[key] : undefined - props[key] = (...args: any[]) => { - if (isFn(prevEvent)) { - prevEvent(...args) - } - if (isFn(event)) { - return event(...args) + each( + effect((type, payload) => notify(type, { payload }), { ...props }), + (event, key) => { + const prevEvent = key === 'onChange' ? props[key] : undefined + props[key] = (...args: any[]) => { + if (isFn(prevEvent)) { + prevEvent(...args) + } + if (isFn(event)) { + return event(...args) + } } } - }) + ) return props } @@ -92,7 +95,6 @@ export const connect = (options?: IConnectOptions) => { } const extendsEffect = schema.getExtendsEffect() - if (isFn(extendsEffect)) { componentProps = bindEffects(componentProps, extendsEffect, form.notify) } @@ -104,8 +106,8 @@ export const connect = (options?: IConnectOptions) => { } } - if (isArr(schema.enum) && !componentProps.dataSource) { - componentProps.dataSource = createEnum(schema.enum) + if (isArr((props as ISchema).enum) && !componentProps.dataSource) { + componentProps.dataSource = createEnum((props as ISchema).enum) } if (componentProps.editable !== undefined) { diff --git a/packages/react/src/hooks/useField.ts b/packages/react/src/hooks/useField.ts index 12df9431f60..767d167d991 100644 --- a/packages/react/src/hooks/useField.ts +++ b/packages/react/src/hooks/useField.ts @@ -7,7 +7,9 @@ import { useForceUpdate } from './useForceUpdate' import { IFieldHook } from '../types' import FormContext from '../context' -export const useField = (options: IFieldStateProps): IFieldHook => { +export const useField = ( + options: IFieldStateProps & { triggerType?: 'onChange' | 'onBlur' } +): IFieldHook => { const forceUpdate = useForceUpdate() const dirty = useDirty(options, ['props', 'rules', 'required', 'editable']) const ref = useRef<{ field: IField; unmounted: boolean }>({ @@ -18,7 +20,7 @@ export const useField = (options: IFieldStateProps): IFieldHook => { if (!form) { throw new Error('Form object cannot be found from context.') } - useMemo(() => { + const mutators = useMemo(() => { let initialized = false ref.current.field = form.registerField(options) ref.current.field.subscribe(() => { @@ -26,6 +28,11 @@ export const useField = (options: IFieldStateProps): IFieldHook => { * 同步Field状态只需要forceUpdate一下触发重新渲染,因为字段状态全部代理在uform core内部 */ if (initialized) { + if (options.triggerType === 'onChange') { + if (ref.current.field.hasChanged('value')) { + mutators.validate() + } + } raf(() => { if (ref.current.unmounted) return forceUpdate() @@ -33,6 +40,7 @@ export const useField = (options: IFieldStateProps): IFieldHook => { } }) initialized = true + return form.createMutators(ref.current.field) }, []) useEffect(() => { @@ -68,7 +76,7 @@ export const useField = (options: IFieldStateProps): IFieldHook => { ...state, errors: state.errors.join(', ') }, - mutators: form.createMutators(ref.current.field), + mutators, props: state.props } } From fc516fbcec5bf1910d40a2b90774078b7e30bb9e Mon Sep 17 00:00:00 2001 From: Janry Date: Tue, 29 Oct 2019 21:13:50 +0800 Subject: [PATCH 07/99] feat(@uform/react): support filter changed (#353) --- docs/Examples/next/Relations.md | 55 +++++++----- packages/core/src/index.ts | 64 +++++++------- packages/core/src/shared/graph.ts | 6 +- packages/core/src/shared/lifecycle.ts | 40 ++------- packages/core/src/shared/model.ts | 83 +++++++++++-------- packages/core/src/shared/subscrible.ts | 44 ---------- packages/core/src/types.ts | 23 ++--- .../src/shared/connect.ts | 15 +--- .../react/src/components/FormProvider.tsx | 2 +- packages/react/src/components/FormSpy.tsx | 9 +- packages/react/src/context.ts | 2 +- packages/react/src/hooks/useField.ts | 15 ++-- packages/react/src/hooks/useForm.ts | 3 - packages/react/src/index.ts | 4 +- packages/react/src/shared.ts | 32 ++++++- packages/react/src/types.ts | 4 +- packages/shared/src/broadcast.ts | 83 ------------------- packages/shared/src/index.ts | 2 +- packages/shared/src/subscribable.ts | 47 +++++++++++ packages/shared/src/types.ts | 7 ++ 20 files changed, 245 insertions(+), 295 deletions(-) delete mode 100644 packages/core/src/shared/subscrible.ts delete mode 100644 packages/shared/src/broadcast.ts create mode 100644 packages/shared/src/subscribable.ts diff --git a/docs/Examples/next/Relations.md b/docs/Examples/next/Relations.md index 3e04127cdbc..de06d314bc6 100644 --- a/docs/Examples/next/Relations.md +++ b/docs/Examples/next/Relations.md @@ -39,7 +39,8 @@ import { FormCard, FormPath, FormBlock, - FormLayout + FormLayout, + filterChanged } from '@uform/next' import { filter, combineLatest, map, debounceTime } from 'rxjs/operators' import { Button } from '@alifd/next' @@ -63,31 +64,41 @@ const App = () => { } }) }) - $('onFieldValueChange', 'aa').subscribe(fieldState => { - setFieldState('bb', state => { - state.visible = !fieldState.value + $('onFieldChange', '*(aa,bb)') + .pipe(filterChanged('value')) + .subscribe(fieldState => { + console.log('aa或者bb发生变化了') }) - }) - $('onFieldValueChange', 'cc').subscribe(fieldState => { - setFieldState('dd', state => { - state.visible = !fieldState.value + $('onFieldChange', 'aa') + .pipe(filterChanged('value')) + .subscribe(fieldState => { + setFieldState('bb', state => { + state.visible = !fieldState.value + }) }) - setFieldState('gg', state => { - if (fieldState.value) { - state.value = 'aaaa' - state.props.enum = [ - { label: 'aaaa', value: 'aaaa', extra: ['x1', 'x2', 'x3'] }, - { label: 'bbbb', value: 'bbbb', extra: ['x4', 'x5', 'x6'] }, - { label: 'cccc', value: 'cccc', extra: ['x7', 'x8', 'x9'] } - ] - } else { - state.value = '123333' - state.props.enum = ['123333', '333333'] - } + $('onFieldChange', 'cc') + .pipe(filterChanged('value')) + .subscribe(fieldState => { + setFieldState('dd', state => { + state.visible = !fieldState.value + }) + setFieldState('gg', state => { + if (fieldState.value) { + state.value = 'aaaa' + state.props.enum = [ + { label: 'aaaa', value: 'aaaa', extra: ['x1', 'x2', 'x3'] }, + { label: 'bbbb', value: 'bbbb', extra: ['x4', 'x5', 'x6'] }, + { label: 'cccc', value: 'cccc', extra: ['x7', 'x8', 'x9'] } + ] + } else { + state.value = '123333' + state.props.enum = ['123333', '333333'] + } + }) }) - }) - $('onFieldValueChange', 'gg') + $('onFieldChange', 'gg') .pipe( + filterChanged('value'), combineLatest($('onChangeOption')), map(([fieldState, { payload: option }]) => { return { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d599b539e56..ce29957fd20 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -45,14 +45,14 @@ export * from './types' export const createForm = (options: IFormCreatorOptions = {}): IForm => { function onGraphChange({ type, payload }) { - heart.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE, graph) + heart.publish(LifeCycleTypes.ON_FORM_GRAPH_CHANGE, graph) if (type === 'GRAPH_NODE_WILL_UNMOUNT') { validator.unregister(payload.path.toString()) } } function onFormChange(published: IFormState) { - heart.notify(LifeCycleTypes.ON_FORM_CHANGE, state) + heart.publish(LifeCycleTypes.ON_FORM_CHANGE, state) const valuesChanged = state.hasChanged('values') const initialValuesChanged = state.hasChanged('initialValues') const unmountedChanged = state.hasChanged('unmounted') @@ -118,10 +118,10 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { }) }) if (valuesChanged) { - heart.notify(LifeCycleTypes.ON_FORM_VALUES_CHANGE, state) + heart.publish(LifeCycleTypes.ON_FORM_VALUES_CHANGE, state) } if (initialValuesChanged) { - heart.notify(LifeCycleTypes.ON_FORM_INITIAL_VALUES_CHANGE, state) + heart.publish(LifeCycleTypes.ON_FORM_INITIAL_VALUES_CHANGE, state) } } @@ -136,13 +136,13 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { } if (unmountedChanged && published.unmounted) { - heart.notify(LifeCycleTypes.ON_FORM_UNMOUNT, state) + heart.publish(LifeCycleTypes.ON_FORM_UNMOUNT, state) } if (mountedChanged && published.mounted) { - heart.notify(LifeCycleTypes.ON_FORM_MOUNT, state) + heart.publish(LifeCycleTypes.ON_FORM_MOUNT, state) } if (initializedChanged) { - heart.notify(LifeCycleTypes.ON_FORM_INIT, state) + heart.publish(LifeCycleTypes.ON_FORM_INIT, state) } } @@ -158,7 +158,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { const warningsChanged = field.hasChanged('warnings') const errorsChanges = field.hasChanged('errors') if (initializedChanged) { - heart.notify(LifeCycleTypes.ON_FIELD_INIT, field) + heart.publish(LifeCycleTypes.ON_FIELD_INIT, field) const isEmptyValue = !isValid(published.value) const isEmptyInitialValue = !isValid(published.initialValue) if (isEmptyValue || isEmptyInitialValue) { @@ -196,15 +196,15 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { } } if (mountedChanged && published.mounted) { - heart.notify(LifeCycleTypes.ON_FIELD_MOUNT, field) + heart.publish(LifeCycleTypes.ON_FIELD_MOUNT, field) } if (valueChanged) { setFormValuesIn(path, published.value) - heart.notify(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, field) + heart.publish(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, field) } if (initialValueChanged) { setFormInitialValuesIn(path, published.initialValue) - heart.notify(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, field) + heart.publish(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, field) } if (errorsChanges) { @@ -214,7 +214,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { if (warningsChanged) { syncFormMessages('warnings', published.name, published.warnings) } - heart.notify(LifeCycleTypes.ON_FIELD_CHANGE, field) + heart.publish(LifeCycleTypes.ON_FIELD_CHANGE, field) if (!(!env.shadowStage || env.leadingStage)) { return false } @@ -230,7 +230,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { const initializedChnaged = field.hasChanged('initialized') if (initializedChnaged) { - heart.notify(LifeCycleTypes.ON_FIELD_INIT, field) + heart.publish(LifeCycleTypes.ON_FIELD_INIT, field) } if (visibleChanged) { @@ -257,9 +257,9 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { }) } if (mountedChanged && published.mounted) { - heart.notify(LifeCycleTypes.ON_FIELD_MOUNT, field) + heart.publish(LifeCycleTypes.ON_FIELD_MOUNT, field) } - heart.notify(LifeCycleTypes.ON_FIELD_CHANGE, field) + heart.publish(LifeCycleTypes.ON_FIELD_CHANGE, field) } } @@ -326,7 +326,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { field.subscription = { notify: onFieldChange({ field, path: nodePath }) } - heart.notify(LifeCycleTypes.ON_FIELD_WILL_INIT, field) + heart.publish(LifeCycleTypes.ON_FIELD_WILL_INIT, field) field.batch(() => { field.setState((state: IFieldState) => { const formValue = getFormValuesIn(dataPath) @@ -490,8 +490,8 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { state.value = values[0] state.values = values }) - heart.notify(LifeCycleTypes.ON_FIELD_INPUT_CHANGE, field) - heart.notify(LifeCycleTypes.ON_FORM_INPUT_CHANGE, state) + heart.publish(LifeCycleTypes.ON_FIELD_INPUT_CHANGE, field) + heart.publish(LifeCycleTypes.ON_FORM_INPUT_CHANGE, state) } function removeValue(key: string | number) { @@ -502,10 +502,10 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { state.values = [] }, true) deleteFormValuesIn(key ? FormPath.parse(name).concat(key) : name) - heart.notify(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, field) - heart.notify(LifeCycleTypes.ON_FIELD_INPUT_CHANGE, field) - heart.notify(LifeCycleTypes.ON_FORM_INPUT_CHANGE, state) - heart.notify(LifeCycleTypes.ON_FIELD_CHANGE, field) + heart.publish(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, field) + heart.publish(LifeCycleTypes.ON_FIELD_INPUT_CHANGE, field) + heart.publish(LifeCycleTypes.ON_FORM_INPUT_CHANGE, state) + heart.publish(LifeCycleTypes.ON_FIELD_CHANGE, field) } function getValue() { @@ -683,7 +683,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { state.setState(state => { state.submitting = true }) - heart.notify(LifeCycleTypes.ON_FORM_SUBMIT_START, state) + heart.publish(LifeCycleTypes.ON_FORM_SUBMIT_START, state) env.submittingTask = validate() .then(validated => { const { errors } = validated @@ -691,7 +691,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { state.setState(state => { state.submitting = false }) - heart.notify(LifeCycleTypes.ON_FORM_SUBMIT_END, state) + heart.publish(LifeCycleTypes.ON_FORM_SUBMIT_END, state) return Promise.reject(errors) } if (isFn(onSubmit)) { @@ -708,7 +708,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { state.setState(state => { state.submitting = false }) - heart.notify(LifeCycleTypes.ON_FORM_SUBMIT_END, state) + heart.publish(LifeCycleTypes.ON_FORM_SUBMIT_END, state) if (errors.length) { return Promise.reject(errors) } @@ -735,7 +735,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { }, 60) } - heart.notify(LifeCycleTypes.ON_FORM_VALIDATE_START, state) + heart.publish(LifeCycleTypes.ON_FORM_VALIDATE_START, state) return validator.validate(path, opts).then(payload => { clearTimeout(env.validateTimer) state.setState(state => { @@ -744,7 +744,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { if (isFn(options.onValidateFailed)) { options.onValidateFailed(payload) } - heart.notify(LifeCycleTypes.ON_FORM_VALIDATE_END, state) + heart.publish(LifeCycleTypes.ON_FORM_VALIDATE_END, state) return payload }) } @@ -943,13 +943,13 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { 'Please use the getFieldInitialValue.' ), subscribe: (callback?: FormHeartSubscriber) => { - heart.subscribe(callback) + return heart.subscribe(callback) }, - unsubscribe: (callback?: FormHeartSubscriber) => { - heart.unsubscribe(callback) + unsubscribe: (id: number) => { + heart.unsubscribe(id) }, notify: (type: string, payload: T) => { - heart.notify(type, payload) + heart.publish(type, payload) } } const heart = new FormHeart({ ...options, context: formApi }) @@ -963,7 +963,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { removeNodes: {}, submittingTask: undefined } - heart.notify(LifeCycleTypes.ON_FORM_WILL_INIT, state) + heart.publish(LifeCycleTypes.ON_FORM_WILL_INIT, state) state.subscription = { notify: onFormChange } diff --git a/packages/core/src/shared/graph.ts b/packages/core/src/shared/graph.ts index bff3405e0a6..bf01891057a 100644 --- a/packages/core/src/shared/graph.ts +++ b/packages/core/src/shared/graph.ts @@ -4,9 +4,9 @@ import { map, isFn, FormPath, - FormPathPattern + FormPathPattern, + Subscribable } from '@uform/shared' -import { Subscrible } from './subscrible' import { FormGraphNodeRef, FormGraphMatcher, @@ -14,7 +14,7 @@ import { FormGraphProps } from '../types' -export class FormGraph extends Subscrible<{ +export class FormGraph extends Subscribable<{ type: string payload: FormGraphNodeRef }> { diff --git a/packages/core/src/shared/lifecycle.ts b/packages/core/src/shared/lifecycle.ts index 0f899bd0d9a..136fe6eb350 100644 --- a/packages/core/src/shared/lifecycle.ts +++ b/packages/core/src/shared/lifecycle.ts @@ -1,9 +1,5 @@ -import { isFn, isStr, isArr, isObj, each } from '@uform/shared' -import { - FormLifeCyclePayload, - FormLifeCycleHandler, - FormHeartSubscriber -} from '../types' +import { isFn, isStr, isArr, isObj, each, Subscribable } from '@uform/shared' +import { FormLifeCyclePayload, FormLifeCycleHandler } from '../types' export class FormLifeCycle { private listener: FormLifeCyclePayload @@ -46,13 +42,11 @@ export class FormLifeCycle { } } -export class FormHeart { +export class FormHeart extends Subscribable { private lifecycles: FormLifeCycle[] private context: Context - private subscribers: FormHeartSubscriber[] - constructor({ lifecycles, context @@ -60,8 +54,8 @@ export class FormHeart { lifecycles?: FormLifeCycle[] context?: Context }) { + super() this.lifecycles = this.buildLifeCycles(lifecycles || []) - this.subscribers = [] this.context = context } @@ -81,32 +75,14 @@ export class FormHeart { }, []) } - unsubscribe = (callback?: FormHeartSubscriber) => { - if (isFn(callback)) { - this.subscribers = this.subscribers.filter( - fn => fn.toString() !== callback.toString() - ) - } else { - this.subscribers = [] - } - } - - subscribe = (callback?: FormHeartSubscriber) => { - if ( - isFn(callback) && - !this.subscribers.some(fn => fn.toString() === callback.toString()) - ) { - this.subscribers.push(callback) - } - } - - notify = (type: any, payload: P, context?: C) => { + publish = (type: any, payload: P, context?: C) => { if (isStr(type)) { this.lifecycles.forEach(lifecycle => { lifecycle.notify(type, payload, context || this.context) }) - this.subscribers.forEach(callback => { - callback({ type, payload }) + this.notify({ + type, + payload }) } } diff --git a/packages/core/src/shared/model.ts b/packages/core/src/shared/model.ts index 939e7535876..6508c1c385e 100644 --- a/packages/core/src/shared/model.ts +++ b/packages/core/src/shared/model.ts @@ -1,13 +1,19 @@ -import { clone, isEqual, isFn, each, globalThisPolyfill } from '@uform/shared' +import { + clone, + isEqual, + isFn, + each, + globalThisPolyfill, + Subscribable +} from '@uform/shared' import produce, { Draft } from 'immer' -import { Subscrible } from './subscrible' import { IStateModelFactory, StateDirtyMap, IModel, StateModel } from '../types' const hasProxy = !!globalThisPolyfill.Proxy export const createStateModel = ( Factory: IStateModelFactory ) => { - return class Model extends Subscrible + return class Model extends Subscribable implements IModel { public state: State & { displayName?: string } public props: Props & @@ -16,8 +22,10 @@ export const createStateModel = ( } public displayName?: string public dirtyNum: number - public dirtyMap: StateDirtyMap + public dirtys: StateDirtyMap + public persistDirtys: StateDirtyMap public batching: boolean + public processing: boolean public controller: StateModel constructor(defaultProps: DefaultProps) { @@ -27,7 +35,8 @@ export const createStateModel = ( ...Factory.defaultProps, ...defaultProps } - this.dirtyMap = {} + this.dirtys = {} + this.persistDirtys = {} this.dirtyNum = 0 this.batching = false this.controller = new Factory(this.state, this.props) @@ -43,7 +52,7 @@ export const createStateModel = ( if (this.dirtyNum > 0) { this.notify(this.getState()) } - this.dirtyMap = {} + this.dirtys = {} this.dirtyNum = 0 this.batching = false } @@ -89,8 +98,12 @@ export const createStateModel = ( if (!hasProxy || this.props.useDirty) { const draft = this.getState() if (!this.batching) { + this.dirtys = {} this.dirtyNum = 0 - this.dirtyMap = {} + } + if (!this.processing) { + this.persistDirtys = {} + this.processing = true } callback(draft) if (isFn(this.controller.computeState)) { @@ -103,28 +116,28 @@ export const createStateModel = ( (value, key) => { if (!isEqual(value, draft[key])) { this.state[key] = draft[key] - this.dirtyMap[key] = true + this.dirtys[key] = true + this.persistDirtys[key] = true this.dirtyNum++ } } ) if (isFn(this.controller.dirtyCheck)) { - const result = this.controller.dirtyCheck(this.dirtyMap) + const result = this.controller.dirtyCheck(this.dirtys) if (result !== undefined) { - Object.assign(this.dirtyMap, result) + Object.assign(this.dirtys, result) } } if (this.dirtyNum > 0 && !silent) { if (this.batching) return this.notify(this.getState()) - - this.dirtyMap = {} + this.dirtys = {} this.dirtyNum = 0 } } else { if (!this.batching) { + this.dirtys = {} this.dirtyNum = 0 - this.dirtyMap = {} } //用proxy解决脏检查计算属性问题 this.state = produce( @@ -139,49 +152,51 @@ export const createStateModel = ( patches.forEach(({ path, op, value }) => { if (op === 'replace') { if (!isEqual(this.state[path[0]], value)) { - this.dirtyMap[path[0]] = true + this.dirtys[path[0]] = true + this.persistDirtys[path[0]] = true this.dirtyNum++ } } else { - this.dirtyMap[path[0]] = true + this.dirtys[path[0]] = true + this.persistDirtys[path[0]] = true this.dirtyNum++ } }) } ) if (isFn(this.controller.dirtyCheck)) { - const result = this.controller.dirtyCheck(this.dirtyMap) + const result = this.controller.dirtyCheck(this.dirtys) if (result !== undefined) { - Object.assign(this.dirtyMap, result) + Object.assign(this.dirtys, result) } } if (this.dirtyNum > 0 && !silent) { if (this.batching) return this.notify(this.getState()) - this.dirtyMap = {} + this.dirtys = {} this.dirtyNum = 0 + //1. onFieldChange内的setFormValuesIn中不希望重置当前字段的dirtymap,如果不重置就会死循环 + //2. 自己监听自己,自己修改自己的状态,希望触发onFieldChange } } } } - + /** + * 当前操作的变化情况 + */ hasChanged = (key?: string) => - key ? this.dirtyMap[key] === true : this.dirtyNum > 0 + key ? this.dirtys[key] === true : this.dirtyNum > 0 + /** + * + *在一组操作过程中的变化情况 + */ + hasChangedInSequence = (key?: string) => + key + ? this.persistDirtys[key] + : Object.keys(this.persistDirtys || {}).length > 0 - getChanged = () => { - if (!hasProxy || this.props.useDirty) { - return clone(this.dirtyMap) - } else { - return this.dirtyMap - } - } + getChanged = () => this.dirtys - watch = (key: string, callback?: (dirtys: StateDirtyMap) => any) => { - if (this.hasChanged(key)) { - if (isFn(callback)) { - callback(this.getChanged()) - } - } - } + getChangedInSequence = () => this.persistDirtys } } diff --git a/packages/core/src/shared/subscrible.ts b/packages/core/src/shared/subscrible.ts deleted file mode 100644 index c8c49c8daef..00000000000 --- a/packages/core/src/shared/subscrible.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { isFn, each } from '@uform/shared' -import { Subscriber, Subscription } from '../types' - -export class Subscrible { - subscribers: Subscriber[] = [] - - subscription: Subscription - - subscribe = (callback?: Subscriber) => { - if ( - isFn(callback) && - !this.subscribers.some(fn => fn.toString() === callback.toString()) - ) { - this.subscribers.push(callback) - } - } - - unsubscribe = (callback?: Subscriber) => { - if (isFn(callback)) { - this.subscribers = this.subscribers.filter(fn => { - return fn.toString() !== callback.toString() - }) - } else { - this.subscribers.length = 0 - } - } - - notify = (payload?: Payload) => { - if (this.subscription) { - if (this.subscription && isFn(this.subscription.notify)) { - if (this.subscription.notify.call(this, payload) === false) { - return - } - } - } - const filter = (payload: Payload) => { - if (this.subscription && isFn(this.subscription.filter)) { - return this.subscription.filter.call(this, payload) - } - return payload - } - each(this.subscribers, callback => callback(filter(payload))) - } -} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 3927007b450..dafabe57320 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,8 +1,7 @@ -import { FormPath, FormPathPattern, isFn } from '@uform/shared' +import { FormPath, FormPathPattern, isFn, Subscribable } from '@uform/shared' import { ValidatePatternRules, ValidateNodeResult } from '@uform/validator' import { FormLifeCycle } from './shared/lifecycle' import { Draft } from 'immer' -import { Subscrible } from './shared/subscrible' export type FormLifeCycleHandler = (payload: T, context: any) => void @@ -265,21 +264,15 @@ export interface IMutators { exist(index?: number | string): boolean } -export type Subscriber = (payload: S) => void - -export interface Subscription { - notify?: (payload: S) => void | boolean - filter?: (payload: S) => any -} - -export interface IModel extends Subscrible { +export interface IModel extends Subscribable { state: S props: P displayName?: string dirtyNum: number - dirtyMap: StateDirtyMap - subscribers: Subscriber[] + dirtys: StateDirtyMap + persistDirtys: StateDirtyMap batching: boolean + processing: boolean controller: StateModel batch: (callback?: () => void) => void getState: (callback?: (state: S) => any) => any @@ -287,7 +280,9 @@ export interface IModel extends Subscrible { unsafe_getSourceState: (callback?: (state: S) => any) => any unsafe_setSourceState: (callback?: (state: S) => void) => void hasChanged: (key?: string) => boolean + hasChangedInSequence: (key?: string) => boolean getChanged: () => StateDirtyMap + getChangedInSequence: (key?: string) => StateDirtyMap } export type IField = IModel @@ -319,8 +314,8 @@ export interface IForm { createMutators(field: IField): IMutators getFormGraph(): IFormGraph setFormGraph(graph: IFormGraph): void - subscribe(callback?: FormHeartSubscriber): void - unsubscribe(callback?: FormHeartSubscriber): void + subscribe(callback?: FormHeartSubscriber): number + unsubscribe(id: number): void notify: (type: string, payload?: T) => void setFieldValue(path?: FormPathPattern, value?: any): void getFieldValue(path?: FormPathPattern): any diff --git a/packages/react-schema-renderer/src/shared/connect.ts b/packages/react-schema-renderer/src/shared/connect.ts index 625ab95f0c9..d2958e0c67a 100644 --- a/packages/react-schema-renderer/src/shared/connect.ts +++ b/packages/react-schema-renderer/src/shared/connect.ts @@ -6,6 +6,7 @@ import { ISchemaFieldComponentProps, IConnectProps } from '../types' +import { Schema } from './schema' const createEnum = (enums: any) => { if (isArr(enums)) { @@ -57,19 +58,11 @@ export const connect = (options?: IConnectOptions) => { } return (Component: React.JSXElementConstructor) => { return (fieldProps: ISchemaFieldComponentProps) => { - const { - value, - name, - mutators, - form, - schema, - editable, - props - } = fieldProps + const { value, name, mutators, form, editable, props } = fieldProps + const schema = new Schema(props) let componentProps: IConnectProps = { ...options.defaultProps, - ...props['x-props'], - ...props['x-component-props'], + ...schema.getExtendsComponentProps(), [options.valueName]: value, [options.eventName]: (event: any, ...args: any[]) => { mutators.change( diff --git a/packages/react/src/components/FormProvider.tsx b/packages/react/src/components/FormProvider.tsx index ac73cc3cbdd..fdb75ccb2b9 100644 --- a/packages/react/src/components/FormProvider.tsx +++ b/packages/react/src/components/FormProvider.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react' import { BroadcastContext } from '../context' -import { Broadcast } from '@uform/shared' +import { Broadcast } from '../shared' const { Provider } = BroadcastContext diff --git a/packages/react/src/components/FormSpy.tsx b/packages/react/src/components/FormSpy.tsx index 444ac63cdbd..396ef2f4bfd 100644 --- a/packages/react/src/components/FormSpy.tsx +++ b/packages/react/src/components/FormSpy.tsx @@ -16,6 +16,7 @@ export const FormSpy: React.FunctionComponent = props => { const broadcast = useContext(BroadcastContext) const form = useContext(FormContext) const initializedRef = useRef(false) + const subscriberId = useRef() const [type, setType] = useState(LifeCycleTypes.ON_FORM_INIT) const [state, dispatch] = useReducer( (state, action) => props.reducer(state, action, form), @@ -40,18 +41,18 @@ export const FormSpy: React.FunctionComponent = props => { useMemo(() => { initializedRef.current = true if (form) { - form.subscribe(subscriber) + subscriberId.current = form.subscribe(subscriber) } else if (broadcast) { - broadcast.subscribe(subscriber) + subscriberId.current = broadcast.subscribe(subscriber) } initializedRef.current = false }, []) useEffect(() => { return () => { if (form) { - form.unsubscribe(subscriber) + form.unsubscribe(subscriberId.current) } else if (broadcast) { - broadcast.unsubscribe(subscriber) + broadcast.unsubscribe(subscriberId.current) } } }, []) diff --git a/packages/react/src/context.ts b/packages/react/src/context.ts index 4097188b652..a90a7c3f514 100644 --- a/packages/react/src/context.ts +++ b/packages/react/src/context.ts @@ -1,5 +1,5 @@ import { createContext } from 'react' -import { Broadcast } from '@uform/shared' +import { Broadcast } from './shared' import { IForm } from '@uform/core' export const BroadcastContext = createContext(null) diff --git a/packages/react/src/hooks/useField.ts b/packages/react/src/hooks/useField.ts index 767d167d991..7340924b219 100644 --- a/packages/react/src/hooks/useField.ts +++ b/packages/react/src/hooks/useField.ts @@ -12,9 +12,14 @@ export const useField = ( ): IFieldHook => { const forceUpdate = useForceUpdate() const dirty = useDirty(options, ['props', 'rules', 'required', 'editable']) - const ref = useRef<{ field: IField; unmounted: boolean }>({ + const ref = useRef<{ + field: IField + unmounted: boolean + subscriberId: number + }>({ field: null, - unmounted: false + unmounted: false, + subscriberId: null }) const form = useContext(FormContext) if (!form) { @@ -23,13 +28,13 @@ export const useField = ( const mutators = useMemo(() => { let initialized = false ref.current.field = form.registerField(options) - ref.current.field.subscribe(() => { + ref.current.subscriberId = ref.current.field.subscribe(() => { /** * 同步Field状态只需要forceUpdate一下触发重新渲染,因为字段状态全部代理在uform core内部 */ if (initialized) { if (options.triggerType === 'onChange') { - if (ref.current.field.hasChanged('value')) { + if (ref.current.field.hasChangedInSequence('value')) { mutators.validate() } } @@ -62,7 +67,7 @@ export const useField = ( ref.current.unmounted = false return () => { ref.current.unmounted = true - ref.current.field.unsubscribe() + ref.current.field.unsubscribe(ref.current.subscriberId) ref.current.field.setState((state: IFieldState) => { state.unmounted = true }) diff --git a/packages/react/src/hooks/useForm.ts b/packages/react/src/hooks/useForm.ts index 3958e2aea7e..65d9a9d50b8 100644 --- a/packages/react/src/hooks/useForm.ts +++ b/packages/react/src/hooks/useForm.ts @@ -111,9 +111,6 @@ export const useForm = < ...form, dispatch: form.notify } - if (broadcast) { - broadcast.setContext(actions) - } implementActions(actions) } ) diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 7427cdcb193..1afb239166a 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -2,7 +2,8 @@ import { FormEffectHooks, createEffectHook, createFormActions, - createAsyncFormActions + createAsyncFormActions, + filterChanged } from './shared' export * from '@uform/core' export * from './components/Form' @@ -17,6 +18,7 @@ export * from './hooks/useVirtualField' export * from './types' export { + filterChanged, FormEffectHooks, createEffectHook, createFormActions, diff --git a/packages/react/src/shared.ts b/packages/react/src/shared.ts index 3bc79a02713..a8208f3ba83 100644 --- a/packages/react/src/shared.ts +++ b/packages/react/src/shared.ts @@ -1,7 +1,13 @@ -import { isFn, FormPath, globalThisPolyfill } from '@uform/shared' +import { + isFn, + isEqual, + FormPath, + globalThisPolyfill, + Subscribable +} from '@uform/shared' import { IFormEffect, IFormActions, IFormAsyncActions } from './types' import { Observable } from 'rxjs/internal/Observable' -import { filter } from 'rxjs/internal/operators' +import { filter } from 'rxjs/internal/operators/filter' import { createActions, createAsyncActions } from 'react-eva' import { LifeCycleTypes, @@ -36,6 +42,17 @@ export const createFormActions = (): IFormActions => { ) as IFormActions } +export const filterChanged = (key?: string) => { + const caches = {} + return filter(x => { + if (!x) return true + const old = caches[x.name] || {} + const result = key ? isEqual(x[key], old[key]) : isEqual(x, old) + caches[x.name] = x + return !result + }) +} + export const createAsyncFormActions = (): IFormAsyncActions => createAsyncActions( 'submit', @@ -144,6 +161,17 @@ const getScheduler = () => { } } +export class Broadcast extends Subscribable { + context: any + + setContext(context: any) { + this.context = context + } + getContext() { + return this.context + } +} + export const env = { effectStart: false, effectSelector: null, diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 2a1ac48ce5e..03e35bf4865 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -142,7 +142,7 @@ export interface IFormActions { getFormGraph(): IFormGraph setFormGraph(graph: IFormGraph): void subscribe(callback?: FormHeartSubscriber): void - unsubscribe(callback?: FormHeartSubscriber): void + unsubscribe(id: number): void notify: (type: string, payload: T) => void dispatch: (type: string, payload: T) => void setFieldValue(path?: FormPathPattern, value?: any): void @@ -171,7 +171,7 @@ export interface IFormAsyncActions { getFormGraph(): Promise setFormGraph(graph: IFormGraph): Promise subscribe(callback?: FormHeartSubscriber): Promise - unsubscribe(callback?: FormHeartSubscriber): Promise + unsubscribe(id: number): Promise notify: (type: string, payload: T) => Promise dispatch: (type: string, payload: T) => void setFieldValue(path?: FormPathPattern, value?: any): Promise diff --git a/packages/shared/src/broadcast.ts b/packages/shared/src/broadcast.ts deleted file mode 100644 index d96454511bf..00000000000 --- a/packages/shared/src/broadcast.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { each } from './array' -import { isFn } from './types' - -type Subscriber = (notification: N) => void - -type Filter = (payload: P, subscription: S) => any - -const noop = () => undefined - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IBroadcast extends Broadcast {} - -export class Broadcast { - private entries = [] - private buffer = [] - private length: number - private context: any - - public subscribe(subscriber: Subscriber, subscription?: any) { - if (!isFn(subscriber)) { - return noop - } - const index = this.entries.length - this.entries.push({ - subscriber, - subscription - }) - this.flushBuffer(this.entries[index]) - return () => { - this.entries.splice(index, 1) - } - } - - public setContext(context?: any) { - this.context = context - } - - public getContext() { - return this.context - } - - public unsubscribe(subscriber?: Subscriber) { - if (subscriber) { - this.entries = this.entries.filter(suber => { - return suber.subscriber !== subscriber - }) - } else { - this.entries.length = 0 - this.buffer.length = 0 - } - } - - public flushBuffer({ subscriber, subscription }) { - each(this.buffer, ({ payload, filter }) => { - if (isFn(filter)) { - const notification = filter(payload, subscription) - if (notification !== undefined) { - subscriber.call(this.context, notification) - } - } else { - subscriber.call(this.context, payload, subscription) - } - }) - } - - public notify(payload: Payload, filter?: Filter) { - if (this.length === 0) { - this.buffer.push({ payload, filter }) - return - } - each(this.entries, ({ subscriber, subscription }) => { - if (isFn(filter)) { - const notification = filter(payload, subscription) - if (notification !== undefined) { - subscriber.call(this.context, notification) - } - } else { - subscriber.call(this.context, payload, subscription) - } - }) - this.buffer.length = 0 - } -} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 79e1ef38ad6..0ad7e891da2 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -8,5 +8,5 @@ export * from './string' export * from './global' export * from './path' export * from './deprecate' -export * from './broadcast' +export * from './subscribable' export * from './merge' diff --git a/packages/shared/src/subscribable.ts b/packages/shared/src/subscribable.ts new file mode 100644 index 00000000000..e71b1586c3b --- /dev/null +++ b/packages/shared/src/subscribable.ts @@ -0,0 +1,47 @@ +import { isFn, Subscriber, Subscription } from './types' +import { each } from './array' + +export class Subscribable { + subscribers: { + index?: number + [key: number]: Subscriber + } = { + index: 0 + } + + subscription: Subscription + + subscribe = (callback?: Subscriber): number => { + if (isFn(callback)) { + let index: number = this.subscribers.index + 1 + this.subscribers[index] = callback + this.subscribers.index++ + return index + } + } + + unsubscribe = (index: number) => { + if (this.subscribers[index]) { + delete this.subscribers[index] + } + } + + notify = (payload?: Payload) => { + if (this.subscription) { + if (this.subscription && isFn(this.subscription.notify)) { + if (this.subscription.notify.call(this, payload) === false) { + return + } + } + } + const filter = (payload: Payload) => { + if (this.subscription && isFn(this.subscription.filter)) { + return this.subscription.filter.call(this, payload) + } + return payload + } + each(this.subscribers, (callback: any) => { + if (isFn(callback)) callback(filter(payload)) + }) + } +} diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 828bb72ee5b..31a19f4d322 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -9,3 +9,10 @@ export const isBool = isType('Boolean') export const isNum = isType('Number') export const isObj = (val: unknown): val is object => typeof val === 'object' export const isRegExp = isType('RegExp') + +export type Subscriber = (payload: S) => void + +export interface Subscription { + notify?: (payload: S) => void | boolean + filter?: (payload: S) => any +} From d71235f973494181ac8d882b366bc356153f14e7 Mon Sep 17 00:00:00 2001 From: quirkyvar Date: Thu, 31 Oct 2019 18:44:27 +0800 Subject: [PATCH 08/99] [WIP]feat(core): add lifecycles unit test (#355) * feat: add lifecycles test --- packages/core/src/__tests__/lifecycle.spec.ts | 174 +++++++++++++++++- 1 file changed, 165 insertions(+), 9 deletions(-) diff --git a/packages/core/src/__tests__/lifecycle.spec.ts b/packages/core/src/__tests__/lifecycle.spec.ts index d93d33fc19d..3a886d2e004 100644 --- a/packages/core/src/__tests__/lifecycle.spec.ts +++ b/packages/core/src/__tests__/lifecycle.spec.ts @@ -1,13 +1,169 @@ -//import { FormHeart, FormLifeCycle, LifeCycleTypes } from '../shared/lifecycle' +import { FormHeart, FormLifeCycle } from '../shared/lifecycle' +import { LifeCycleTypes } from '../types' -test('create lifecycle',()=>{ - //todo -}) +describe('FormLifeCycle', () => { + test('handler',()=>{ + const cb = jest.fn() + const lifeCycle = new FormLifeCycle(cb) + const data = { hello: 'world' } + lifeCycle.notify(LifeCycleTypes.ON_FORM_INIT, data) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith({ payload: data, type: LifeCycleTypes.ON_FORM_INIT }, undefined) + lifeCycle.notify(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, data) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith({ payload: data, type: LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE }, undefined) + }) + + test('handler with context',()=>{ + const cb = jest.fn() + const lifeCycle = new FormLifeCycle(cb) + const data = { hello: 'world' } + const context = { temp: true } + lifeCycle.notify(LifeCycleTypes.ON_FORM_INIT, data, context) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith({ payload: data, type: LifeCycleTypes.ON_FORM_INIT }, context) + lifeCycle.notify(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, data, context) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith({ payload: data, type: LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE }, context) + }) + + test('type/handler',()=>{ + const cb1 = jest.fn() + const cb2 = jest.fn() + const lifeCycle1= new FormLifeCycle(LifeCycleTypes.ON_FORM_INIT, cb1); + const lifeCycle2= new FormLifeCycle(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, cb2); + const data = { hello: 'world' } + lifeCycle1.notify(LifeCycleTypes.ON_FORM_INIT, data) + expect(cb1).toBeCalledTimes(1) + expect(cb2).toBeCalledTimes(0) + expect(cb1).toBeCalledWith(data, undefined) + lifeCycle2.notify(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, data) + expect(cb1).toBeCalledTimes(1) + expect(cb2).toBeCalledTimes(1) + expect(cb2).toBeCalledWith(data, undefined) + }) -test('create form heart',()=>{ - //todo + test('map',()=>{ + const cb1 = jest.fn() + const cb2 = jest.fn() + const lifeCycle = new FormLifeCycle({ + [LifeCycleTypes.ON_FORM_INIT]: cb1, + [LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE]: cb2, + }) + const data = { hello: 'world' } + lifeCycle.notify(LifeCycleTypes.ON_FORM_INIT, data) + expect(cb1).toBeCalledTimes(1) + expect(cb2).toBeCalledTimes(0) + expect(cb1).toBeCalledWith(data, undefined) + lifeCycle.notify(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, data) + expect(cb1).toBeCalledTimes(1) + expect(cb2).toBeCalledTimes(1) + expect(cb2).toBeCalledWith(data, undefined) + }) }) -test('heart with lifecycle',()=>{ - //todo -}) \ No newline at end of file +describe('FormHeart', () => { + test('heart is instance of Subscribe',()=>{ + const heart = new FormHeart({}) + const cb = jest.fn() + const idx = heart.subscribe(cb) + const data = { hello: 'world' } + heart.notify(data) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(data) + heart.unsubscribe(idx) + heart.notify(data) + expect(cb).toBeCalledTimes(1) + }) + + test('lifecycles constructor handler',()=>{ + const cb = jest.fn() + const heart = new FormHeart({ + lifecycles: [ + new FormLifeCycle(cb), + ] + }) + const data = { hello: 'world' } + heart.publish(LifeCycleTypes.ON_FORM_INIT, data) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith({ payload: data, type: LifeCycleTypes.ON_FORM_INIT }, undefined) + heart.publish(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, data) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith({ payload: data, type: LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE }, undefined) + }) + + test('lifecycles constructor type/handler',()=>{ + const cb1 = jest.fn() + const cb2 = jest.fn() + const heart = new FormHeart({ + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FORM_INIT, cb1), + new FormLifeCycle(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, cb2), + ] + }) + const data = { hello: 'world' } + heart.publish(LifeCycleTypes.ON_FORM_INIT, data) + expect(cb1).toBeCalledTimes(1) + expect(cb2).toBeCalledTimes(0) + expect(cb1).toBeCalledWith(data, undefined) + heart.publish(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, data) + expect(cb1).toBeCalledTimes(1) + expect(cb2).toBeCalledTimes(1) + expect(cb2).toBeCalledWith(data, undefined) + }) + + test('lifecycles constructor map',()=>{ + const cb1 = jest.fn() + const cb2 = jest.fn() + const heart = new FormHeart({ + lifecycles: [ + new FormLifeCycle({ + [LifeCycleTypes.ON_FORM_INIT]: cb1, + [LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE]: cb2, + }), + ] + }) + const data = { hello: 'world' } + heart.publish(LifeCycleTypes.ON_FORM_INIT, data) + expect(cb1).toBeCalledTimes(1) + expect(cb2).toBeCalledTimes(0) + expect(cb1).toBeCalledWith(data, undefined) + heart.publish(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, data) + expect(cb1).toBeCalledTimes(1) + expect(cb2).toBeCalledTimes(1) + expect(cb2).toBeCalledWith(data, undefined) + }) + + test('lifecycles with constructor context',()=>{ + const cb = jest.fn() + const context = { constructor: true } + const heart = new FormHeart({ + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FORM_INIT, cb), + ], + context, + }) + const data = { hello: 'world' } + heart.publish(LifeCycleTypes.ON_FORM_INIT, data) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(data, context) + }) + + test('lifecycles with temp context',()=>{ + const cb = jest.fn() + const context = { constructor: true } + const heart = new FormHeart({ + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FORM_INIT, cb), + ], + }) + const data = { hello: 'world' } + heart.publish(LifeCycleTypes.ON_FORM_INIT, data) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(data, undefined) + + heart.publish(LifeCycleTypes.ON_FORM_INIT, data, context) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(data, context) + }) +}) From 778a0201dcba43cf20a523f4c909db4a1de31a9e Mon Sep 17 00:00:00 2001 From: quirkyvar Date: Fri, 1 Nov 2019 10:10:28 +0800 Subject: [PATCH 09/99] feat(core): add graph unit test (#357) * feat: add lifecycles test * feat: fix commit * feat: update graph unit test * feat: add deep remove case --- packages/core/src/__tests__/graph.spec.ts | 356 ++++++++++++++++++++-- packages/core/src/shared/graph.ts | 18 +- 2 files changed, 344 insertions(+), 30 deletions(-) diff --git a/packages/core/src/__tests__/graph.spec.ts b/packages/core/src/__tests__/graph.spec.ts index b707eb01da0..cc69ac75192 100644 --- a/packages/core/src/__tests__/graph.spec.ts +++ b/packages/core/src/__tests__/graph.spec.ts @@ -1,49 +1,357 @@ -//import { FormGraph } from '../shared/graph' +import { FormGraph } from '../shared/graph' +import { FormPathPattern, FormPath } from '@uform/shared' +import { IField, IVirtualField } from '../types' +import createForm from '../' +import { FormState } from '../state/form' -test('constructor',()=>{ - //todo +test('constructor with strategy',()=>{ + function matchStrategy( + pattern: FormPathPattern, + node: IField | IVirtualField + ) { + const matchPattern = FormPath.parse(pattern) + return node.unsafe_getSourceState( + state => matchPattern.match(state.name) || matchPattern.match(state.path) + ) + } + + const form = createForm() + const vf1 =form.registerVirtualField({ name: 'a' }) + const graph = new FormGraph({ matchStrategy }) + graph.appendNode("a", vf1) + expect(graph.select("a")).toEqual(vf1) }) -test('select',()=>{ - //todo +test('constructor without strategy',()=>{ + const form = createForm() + const vf1 =form.registerVirtualField({ name: 'a' }) + const graph = new FormGraph() + graph.appendNode(vf1.state.path, vf1) + expect(graph.select(vf1.state.path)).toEqual(vf1) }) -test('selectParent',()=>{ - //todo +test('appendNode',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf2 = form.registerVirtualField({ name: 'b' }) + const vf2Children = form.registerVirtualField({ name: 'b.b' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf2.state.path, vf2) + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf2Children.state.path, vf2Children) + // DFS + expect(graph.selectChildren("")[0]).toEqual(vf1) + expect(graph.selectChildren("")[1]).toEqual(vf1Children) + expect(graph.selectChildren("")[2]).toEqual(vf2) + expect(graph.selectChildren("")[3]).toEqual(vf2Children) + expect(graph.selectChildren(vf1.state.path)).toEqual([vf1Children]) + expect(graph.selectChildren(vf2.state.path)).toEqual([vf2Children]) + expect(graph.selectChildren(vf1Children.state.path)).toEqual([]) + expect(graph.selectChildren(vf2Children.state.path)).toEqual([]) }) -test('selectChildren',()=>{ - //todo +test('appendNode disOreder',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf2 = form.registerVirtualField({ name: 'b' }) + const graph = new FormGraph() + // 极端情况,先加子节点,再加父节点不会生效,保证上层代码正常即可 + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf2.state.path, vf2) + expect(graph.selectChildren(vf1.state.path)).toEqual([]) + expect(graph.selectChildren("")).toEqual([]) + graph.appendNode("", state) + expect(graph.selectChildren("")).toEqual([]) +}) + +test('select',()=>{ + const form = createForm() + const vf1 =form.registerVirtualField({ name: 'a' }) + const graph = new FormGraph() + graph.appendNode(vf1.state.path, vf1) + expect(graph.select(vf1.state.path)).toEqual(vf1) + expect(graph.select("b")).toEqual(undefined) +}) + +test('get',()=>{ + const form = createForm() + const vf1 =form.registerVirtualField({ name: 'a' }) + const graph = new FormGraph() + graph.appendNode(vf1.state.path, vf1) + expect(graph.get(vf1.state.path)).toEqual(vf1) + expect(graph.get("b")).toEqual(undefined) }) test('exist',()=>{ - //todo + const form = createForm() + const vf1 =form.registerVirtualField({ name: 'a' }) + const graph = new FormGraph() + graph.appendNode(vf1.state.path, vf1) + expect(graph.exist(vf1.state.path)).toEqual(true) + expect(graph.exist("b")).toEqual(false) }) -test('eachChildren',()=>{ - //todo +test('replace',()=>{ + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf2 = form.registerVirtualField({ name: 'b' }) + const graph = new FormGraph() + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf1Children.state.path, vf1Children) + expect(graph.get(vf1.state.path)).toEqual(vf1) + expect(graph.get(vf1Children.state.path)).toEqual(vf1Children) + graph.replace(vf1.state.path, vf2) + expect(graph.get(vf1.state.path)).toEqual(vf2) + // 完全打平, 更换parent不会影响子类 + expect(graph.get(vf1Children.state.path)).toEqual(vf1Children) }) -test('eachParent',()=>{ - //todo +test('remove',()=>{ + const form = createForm() + const vf1 =form.registerVirtualField({ name: 'a' }) + const graph = new FormGraph() + graph.appendNode(vf1.state.path, vf1) + expect(graph.exist(vf1.state.path)).toEqual(true) + graph.remove(vf1.state.path) + expect(graph.exist(vf1.state.path)).toEqual(false) }) -test('getLatestParent',()=>{ - //todo +test('remove deep', () => { + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf1GrandChildren = form.registerVirtualField({ name: 'a.a.a' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf1GrandChildren.state.path, vf1GrandChildren) + graph.remove(vf1.state.path) + expect(graph.exist(vf1.state.path)).toEqual(false) + expect(graph.exist(vf1Children.state.path)).toEqual(false) + expect(graph.exist(vf1GrandChildren.state.path)).toEqual(false) }) -test('appendNode',()=>{ - //todo +test('map',()=>{ + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf2 = form.registerVirtualField({ name: 'b' }) + const graph = new FormGraph() + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf2.state.path, vf2) + // nodes 是kv结构, map后也是kv结构 + const result = graph.map(node => node) + expect(result).toEqual({ a: vf1, b: vf2 }) }) -test('remove',()=>{ - //todo +test('reduce',()=>{ + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf2 = form.registerVirtualField({ name: 'b' }) + const graph = new FormGraph() + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf2.state.path, vf2) + const result = graph.reduce((buf, node) => [...buf].concat(node), []) + expect(result).toEqual([vf1, vf2]) }) -test('toJSON',()=>{ - //todo +test('selectChildren',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf2 = form.registerVirtualField({ name: 'b' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf2.state.path, vf2) + expect(graph.selectChildren("")).toEqual([vf1, vf2]) + expect(graph.selectChildren(vf1.state.path)).toEqual([]) + expect(graph.selectChildren(vf2.state.path)).toEqual([]) +}) + + +test('selectParent',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf2 = form.registerVirtualField({ name: 'b' }) + const vf2Children = form.registerVirtualField({ name: 'b.b' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf2.state.path, vf2) + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf2Children.state.path, vf2Children) + expect(graph.selectParent("")).toEqual(undefined) + expect(graph.selectParent(vf1.state.path)).toEqual(state) + expect(graph.selectParent(vf2.state.path)).toEqual(state) + expect(graph.selectParent(vf1Children.state.path)).toEqual(vf1) + expect(graph.selectParent(vf2Children.state.path)).toEqual(vf2) }) -test('fromJSON',()=>{ - //todo +test('eachChildren eacher',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf1GrandChildren = form.registerVirtualField({ name: 'a.a.a' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf1GrandChildren.state.path, vf1GrandChildren) + + // shallow mode: recursion = false + const shallowChilds = [] + graph.eachChildren((node, path) => { + shallowChilds.push({ node, path }) + }, false) + + expect(shallowChilds).toEqual([{ node: vf1, path: FormPath.getPath(vf1.state.path)}]) + + // deep mode: recursion = true + const deepChilds = [] + graph.eachChildren((node, path) => { + deepChilds.push({ node, path }) + }, true) + expect(deepChilds).toEqual([ + { node: vf1, path: FormPath.getPath(vf1.state.path)}, + { node: vf1Children, path: FormPath.getPath(vf1Children.state.path)}, + { node: vf1GrandChildren, path: FormPath.getPath(vf1GrandChildren.state.path)}, + ]) +}) + +test('eachChildren path eacher',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf1GrandChildren = form.registerVirtualField({ name: 'a.a.a' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf1GrandChildren.state.path, vf1GrandChildren) + + // deep mode: recursion = true + const deepChilds = [] + graph.eachChildren("a", (node, path) => { + deepChilds.push({ node, path }) + }, true) + expect(deepChilds).toEqual([ + { node: vf1Children, path: FormPath.getPath(vf1Children.state.path)}, + { node: vf1GrandChildren, path: FormPath.getPath(vf1GrandChildren.state.path)}, + ]) +}) + +test('eachChildren path selector eacher',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf1GrandChildren = form.registerVirtualField({ name: 'a.a.a' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf1GrandChildren.state.path, vf1GrandChildren) + + // shallow mode: recursion = false + const shallowChilds = [] + graph.eachChildren("a", "a.a.a", (node, path) => { + shallowChilds.push({ node, path }) + }, true) + expect(shallowChilds).toEqual([]) + + // deep mode: recursion = true + const deepChilds = [] + graph.eachChildren("a", "a.a", (node, path) => { + deepChilds.push({ node, path }) + }, true) + expect(deepChilds).toEqual([ + { node: vf1Children, path: FormPath.getPath(vf1Children.state.path)}, + ]) +}) + +test('eachParent',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf1GrandChildren = form.registerVirtualField({ name: 'a.a.a' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf1GrandChildren.state.path, vf1GrandChildren) + const rootEacher = jest.fn() + graph.eachParent("", rootEacher) + expect(rootEacher).toBeCalledTimes(1) + expect(rootEacher).toBeCalledWith(state, FormPath.getPath("")) + + const vf1Eacher = jest.fn() + graph.eachParent(vf1.state.path, vf1Eacher) + expect(vf1Eacher).toBeCalledTimes(2) + + const vf1CEacher = jest.fn() + graph.eachParent(vf1Children.state.path, vf1CEacher) + expect(vf1CEacher).toBeCalledTimes(3) + + const vf1GEacher = jest.fn() + graph.eachParent(vf1GrandChildren.state.path, vf1GEacher) + expect(vf1GEacher).toBeCalledTimes(4) +}) + +test('getLatestParent',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf1GrandChildren = form.registerVirtualField({ name: 'a.a.a' }) + const vf2 = form.registerVirtualField({ name: 'b' }) + const vf2Children = form.registerVirtualField({ name: 'b.b' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf2.state.path, vf2) + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf1GrandChildren.state.path, vf1GrandChildren) + graph.appendNode(vf2Children.state.path, vf2Children) + expect(graph.getLatestParent("")).toEqual(undefined) + const rootPath = FormPath.getPath("") + const v1Path = FormPath.getPath(vf1.state.path) + const v2Path = FormPath.getPath(vf2.state.path) + const v1CPath = FormPath.getPath(vf1Children.state.path) + const v2CPath = FormPath.getPath(vf2Children.state.path) + const root = { + path: rootPath, + ref: { path: rootPath, children: [v1Path, v2Path] } + } + expect(graph.getLatestParent(vf1.state.path)).toEqual(root) + expect(graph.getLatestParent(vf2.state.path)).toEqual(root) + expect(graph.getLatestParent(vf1Children.state.path)).toEqual({ + path: v1Path, + ref: { path: v1Path, children: [v1CPath], parent: graph.getLatestParent(vf1.state.path).ref } + }) + expect(graph.getLatestParent(vf2Children.state.path)).toEqual({ + path: v2Path, + ref: { path: v2Path, children: [v2CPath], parent: graph.getLatestParent(vf2.state.path).ref } + }) + expect(graph.getLatestParent(vf1GrandChildren.state.path)).toEqual({ + path: v1CPath, + ref: { path: v1CPath, + children: [FormPath.getPath(vf1GrandChildren.state.path)], + parent: graph.getLatestParent(vf1Children.state.path).ref + } + }) }) \ No newline at end of file diff --git a/packages/core/src/shared/graph.ts b/packages/core/src/shared/graph.ts index bf01891057a..3c0afc93d24 100644 --- a/packages/core/src/shared/graph.ts +++ b/packages/core/src/shared/graph.ts @@ -50,9 +50,9 @@ export class FormGraph extends Subscribable<{ * @param path * @param matcher */ - select(path: FormPathPattern, matcher?: FormGraphMatcher) { + select(path: FormPathPattern, eacher?: FormGraphMatcher) { const pattern = FormPath.parse(path) - if (!matcher) { + if (!eacher) { const node = this.get(pattern) if (node) { return node @@ -65,8 +65,8 @@ export class FormGraph extends Subscribable<{ ? this.matchStrategy(pattern, node) : pattern.match(name) ) { - if (isFn(matcher)) { - const result = matcher(node, FormPath.parse(name)) + if (isFn(eacher)) { + const result = eacher(node, FormPath.parse(name)) if (result === false) { return node } @@ -82,7 +82,11 @@ export class FormGraph extends Subscribable<{ } selectParent(path: FormPathPattern) { - return this.get(FormPath.getPath(path).parent()) + const selfPath = FormPath.getPath(path) + const parentPath = FormPath.getPath(path).parent() + if (selfPath.toString() === parentPath.toString()) return undefined + + return this.get(parentPath) } selectChildren(path: FormPathPattern) { @@ -136,6 +140,7 @@ export class FormGraph extends Subscribable<{ eacher = selector selector = '*' } + const ref = this.refrences[FormPath.getPath(path).toString()] if (ref && ref.children) { return each(ref.children, path => { @@ -174,9 +179,10 @@ export class FormGraph extends Subscribable<{ getLatestParent(path: FormPathPattern) { const selfPath = FormPath.getPath(path) const parentPath = FormPath.getPath(path).parent() + if (selfPath.toString() === parentPath.toString()) return undefined if (this.refrences[parentPath.toString()]) - return { ref: this.refrences[parentPath.toString()], path: parentPath } + return { ref: this.refrences[parentPath.toString()], path: FormPath.getPath(parentPath.toString()) } return this.getLatestParent(parentPath) } From 985aca385f80f69d3c627296af6816bf17ddddbc Mon Sep 17 00:00:00 2001 From: Janry Date: Fri, 1 Nov 2019 16:42:34 +0800 Subject: [PATCH 10/99] V1 fix subscribe (#358) * refactor(antd/next): remove uncase file * fix(@uform/react): fix subscribe --- packages/antd/src/components/button.tsx | 87 ---------- .../antd/src/components/formButtonGroup.tsx | 155 ----------------- packages/next/src/components/button.tsx | 102 ------------ .../next/src/components/formButtonGroup.tsx | 157 ------------------ packages/react/src/hooks/useForm.ts | 3 + 5 files changed, 3 insertions(+), 501 deletions(-) delete mode 100644 packages/antd/src/components/button.tsx delete mode 100644 packages/antd/src/components/formButtonGroup.tsx delete mode 100644 packages/next/src/components/button.tsx delete mode 100644 packages/next/src/components/formButtonGroup.tsx diff --git a/packages/antd/src/components/button.tsx b/packages/antd/src/components/button.tsx deleted file mode 100644 index 5d07468e830..00000000000 --- a/packages/antd/src/components/button.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react' -import { FormSpy, LifeCycleTypes } from '@uform/react-schema-renderer' -import { Button } from 'antd' -import { ButtonProps } from 'antd/lib/button' -import { ISubmitProps, IResetProps } from '../types' - -export const TextButton: React.FC = props => ( - - ) - }} - - ) -} - -Submit.defaultProps = { - showLoading: true -} - -export const Reset: React.FC = ({ - children, - forceClear, - validate, - ...props -}) => { - return ( - - {({ form }) => { - return ( - - ) - }} - - ) -} diff --git a/packages/antd/src/components/formButtonGroup.tsx b/packages/antd/src/components/formButtonGroup.tsx deleted file mode 100644 index 7899c572d23..00000000000 --- a/packages/antd/src/components/formButtonGroup.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import React, { useRef } from 'react' -import { Row, Col } from 'antd' -import Sticky from 'react-stikky' -import cls from 'classnames' -import styled from 'styled-components' -import { useFormItem } from '../compat/context' -import { IFormButtonGroupProps } from '../types' - -export interface IOffset { - top: number | string - right: number | string - bottom: number | string - left: number | string -} - -const getAlign = align => { - if (align === 'start' || align === 'end') return align - if (align === 'left' || align === 'top') return 'flex-start' - if (align === 'right' || align === 'bottom') return 'flex-end' - return align -} - -const isElementInViewport = ( - rect: ClientRect, - { - offset = 0, - threshold = 0 - }: { - offset?: IOffset | number - threshold?: number - } = {} -): boolean => { - const { top, right, bottom, left, width, height } = rect - const intersection = { - t: bottom, - r: window.innerWidth - left, - b: window.innerHeight - top, - l: right - } - - const elementThreshold = { - x: threshold * width, - y: threshold * height - } - - return ( - intersection.t >= - ((offset as IOffset).top || (offset as number) + elementThreshold.y) && - intersection.r >= - ((offset as IOffset).right || (offset as number) + elementThreshold.x) && - intersection.b >= - ((offset as IOffset).bottom || (offset as number) + elementThreshold.y) && - intersection.l >= - ((offset as IOffset).left || (offset as number) + elementThreshold.x) - ) -} - -export const FormButtonGroup = styled( - (props: React.PropsWithChildren) => { - const { - span, - zIndex, - sticky, - style, - offset, - className, - children, - triggerDistance, - itemStyle - } = props - const { inline } = useFormItem() - const selfRef = useRef() - const renderChildren = () => { - return ( -
    - - - -
    - {children} -
    - - -
    -
    - ) - } - const getStickyBoundaryHandler = () => { - return () => { - if (selfRef.current && selfRef.current.parentElement) { - const container = selfRef.current.parentElement - return isElementInViewport(container.getBoundingClientRect()) - } - return true - } - } - - const content = ( -
    - {renderChildren()} -
    - ) - - if (sticky) { - return ( -
    - -
    - {content} -
    -
    -
    - ) - } - - return content - } -)` - ${(props: IFormButtonGroupProps) => - props.align ? `display:flex;justify-content: ${getAlign(props.align)}` : ''} - &.is-inline { - display: inline-block; - flex-grow: 3; - } - .button-group { - .inline { - display: inline-block; - .inline-view { - & > * { - margin-right: 10px; - margin-left: 0px; - display: inline-block; - } - & > *:last-child { - margin-right: 0 !important; - } - } - } - } -` diff --git a/packages/next/src/components/button.tsx b/packages/next/src/components/button.tsx deleted file mode 100644 index 9a3b67750b4..00000000000 --- a/packages/next/src/components/button.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react' -import { FormSpy, LifeCycleTypes } from '@uform/react-schema-renderer' -import { Button } from '@alifd/next' -import { ButtonProps } from '@alifd/next/types/button' -import { ISubmitProps, IResetProps } from '../types' -import styled from 'styled-components' - -export const TextButton: React.FC = props => ( - - ) -})` - border-radius: 50% !important; - padding: 0 !important; - min-width: 28px; - &.next-large { - min-width: 40px; - } - &.next-small { - min-width: 20px; - } - &.has-text { - .next-icon { - margin-right: 5px; - } - background: none !important; - border: none !important; - } -` - -export const Submit = ({ showLoading, onSubmit, ...props }: ISubmitProps) => { - return ( - { - switch (action.type) { - case LifeCycleTypes.ON_FORM_SUBMIT_START: - return { - ...state, - submitting: true - } - case LifeCycleTypes.ON_FORM_SUBMIT_END: - return { - ...state, - submitting: false - } - default: - return state - } - }} - > - {({ state, form }) => { - return ( - - ) - }} - - ) -} - -Submit.defaultProps = { - showLoading: true -} - -export const Reset: React.FC = ({ - children, - forceClear, - validate, - ...props -}) => { - return ( - - {({ form }) => { - return ( - - ) - }} - - ) -} diff --git a/packages/next/src/components/formButtonGroup.tsx b/packages/next/src/components/formButtonGroup.tsx deleted file mode 100644 index 2faea71c2a4..00000000000 --- a/packages/next/src/components/formButtonGroup.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React, { useRef } from 'react' -import { Grid } from '@alifd/next' -import Sticky from 'react-stikky' -import cls from 'classnames' -import styled from 'styled-components' -import { useFormItem } from '../compat/context' -import { IFormButtonGroupProps } from '../types' - -const { Row, Col } = Grid - -export interface IOffset { - top: number | string - right: number | string - bottom: number | string - left: number | string -} - -const getAlign = align => { - if (align === 'start' || align === 'end') return align - if (align === 'left' || align === 'top') return 'flex-start' - if (align === 'right' || align === 'bottom') return 'flex-end' - return align -} - -const isElementInViewport = ( - rect: ClientRect, - { - offset = 0, - threshold = 0 - }: { - offset?: IOffset | number - threshold?: number - } = {} -): boolean => { - const { top, right, bottom, left, width, height } = rect - const intersection = { - t: bottom, - r: window.innerWidth - left, - b: window.innerHeight - top, - l: right - } - - const elementThreshold = { - x: threshold * width, - y: threshold * height - } - - return ( - intersection.t >= - ((offset as IOffset).top || (offset as number) + elementThreshold.y) && - intersection.r >= - ((offset as IOffset).right || (offset as number) + elementThreshold.x) && - intersection.b >= - ((offset as IOffset).bottom || (offset as number) + elementThreshold.y) && - intersection.l >= - ((offset as IOffset).left || (offset as number) + elementThreshold.x) - ) -} - -export const FormButtonGroup = styled( - (props: React.PropsWithChildren) => { - const { - span, - zIndex, - sticky, - style, - offset, - className, - children, - triggerDistance, - itemStyle - } = props - const { inline } = useFormItem() - const selfRef = useRef() - const renderChildren = () => { - return ( -
    - - - -
    - {children} -
    - - -
    -
    - ) - } - const getStickyBoundaryHandler = () => { - return () => { - if (selfRef.current && selfRef.current.parentElement) { - const container = selfRef.current.parentElement - return isElementInViewport(container.getBoundingClientRect()) - } - return true - } - } - - const content = ( -
    - {renderChildren()} -
    - ) - - if (sticky) { - return ( -
    - -
    - {content} -
    -
    -
    - ) - } - - return content - } -)` - ${(props: IFormButtonGroupProps) => - props.align ? `display:flex;justify-content: ${getAlign(props.align)}` : ''} - &.is-inline { - display: inline-block; - flex-grow: 3; - } - .button-group { - .inline { - display: inline-block; - .inline-view { - & > * { - margin-right: 10px; - margin-left: 0px; - display: inline-block; - } - & > *:last-child { - margin-right: 0 !important; - } - } - } - } -` diff --git a/packages/react/src/hooks/useForm.ts b/packages/react/src/hooks/useForm.ts index 65d9a9d50b8..bad3aad355f 100644 --- a/packages/react/src/hooks/useForm.ts +++ b/packages/react/src/hooks/useForm.ts @@ -112,6 +112,9 @@ export const useForm = < dispatch: form.notify } implementActions(actions) + if(broadcast){ + broadcast.setContext(actions) + } } ) ], From 42952c2b3557c8000bab44e35478218e7baf6e64 Mon Sep 17 00:00:00 2001 From: janryWang Date: Fri, 1 Nov 2019 18:03:41 +0800 Subject: [PATCH 11/99] fix(@uform/core): fix process calculation logic --- packages/core/src/shared/model.ts | 11 ++++++++--- packages/core/src/types.ts | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/core/src/shared/model.ts b/packages/core/src/shared/model.ts index 6508c1c385e..87833200005 100644 --- a/packages/core/src/shared/model.ts +++ b/packages/core/src/shared/model.ts @@ -25,7 +25,7 @@ export const createStateModel = ( public dirtys: StateDirtyMap public persistDirtys: StateDirtyMap public batching: boolean - public processing: boolean + public processFlag: number public controller: StateModel constructor(defaultProps: DefaultProps) { @@ -38,6 +38,7 @@ export const createStateModel = ( this.dirtys = {} this.persistDirtys = {} this.dirtyNum = 0 + this.processFlag = 0 this.batching = false this.controller = new Factory(this.state, this.props) this.displayName = Factory.displayName @@ -101,9 +102,9 @@ export const createStateModel = ( this.dirtys = {} this.dirtyNum = 0 } - if (!this.processing) { + if (!this.processFlag) { this.persistDirtys = {} - this.processing = true + this.processFlag++ } callback(draft) if (isFn(this.controller.computeState)) { @@ -179,6 +180,10 @@ export const createStateModel = ( //2. 自己监听自己,自己修改自己的状态,希望触发onFieldChange } } + this.processFlag-- + if (!this.processFlag) { + this.persistDirtys = {} + } } } /** diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index dafabe57320..8521b4a0071 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -272,7 +272,7 @@ export interface IModel extends Subscribable { dirtys: StateDirtyMap persistDirtys: StateDirtyMap batching: boolean - processing: boolean + processFlag: number controller: StateModel batch: (callback?: () => void) => void getState: (callback?: (state: S) => any) => any From ba5678c2b2fe1b7dbabfc5129186ab14ec031567 Mon Sep 17 00:00:00 2001 From: janryWang Date: Sun, 3 Nov 2019 20:11:28 +0800 Subject: [PATCH 12/99] fix(@uform/core): fix position of stack counter --- packages/core/src/shared/model.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/src/shared/model.ts b/packages/core/src/shared/model.ts index 87833200005..77d420d446b 100644 --- a/packages/core/src/shared/model.ts +++ b/packages/core/src/shared/model.ts @@ -25,7 +25,7 @@ export const createStateModel = ( public dirtys: StateDirtyMap public persistDirtys: StateDirtyMap public batching: boolean - public processFlag: number + public stackCount: number public controller: StateModel constructor(defaultProps: DefaultProps) { @@ -38,7 +38,7 @@ export const createStateModel = ( this.dirtys = {} this.persistDirtys = {} this.dirtyNum = 0 - this.processFlag = 0 + this.stackCount = 0 this.batching = false this.controller = new Factory(this.state, this.props) this.displayName = Factory.displayName @@ -102,10 +102,10 @@ export const createStateModel = ( this.dirtys = {} this.dirtyNum = 0 } - if (!this.processFlag) { + if (!this.stackCount) { this.persistDirtys = {} - this.processFlag++ } + this.stackCount++ callback(draft) if (isFn(this.controller.computeState)) { this.controller.computeState(draft, this.state) @@ -180,8 +180,8 @@ export const createStateModel = ( //2. 自己监听自己,自己修改自己的状态,希望触发onFieldChange } } - this.processFlag-- - if (!this.processFlag) { + this.stackCount-- + if (!this.stackCount) { this.persistDirtys = {} } } From e7edcfa202f78cbd14b84043972135487937fa58 Mon Sep 17 00:00:00 2001 From: quirkyvar Date: Sun, 3 Nov 2019 20:11:58 +0800 Subject: [PATCH 13/99] feat(core): add model unit test (#359) --- package.json | 2 +- .../core/src/__tests__/field.state.spec.ts | 434 +++++++++++++++++- .../core/src/__tests__/form.state.spec.ts | 242 +++++++++- packages/core/src/__tests__/model.spec.ts | 342 ++++++++++++-- .../core/src/__tests__/vfield.state.spec.ts | 176 ++++++- packages/core/src/shared/model.ts | 36 +- packages/core/src/state/form.ts | 9 +- 7 files changed, 1142 insertions(+), 99 deletions(-) diff --git a/package.json b/package.json index 20829c6663f..b22a7f2e99f 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "start": "node ./scripts/docs.js start -i docs", "sort-api": "node ./scripts/sort-api-table.js", "test": "npm run lint && jest", - "test:core": "jest --watch packages/core/src/__tests__/index.spec.ts", + "test:core": "jest --watch packages/core/src/__tests__/*.spec.ts", "test:prod": "cross-env TEST_ENV=production npm run build && jest", "doc:core": "doc-scripts start -i packages/core", "doc:react": "doc-scripts start -i packages/react", diff --git a/packages/core/src/__tests__/field.state.spec.ts b/packages/core/src/__tests__/field.state.spec.ts index 5c9cd2c1486..ed9a530b730 100644 --- a/packages/core/src/__tests__/field.state.spec.ts +++ b/packages/core/src/__tests__/field.state.spec.ts @@ -1,32 +1,436 @@ -//import { FieldState } from '../state/field' +import { FieldState } from '../state/field' -test('subscribe', () => { - //todo +test('computeState setValues', () => { + const state = new FieldState({ useDirty: false }) + expect(state.getState().visible).toEqual(true) + expect(state.getState().value).toEqual(undefined) + //如果是隐藏状态,则禁止修改值 + state.setState((draft) => { + draft.visible = false + draft.value = 123 + draft.initialValue = 456 + }) + expect(state.getState().unmounted).toEqual(false) + expect(state.getState().visible).toEqual(false) + expect(state.getState().value).toEqual(undefined) + expect(state.getState().initialValue).toEqual(undefined) + state.setState((draft) => { + draft.visible = true + draft.unmounted = true + draft.value = 123 + draft.initialValue = 456 + }) + expect(state.getState().visible).toEqual(true) + expect(state.getState().unmounted).toEqual(true) + expect(state.getState().value).toEqual(undefined) + expect(state.getState().initialValue).toEqual(undefined) + state.setState((draft) => { + draft.visible = true + draft.unmounted = false + draft.value = 123 + draft.initialValue = 456 + }) + expect(state.getState().unmounted).toEqual(false) + expect(state.getState().visible).toEqual(true) + expect(state.getState().value).toEqual(123) + expect(state.getState().initialValue).toEqual(456) }) -test('unsubscribe', () => { - //todo + +test('computeState value and readValues', () => { + const state = new FieldState({ useDirty: false }) + expect(state.getState().value).toEqual(undefined) + expect(state.getState().values).toEqual([]) + expect(state.getState().initialized).toEqual(false) + expect(state.getState().modified).toEqual(false) + + // modified depends on initialized and value change + // invalid values and + state.setState((draft) => { + draft.values = undefined + draft.value = 123 + draft.initialized = true + }) + + expect(state.getState().value).toEqual(123) + expect(state.getState().values).toEqual([123]) + expect(state.getState().initialized).toEqual(true) + expect(state.getState().modified).toEqual(false) + + // valid values + state.setState((draft) => { + draft.values = [1,2,3] + draft.value = 456 + }) + expect(state.getState().modified).toEqual(true) + expect(state.getState().value).toEqual(456) + expect(state.getState().values).toEqual([456,2,3]) +}) + +test('computeState editable', () => { + const state = new FieldState({ useDirty: false }) + expect(state.getState().editable).toEqual(true) + expect(state.getState().selfEditable).toEqual(undefined) + expect(state.getState().formEditable).toEqual(undefined) + + // selfEditable + state.setState((draft) => { + draft.editable = false + }) + expect(state.getState().editable).toEqual(false) + expect(state.getState().selfEditable).toEqual(false) + expect(state.getState().formEditable).toEqual(undefined) + + // priority: selfEditable > formEditable > true + const formEditable = () => true + state.setState((draft) => { + draft.editable = undefined + draft.formEditable = formEditable + }) + expect(state.getState().editable).toEqual(true) + expect(state.getState().selfEditable).toEqual(undefined) + expect(state.getState().formEditable).toEqual(formEditable) + + // priority: selfEditable > formEditable > true + state.setState((draft) => { + draft.editable = undefined + draft.formEditable = undefined + }) + expect(state.getState().editable).toEqual(true) + expect(state.getState().selfEditable).toEqual(undefined) + expect(state.getState().formEditable).toEqual(undefined) +}) + +test('computeState erors/warning', () => { + const state = new FieldState({ useDirty: false }) + expect(state.getState().invalid).toEqual(false) + expect(state.getState().valid).toEqual(true) + expect(state.getState().errors).toEqual([]) + expect(state.getState().effectErrors).toEqual([]) + expect(state.getState().warnings).toEqual([]) + expect(state.getState().effectWarnings).toEqual([]) + expect(state.getState().ruleErrors).toEqual([]) + expect(state.getState().ruleWarnings).toEqual([]) + + // invalid setting + state.setState((draft) => { + draft.errors = ['', undefined, null] + draft.warnings = ['', undefined, null] + draft.ruleErrors = ['', undefined, null] + draft.ruleWarnings = ['', undefined, null] + }) + expect(state.getState().invalid).toEqual(false) + expect(state.getState().valid).toEqual(true) + expect(state.getState().errors).toEqual([]) + expect(state.getState().effectErrors).toEqual([]) + expect(state.getState().warnings).toEqual([]) + expect(state.getState().effectWarnings).toEqual([]) + expect(state.getState().ruleErrors).toEqual([]) + expect(state.getState().ruleWarnings).toEqual([]) + + // errors = ruleErrors effectErrors + // warnings = ruleWarnings warnings + const errors = ['error1', 'error2'] + const warnings = ['warning1', 'warning2'] + const ruleErrors = ['ruleError1', 'ruleError2'] + const ruleWarnings = ['ruleWarning1', 'ruleWarning2'] + state.setState((draft) => { + draft.errors = errors + draft.warnings = warnings + draft.ruleErrors = ruleErrors + draft.ruleWarnings = ruleWarnings + }) + expect(state.getState().invalid).toEqual(true) + expect(state.getState().valid).toEqual(false) + expect(state.getState().errors).toEqual([...ruleErrors, ...errors]) + expect(state.getState().effectErrors).toEqual(errors) + expect(state.getState().warnings).toEqual([...ruleWarnings, ...warnings]) + expect(state.getState().effectWarnings).toEqual(warnings) + expect(state.getState().ruleErrors).toEqual(ruleErrors) + expect(state.getState().ruleWarnings).toEqual(ruleWarnings) + + // 以下几种情况清理错误和警告信息 + // 1. 字段设置为不可编辑 + // 2. 字段隐藏 + // 3. 字段被卸载 + state.setState((draft) => { + draft.visible = false + }) + expect(state.getState().invalid).toEqual(false) + expect(state.getState().valid).toEqual(true) + expect(state.getState().errors).toEqual([]) + expect(state.getState().effectErrors).toEqual([]) + expect(state.getState().warnings).toEqual([]) + expect(state.getState().effectWarnings).toEqual([]) + expect(state.getState().ruleErrors).toEqual(ruleErrors) + expect(state.getState().ruleWarnings).toEqual(ruleWarnings) + + state.setState((draft) => { + draft.visible = true + }) + + expect(state.getState().invalid).toEqual(true) + expect(state.getState().valid).toEqual(false) + expect(state.getState().errors).toEqual(ruleErrors) + expect(state.getState().effectErrors).toEqual([]) + expect(state.getState().warnings).toEqual(ruleWarnings) + expect(state.getState().effectWarnings).toEqual([]) + expect(state.getState().ruleErrors).toEqual(ruleErrors) + expect(state.getState().ruleWarnings).toEqual(ruleWarnings) + + state.setState((draft) => { + draft.unmounted = true + }) + expect(state.getState().invalid).toEqual(false) + expect(state.getState().valid).toEqual(true) + expect(state.getState().errors).toEqual([]) + expect(state.getState().effectErrors).toEqual([]) + expect(state.getState().warnings).toEqual([]) + expect(state.getState().effectWarnings).toEqual([]) + expect(state.getState().ruleErrors).toEqual(ruleErrors) + expect(state.getState().ruleWarnings).toEqual(ruleWarnings) + + state.setState((draft) => { + draft.unmounted = false + }) + expect(state.getState().invalid).toEqual(true) + expect(state.getState().valid).toEqual(false) + expect(state.getState().errors).toEqual(ruleErrors) + expect(state.getState().effectErrors).toEqual([]) + expect(state.getState().warnings).toEqual(ruleWarnings) + expect(state.getState().effectWarnings).toEqual([]) + expect(state.getState().ruleErrors).toEqual(ruleErrors) + expect(state.getState().ruleWarnings).toEqual(ruleWarnings) + + state.setState((draft) => { + draft.editable = false + }) + expect(state.getState().invalid).toEqual(false) + expect(state.getState().valid).toEqual(true) + expect(state.getState().errors).toEqual([]) + expect(state.getState().effectErrors).toEqual([]) + expect(state.getState().warnings).toEqual([]) + expect(state.getState().effectWarnings).toEqual([]) + expect(state.getState().ruleErrors).toEqual(ruleErrors) + expect(state.getState().ruleWarnings).toEqual(ruleWarnings) + + state.setState((draft) => { + draft.editable = false + }) + expect(state.getState().invalid).toEqual(true) + expect(state.getState().valid).toEqual(false) + expect(state.getState().errors).toEqual(ruleErrors) + expect(state.getState().effectErrors).toEqual([]) + expect(state.getState().warnings).toEqual(ruleWarnings) + expect(state.getState().effectWarnings).toEqual([]) + expect(state.getState().ruleErrors).toEqual(ruleErrors) + expect(state.getState().ruleWarnings).toEqual(ruleWarnings) + +}) + + +test('computeState comon', () => { + // pristine depends on whether values to be equal initialvalues + const state = new FieldState({ useDirty: false }) + expect(state.getState().pristine).toEqual(true) + state.setState(draft => { + draft.pristine = false + }) + expect(state.getState().pristine).toEqual(true) + state.setState(draft => { + draft.value = { change: true } + }) + expect(state.getState().value).toEqual({ change: true }) + expect(state.getState().pristine).toEqual(false) + + // loading depends on validating + expect(state.getState().loading).toEqual(false) + state.setState(draft => { + draft.validating = true + }) + expect(state.getState().validating).toEqual(true) + expect(state.getState().loading).toEqual(true) + + // cannot set invalid props + expect(state.getState().props).toEqual({}) + state.setState((draft) => { + draft.props = { hello: 'world' } + }) + expect(state.getState().props).toEqual({ hello: 'world' }) + state.setState((draft) => { + draft.props = undefined + }) + expect(state.getState().props).toEqual({ hello: 'world' }) + + // mounted and unmounted + expect(state.getState().mounted).toEqual(false) + expect(state.getState().unmounted).toEqual(false) + state.setState(draft => { + draft.mounted = true + }) + expect(state.getState().mounted).toEqual(true) + expect(state.getState().unmounted).toEqual(false) + state.setState(draft => { + draft.unmounted = true + }) + expect(state.getState().mounted).toEqual(false) + expect(state.getState().unmounted).toEqual(true) +}) + +test('subscribe/unsubscribe', () => { + const state = new FieldState({ useDirty: false }) + const cb = jest.fn() + const idx = state.subscribe(cb) + const paylaod = state.getState() + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) + state.unsubscribe(idx) + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) }) test('batch', () => { - //todo + const state = new FieldState({ useDirty: false }) + const cb = jest.fn() + state.batch(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith() + // force run getState + const susCb = jest.fn() + state.subscribe(susCb) + state.dirtyNum = 1 + state.batch(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith() + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith(state.state) }) test('getState', () => { - //todo + const state = new FieldState({ useDirty: false }) + const cb = jest.fn() + state.getState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.getState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.getState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(null) }) test('setState', () => { - //todo + const state = new FieldState({ useDirty: false }) + const susCb = jest.fn() + state.subscribe(susCb) + const cb1 = (draft) => { draft.value = { change: true } } + const cb2 = (draft) => { draft.value = { withNotify: true } } + const cb3 = (draft) => { draft.value = { withBatching: true } } + const cb4 = (draft) => { draft.value = { ...draft.value, withBatching2: true } } + const prevState1 = state.getState() + expect(prevState1.value).toEqual(undefined) + + // 默认 slient = false, 触发notify 通知UI更新 + state.setState(cb1) + expect(state.getState().value).toEqual({ change: true }) + expect(state.getState()).toEqual({ ...prevState1, pristine: false, value: { change: true }, values: [{ change: true }] }) + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith({ ...prevState1, pristine: false, value: { change: true }, values: [{ change: true }] }) + + // slient = true 不触发notify + const prevState2 = state.getState() + expect(prevState2.value.withNotify).toEqual(undefined) + state.setState(cb2, true) + expect(state.getState().value.withNotify).toEqual(true) + expect(state.getState()).toEqual({ ...prevState2, value: { withNotify: true }, values: [{ withNotify: true }]}) + expect(susCb).toBeCalledTimes(1) + + // batching 相当于slient = true + const prevState3 = state.getState() + expect(prevState3.value.withBatching).toEqual(undefined) + expect(prevState3.value.withBatching2).toEqual(undefined) + state.batch(() => { + state.setState(cb3) + state.setState(cb4) + }) + + expect(state.getState().value.withBatching).toEqual(true) + expect(state.getState().value.withBatching2).toEqual(true) + expect(state.getState()).toEqual({ ...prevState3, + value: { withBatching: true, withBatching2: true }, + values: [{ withBatching: true, withBatching2: true }] + }) + // 这次notify是由batch批处理结束调用的 + expect(susCb).toBeCalledTimes(2) + expect(susCb).toBeCalledWith({ ...prevState3, + value: { withBatching: true, withBatching2: true }, + values: [{ withBatching: true, withBatching2: true }] + }) }) test('getSourceState', () => { - //todo + const state = new FieldState({ useDirty: false }) + const cb = jest.fn() + state.unsafe_getSourceState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.unsafe_getSourceState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.unsafe_getSourceState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(state.state) }) test('setSourceState', () => { - //todo -}) -test('setState', () => { - //todo + const state = new FieldState({ useDirty: false }) + const cb1 = (draft) => draft.change = true + const prevState1 = state.unsafe_getSourceState() + expect(prevState1.change).toEqual(undefined) + + state.unsafe_setSourceState(cb1) + expect(state.unsafe_getSourceState()).toEqual({ ...prevState1, change: true }) }) test('hasChanged', () => { - //todo + const state = new FieldState({ useDirty: true }) + expect(state.dirtyNum).toEqual(0) + expect(state.hasChanged()).toEqual(false) + state.dirtyNum = 1 + expect(state.hasChanged()).toEqual(true) + state.dirtyNum = 0 + expect(state.hasChanged()).toEqual(false) + state.dirtys.validating = true + expect(state.hasChanged()).toEqual(false) + expect(state.hasChanged('validating')).toEqual(true) }) test('getChanged', () => { - //todo + const state = new FieldState({ useDirty: true }) + expect(state.getChanged()).toEqual({}) + state.dirtys.validating = true + expect(state.getChanged()).toEqual({ validating: true }) + state.dirtys = { value: true } + expect(state.getChanged()).toEqual({ value: true }) +}) +test('hasChangedInSequence', () => { + const state = new FieldState({ useDirty: true }) + expect(state.hasChangedInSequence()).toEqual(false) + state.persistDirtys.validating = true + expect(state.hasChangedInSequence()).toEqual(true) + expect(state.hasChangedInSequence('validating')).toEqual(true) + state.persistDirtys.validating = false + expect(state.hasChangedInSequence()).toEqual(false) + expect(state.hasChangedInSequence('validating')).toEqual(false) + state.persistDirtys = {} + expect(state.hasChangedInSequence()).toEqual(false) + state.persistDirtys = { validating: true } + expect(state.hasChangedInSequence()).toEqual(true) + expect(state.hasChangedInSequence('validating')).toEqual(true) }) +test('getChangedInSequence', () => { + const state = new FieldState({ useDirty: true }) + expect(state.getChangedInSequence()).toEqual({}) + state.persistDirtys.validating = true + expect(state.getChangedInSequence()).toEqual({ validating: true }) + state.persistDirtys = { value: true } + expect(state.getChangedInSequence()).toEqual({ value: true }) +}) + diff --git a/packages/core/src/__tests__/form.state.spec.ts b/packages/core/src/__tests__/form.state.spec.ts index 6135d4643fd..791c0244c9f 100644 --- a/packages/core/src/__tests__/form.state.spec.ts +++ b/packages/core/src/__tests__/form.state.spec.ts @@ -1,32 +1,244 @@ -//import { FormState } from '../state/form' +import { FormState } from '../state/form' -test('subscribe', () => { - //todo +test('computeState', () => { + const state = new FormState({ useDirty: false }) + expect(state.getState()).toEqual({ + displayName: 'FormState', + editable: undefined, + pristine: true, + valid: true, + invalid: false, + loading: false, + validating: false, + initialized: false, + submitting: false, + errors: [], + warnings: [], + values: {}, + initialValues: {}, + mounted: false, + unmounted: false, + props: {} + }) + + // can not set invalid errors, warnings + state.setState(draft => { + draft.errors = [undefined, null, ''] + draft.warnings = [undefined, null, ''] + }) + expect(state.getState().warnings).toEqual([]) + expect(state.getState().errors).toEqual([]) + + // set errors will change invalid and valid + expect(state.getState().invalid).toEqual(false) + expect(state.getState().valid).toEqual(true) + state.setState(draft => { + draft.invalid = false + draft.valid = true + }) + expect(state.getState().invalid).toEqual(false) + expect(state.getState().valid).toEqual(true) + + state.setState(draft => { + draft.errors = ['sth wrong'] + }) + expect(state.getState().errors).toEqual(['sth wrong']) + expect(state.getState().invalid).toEqual(true) + expect(state.getState().valid).toEqual(false) + + // pristine depends on whether values to be equal initialvalues + expect(state.getState().pristine).toEqual(true) + state.setState(draft => { + draft.pristine = false + }) + expect(state.getState().pristine).toEqual(true) + state.setState(draft => { + draft.values = { change: true } + }) + expect(state.getState().values).toEqual({ change: true }) + expect(state.getState().pristine).toEqual(false) + + // cannot set invalid props + expect(state.getState().props).toEqual({}) + state.setState((draft) => { + draft.props = { hello: 'world' } + }) + expect(state.getState().props).toEqual({ hello: 'world' }) + state.setState((draft) => { + draft.props = undefined + }) + expect(state.getState().props).toEqual({ hello: 'world' }) + + // loading depends on validating + expect(state.getState().loading).toEqual(false) + state.setState(draft => { + draft.validating = true + }) + expect(state.getState().validating).toEqual(true) + expect(state.getState().loading).toEqual(true) + + // mounted and unmounted + expect(state.getState().mounted).toEqual(false) + expect(state.getState().unmounted).toEqual(false) + state.setState(draft => { + draft.mounted = true + }) + expect(state.getState().mounted).toEqual(true) + expect(state.getState().unmounted).toEqual(false) + state.setState(draft => { + draft.unmounted = true + }) + expect(state.getState().mounted).toEqual(false) + expect(state.getState().unmounted).toEqual(true) }) -test('unsubscribe', () => { - //todo +test('subscribe/unsubscribe', () => { + const state = new FormState({ useDirty: false }) + const cb = jest.fn() + const idx = state.subscribe(cb) + const paylaod = state.getState() + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) + state.unsubscribe(idx) + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) }) test('batch', () => { - //todo + const state = new FormState({ useDirty: false }) + const cb = jest.fn() + state.batch(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith() + // force run getState + const susCb = jest.fn() + state.subscribe(susCb) + state.dirtyNum = 1 + state.batch(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith() + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith(state.state) }) test('getState', () => { - //todo + const state = new FormState({ useDirty: false }) + const cb = jest.fn() + state.getState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.getState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.getState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(null) }) test('setState', () => { - //todo + const state = new FormState({ useDirty: false }) + const susCb = jest.fn() + state.subscribe(susCb) + const cb1 = (draft) => { draft.values = { change: true } } + const cb2 = (draft) => { draft.values = { withNotify: true } } + const cb3 = (draft) => { draft.values = { withBatching: true } } + const cb4 = (draft) => { draft.values = { ...draft.values, withBatching2: true } } + const prevState1 = state.getState() + expect(prevState1.values.change).toEqual(undefined) + + // 默认 slient = false, 触发notify 通知UI更新 + state.setState(cb1) + expect(state.getState().values.change).toEqual(true) + expect(state.getState()).toEqual({ ...prevState1, pristine: false, values: { change: true } }) + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith({ ...prevState1, pristine: false, values: { change: true } }) + + // slient = true 不触发notify + const prevState2 = state.getState() + expect(prevState2.values.withNotify).toEqual(undefined) + state.setState(cb2, true) + expect(state.getState().values.withNotify).toEqual(true) + expect(state.getState()).toEqual({ ...prevState2, values: { withNotify: true }}) + expect(susCb).toBeCalledTimes(1) + + // batching 相当于slient = true + const prevState3 = state.getState() + expect(prevState3.values.withBatching).toEqual(undefined) + expect(prevState3.values.withBatching2).toEqual(undefined) + state.batch(() => { + state.setState(cb3) + state.setState(cb4) + }) + + expect(state.getState().values.withBatching).toEqual(true) + expect(state.getState().values.withBatching2).toEqual(true) + expect(state.getState()).toEqual({ ...prevState3, values: { withBatching: true, withBatching2: true } }) + // 这次notify是由batch批处理结束调用的 + expect(susCb).toBeCalledTimes(2) + expect(susCb).toBeCalledWith({ ...prevState3, values: { withBatching: true, withBatching2: true } }) }) test('getSourceState', () => { - //todo + const state = new FormState({ useDirty: false }) + const cb = jest.fn() + state.unsafe_getSourceState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.unsafe_getSourceState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.unsafe_getSourceState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(state.state) }) test('setSourceState', () => { - //todo -}) -test('setState', () => { - //todo + const state = new FormState({ useDirty: false }) + const cb1 = (draft) => draft.change = true + const prevState1 = state.unsafe_getSourceState() + expect(prevState1.change).toEqual(undefined) + + state.unsafe_setSourceState(cb1) + expect(state.unsafe_getSourceState()).toEqual({ ...prevState1, change: true }) }) test('hasChanged', () => { - //todo + const state = new FormState({ useDirty: true }) + expect(state.dirtyNum).toEqual(0) + expect(state.hasChanged()).toEqual(false) + state.dirtyNum = 1 + expect(state.hasChanged()).toEqual(true) + state.dirtyNum = 0 + expect(state.hasChanged()).toEqual(false) + state.dirtys.validating = true + expect(state.hasChanged()).toEqual(false) + expect(state.hasChanged('validating')).toEqual(true) }) test('getChanged', () => { - //todo + const state = new FormState({ useDirty: true }) + expect(state.getChanged()).toEqual({}) + state.dirtys.validating = true + expect(state.getChanged()).toEqual({ validating: true }) + state.dirtys = { values: true } + expect(state.getChanged()).toEqual({ values: true }) +}) +test('hasChangedInSequence', () => { + const state = new FormState({ useDirty: true }) + expect(state.hasChangedInSequence()).toEqual(false) + state.persistDirtys.validating = true + expect(state.hasChangedInSequence()).toEqual(true) + expect(state.hasChangedInSequence('validating')).toEqual(true) + state.persistDirtys.validating = false + expect(state.hasChangedInSequence()).toEqual(false) + expect(state.hasChangedInSequence('validating')).toEqual(false) + state.persistDirtys = {} + expect(state.hasChangedInSequence()).toEqual(false) + state.persistDirtys = { validating: true } + expect(state.hasChangedInSequence()).toEqual(true) + expect(state.hasChangedInSequence('validating')).toEqual(true) +}) +test('getChangedInSequence', () => { + const state = new FormState({ useDirty: true }) + expect(state.getChangedInSequence()).toEqual({}) + state.persistDirtys.validating = true + expect(state.getChangedInSequence()).toEqual({ validating: true }) + state.persistDirtys = { values: true } + expect(state.getChangedInSequence()).toEqual({ values: true }) }) diff --git a/packages/core/src/__tests__/model.spec.ts b/packages/core/src/__tests__/model.spec.ts index bfd1a6dea3e..fe313c00331 100644 --- a/packages/core/src/__tests__/model.spec.ts +++ b/packages/core/src/__tests__/model.spec.ts @@ -1,71 +1,347 @@ -//import { createStateModel } from '../shared/model' +import { createStateModel } from '../shared/model' + +const displayName = 'TEST' +const defaultState = { type: 'controller defaultState' } +const defaultProps = { type: 'controller defaultProps' } + +class State { + static displayName = displayName + static defaultState = defaultState + static defaultProps = defaultProps + + name: string + state: any + props: any + + constructor(state, props) { + this.name = 'inner' + this.state = state + this.props = props + } + + dirtyCheck(dirtys) {} + computeState(state, prevState) {} +} +const StateModel = createStateModel(State) test('createStateModel', () => { - //todo + const params = { modelType: 'model defaultProps' } + const state1 = new StateModel(params) + // model properties + expect(state1.state).toEqual({ displayName, ...defaultState }) + expect(state1.props).toEqual({ ...defaultProps, ...params }) + expect(state1.dirtys).toEqual({}) + expect(state1.persistDirtys).toEqual({}) + expect(state1.dirtyNum).toEqual(0) + expect(state1.batching).toEqual(false) + expect(state1.processing).toEqual(false) + expect(state1.displayName).toEqual(displayName) + expect(state1.controller).toEqual({ + state: { displayName, ...defaultState }, + props: { ...defaultProps, ...params }, + name: 'inner', + }) }) describe('proxy model', () => { - test('subscribe', () => { - //todo - }) - test('unsubscribe', () => { - //todo + test('subscribe/unsubscribe', () => { + const state = new StateModel({ useDirty: false }) + const cb = jest.fn() + const idx = state.subscribe(cb) + const paylaod = { hello: 'world' } + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) + state.unsubscribe(idx) + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) }) - test('batch', () => { - //todo + test('batch with ', () => { + const state = new StateModel({ useDirty: false }) + const cb = jest.fn() + state.batch(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith() + // force run getState + const susCb = jest.fn() + state.subscribe(susCb) + state.dirtyNum = 1 + state.batch(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith() + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith(state.state) }) test('getState', () => { - //todo + const state = new StateModel({ useDirty: false }) + const cb = jest.fn() + state.getState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.getState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.getState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(null) }) test('setState', () => { - //todo + const state = new StateModel({ useDirty: false }) + const susCb = jest.fn() + state.subscribe(susCb) + const cb1 = (draft) => draft.change = true + const cb2 = (draft) => draft.withNotify = true + const cb3 = (draft) => draft.withBatching = true + const cb4 = (draft) => draft.withBatching2 = true + const prevState1 = state.getState() + expect(prevState1.change).toEqual(undefined) + + // 默认 slient = false, 触发notify 通知UI更新 + state.setState(cb1) + expect(state.getState().change).toEqual(true) + expect(state.getState()).toEqual({ ...prevState1, change: true }) + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith({ ...prevState1, change: true }) + + // slient = true 不触发notify + const prevState2 = state.getState() + expect(prevState2.withNotify).toEqual(undefined) + state.setState(cb2, true) + expect(state.getState().withNotify).toEqual(true) + expect(state.getState()).toEqual({ ...prevState2, withNotify: true }) + expect(susCb).toBeCalledTimes(1) + + // batching 相当于slient = true + const prevState3 = state.getState() + expect(prevState3.withBatching).toEqual(undefined) + expect(prevState3.withBatching2).toEqual(undefined) + state.batch(() => { + state.setState(cb3) + state.setState(cb4) + }) + + expect(state.getState().withBatching).toEqual(true) + expect(state.getState().withBatching2).toEqual(true) + expect(state.getState()).toEqual({ ...prevState3, withBatching: true, withBatching2: true }) + // 这次notify是由batch批处理结束调用的 + expect(susCb).toBeCalledTimes(2) + expect(susCb).toBeCalledWith({ ...prevState3, withBatching: true, withBatching2: true }) }) test('getSourceState', () => { - //todo + const state = new StateModel({ useDirty: false }) + const cb = jest.fn() + state.unsafe_getSourceState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.unsafe_getSourceState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.unsafe_getSourceState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(state.state) }) test('setSourceState', () => { - //todo - }) - test('setState', () => { - //todo + const state = new StateModel({ useDirty: false }) + const cb1 = (draft) => draft.change = true + const prevState1 = state.unsafe_getSourceState() + expect(prevState1.change).toEqual(undefined) + + state.unsafe_setSourceState(cb1) + expect(state.unsafe_getSourceState()).toEqual({ ...prevState1, change: true }) }) test('hasChanged', () => { - //todo + const state = new StateModel({ useDirty: false }) + expect(state.dirtyNum).toEqual(0) + expect(state.hasChanged()).toEqual(false) + state.dirtyNum = 1 + expect(state.hasChanged()).toEqual(true) + state.dirtyNum = 0 + expect(state.hasChanged()).toEqual(false) + state.dirtys.change = true + expect(state.hasChanged()).toEqual(false) + expect(state.hasChanged('change')).toEqual(true) }) test('getChanged', () => { - //todo + const state = new StateModel({ useDirty: false }) + expect(state.getChanged()).toEqual({}) + state.dirtys.change = true + expect(state.getChanged()).toEqual({ change: true }) + state.dirtys = { changeObj: true } + expect(state.getChanged()).toEqual({ changeObj: true }) + }) + test('hasChangedInSequence', () => { + const state = new StateModel({ useDirty: false }) + expect(state.hasChangedInSequence()).toEqual(false) + state.persistDirtys.change = true + expect(state.hasChangedInSequence()).toEqual(true) + expect(state.hasChangedInSequence('change')).toEqual(true) + state.persistDirtys.change = false + expect(state.hasChangedInSequence()).toEqual(false) + expect(state.hasChangedInSequence('change')).toEqual(false) + state.persistDirtys = {} + expect(state.hasChangedInSequence()).toEqual(false) + state.persistDirtys = { change: true } + expect(state.hasChangedInSequence()).toEqual(true) + expect(state.hasChangedInSequence('change')).toEqual(true) + }) + test('getChangedInSequence', () => { + const state = new StateModel({ useDirty: false }) + expect(state.getChangedInSequence()).toEqual({}) + state.persistDirtys.change = true + expect(state.getChangedInSequence()).toEqual({ change: true }) + state.persistDirtys = { changeObj: true } + expect(state.getChangedInSequence()).toEqual({ changeObj: true }) }) }) describe('dirty model', () => { - test('subscribe', () => { - //todo - }) - test('unsubscribe', () => { - //todo + test('subscribe/unsubscribe', () => { + const state = new StateModel({ useDirty: true }) + const cb = jest.fn() + const idx = state.subscribe(cb) + const paylaod = { hello: 'world' } + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) + state.unsubscribe(idx) + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) }) test('batch', () => { - //todo + const state = new StateModel({ useDirty: true }) + const cb = jest.fn() + state.batch(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith() + // force run getState + const susCb = jest.fn() + state.subscribe(susCb) + state.dirtyNum = 1 + state.batch(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith() + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith(state.state) }) test('getState', () => { - //todo + const state = new StateModel({ useDirty: true }) + const cb = jest.fn() + state.getState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.getState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.getState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(null) }) test('setState', () => { - //todo + const state = new StateModel({ useDirty: true }) + const susCb = jest.fn() + state.subscribe(susCb) + const cb1 = (draft) => draft.change = true + const cb2 = (draft) => draft.withNotify = true + const cb3 = (draft) => draft.withBatching = true + const cb4 = (draft) => draft.withBatching2 = true + const prevState1 = state.getState() + expect(prevState1.change).toEqual(undefined) + + // 默认 slient = false, 触发notify 通知UI更新 + state.setState(cb1) + expect(state.getState().change).toEqual(true) + expect(state.getState()).toEqual({ ...prevState1, change: true }) + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith({ ...prevState1, change: true }) + + // slient = true 不触发notify + const prevState2 = state.getState() + expect(prevState2.withNotify).toEqual(undefined) + state.setState(cb2, true) + expect(state.getState().withNotify).toEqual(true) + expect(state.getState()).toEqual({ ...prevState2, withNotify: true }) + expect(susCb).toBeCalledTimes(1) + + // batching 相当于slient = true + const prevState3 = state.getState() + expect(prevState3.withBatching).toEqual(undefined) + expect(prevState3.withBatching2).toEqual(undefined) + state.batch(() => { + state.setState(cb3) + state.setState(cb4) + }) + + expect(state.getState().withBatching).toEqual(true) + expect(state.getState().withBatching2).toEqual(true) + expect(state.getState()).toEqual({ ...prevState3, withBatching: true, withBatching2: true }) + // 这次notify是由batch批处理结束调用的 + expect(susCb).toBeCalledTimes(2) + expect(susCb).toBeCalledWith({ ...prevState3, withBatching: true, withBatching2: true }) }) test('getSourceState', () => { - //todo + const state = new StateModel({ useDirty: true }) + const cb = jest.fn() + state.getState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.getState() + expect(syncState).toEqual(state.state) }) test('setSourceState', () => { - //todo - }) - test('setState', () => { - //todo + const state = new StateModel({ useDirty: true }) + const cb1 = (draft) => draft.change = true + const prevState1 = state.unsafe_getSourceState() + expect(prevState1.change).toEqual(undefined) + + state.unsafe_setSourceState(cb1) + expect(state.unsafe_getSourceState()).toEqual({ ...prevState1, change: true }) }) test('hasChanged', () => { - //todo + const state = new StateModel({ useDirty: true }) + expect(state.dirtyNum).toEqual(0) + expect(state.hasChanged()).toEqual(false) + state.dirtyNum = 1 + expect(state.hasChanged()).toEqual(true) + state.dirtyNum = 0 + expect(state.hasChanged()).toEqual(false) + state.dirtys.change = true + expect(state.hasChanged()).toEqual(false) + expect(state.hasChanged('change')).toEqual(true) }) test('getChanged', () => { - //todo + const state = new StateModel({ useDirty: true }) + expect(state.getChanged()).toEqual({}) + state.dirtys.change = true + expect(state.getChanged()).toEqual({ change: true }) + state.dirtys = { changeObj: true } + expect(state.getChanged()).toEqual({ changeObj: true }) + }) + test('hasChangedInSequence', () => { + const state = new StateModel({ useDirty: true }) + expect(state.hasChangedInSequence()).toEqual(false) + state.persistDirtys.change = true + expect(state.hasChangedInSequence()).toEqual(true) + expect(state.hasChangedInSequence('change')).toEqual(true) + state.persistDirtys.change = false + expect(state.hasChangedInSequence()).toEqual(false) + expect(state.hasChangedInSequence('change')).toEqual(false) + state.persistDirtys = {} + expect(state.hasChangedInSequence()).toEqual(false) + state.persistDirtys = { change: true } + expect(state.hasChangedInSequence()).toEqual(true) + expect(state.hasChangedInSequence('change')).toEqual(true) + }) + test('getChangedInSequence', () => { + const state = new StateModel({ useDirty: true }) + expect(state.getChangedInSequence()).toEqual({}) + state.persistDirtys.change = true + expect(state.getChangedInSequence()).toEqual({ change: true }) + state.persistDirtys = { changeObj: true } + expect(state.getChangedInSequence()).toEqual({ changeObj: true }) }) }) diff --git a/packages/core/src/__tests__/vfield.state.spec.ts b/packages/core/src/__tests__/vfield.state.spec.ts index 4c1f7679617..61102052af2 100644 --- a/packages/core/src/__tests__/vfield.state.spec.ts +++ b/packages/core/src/__tests__/vfield.state.spec.ts @@ -1,32 +1,178 @@ -//import { VirtualFieldState } from '../state/VirtualField' +import { VirtualFieldState } from '../state/virtual-field' -test('subscribe', () => { - //todo +test('computeState', () => { + const state = new VirtualFieldState({ useDirty: false }) + state.setState((draft) => { + draft.unmounted = true + }) + expect(state.getState().mounted).toEqual(false) + expect(state.getState().unmounted).toEqual(true) + state.setState((draft) => { + draft.mounted = true + }) + expect(state.getState().mounted).toEqual(true) + expect(state.getState().unmounted).toEqual(false) + // cannot set invalid props + expect(state.getState().props).toEqual({}) + state.setState((draft) => { + draft.props = { hello: 'world' } + }) + expect(state.getState().props).toEqual({ hello: 'world' }) + state.setState((draft) => { + draft.props = undefined + }) + expect(state.getState().props).toEqual({ hello: 'world' }) }) -test('unsubscribe', () => { - //todo + + +test('subscribe/unsubscribe', () => { + const state = new VirtualFieldState({ useDirty: false }) + const cb = jest.fn() + const idx = state.subscribe(cb) + const paylaod = state.getState() + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) + state.unsubscribe(idx) + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) }) test('batch', () => { - //todo + const state = new VirtualFieldState({ useDirty: false }) + const cb = jest.fn() + state.batch(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith() + // force run getState + const susCb = jest.fn() + state.subscribe(susCb) + state.dirtyNum = 1 + state.batch(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith() + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith(state.state) }) test('getState', () => { - //todo + const state = new VirtualFieldState({ useDirty: false }) + const cb = jest.fn() + state.getState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.getState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.getState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(null) }) test('setState', () => { - //todo + const state = new VirtualFieldState({ useDirty: false }) + const susCb = jest.fn() + state.subscribe(susCb) + const cb1 = (draft) => { draft.visible = false } + const cb2 = (draft) => { draft.display = false } + const cb3 = (draft) => { draft.initialized = true } + const cb4 = (draft) => { draft.mounted = true } + const prevState1 = state.getState() + expect(prevState1.visible).toEqual(true) + + // 默认 slient = false, 触发notify 通知UI更新 + state.setState(cb1) + expect(state.getState().visible).toEqual(false) + expect(state.getState()).toEqual({ ...prevState1, visible: false }) + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith({ ...prevState1, visible: false }) + + // slient = true 不触发notify + const prevState2 = state.getState() + expect(prevState2.display).toEqual(true) + state.setState(cb2, true) + expect(state.getState().display).toEqual(false) + expect(state.getState()).toEqual({ ...prevState2, display: false }) + expect(susCb).toBeCalledTimes(1) + + // batching 相当于slient = true + const prevState3 = state.getState() + expect(prevState3.initialized).toEqual(false) + expect(prevState3.mounted).toEqual(false) + state.batch(() => { + state.setState(cb3) + state.setState(cb4) + }) + + expect(state.getState().initialized).toEqual(true) + expect(state.getState().mounted).toEqual(true) + expect(state.getState()).toEqual({ ...prevState3, initialized: true, mounted: true }) + // 这次notify是由batch批处理结束调用的 + expect(susCb).toBeCalledTimes(2) + expect(susCb).toBeCalledWith({ ...prevState3, initialized: true, mounted: true }) }) test('getSourceState', () => { - //todo + const state = new VirtualFieldState({ useDirty: false }) + const cb = jest.fn() + state.unsafe_getSourceState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.unsafe_getSourceState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.unsafe_getSourceState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(state.state) }) test('setSourceState', () => { - //todo -}) -test('setState', () => { - //todo + const state = new VirtualFieldState({ useDirty: false }) + const cb1 = (draft) => draft.change = true + const prevState1 = state.unsafe_getSourceState() + expect(prevState1.change).toEqual(undefined) + + state.unsafe_setSourceState(cb1) + expect(state.unsafe_getSourceState()).toEqual({ ...prevState1, change: true }) }) test('hasChanged', () => { - //todo + const state = new VirtualFieldState({ useDirty: true }) + expect(state.dirtyNum).toEqual(0) + expect(state.hasChanged()).toEqual(false) + state.dirtyNum = 1 + expect(state.hasChanged()).toEqual(true) + state.dirtyNum = 0 + expect(state.hasChanged()).toEqual(false) + state.dirtys.visible = true + expect(state.hasChanged()).toEqual(false) + expect(state.hasChanged('visible')).toEqual(true) }) test('getChanged', () => { - //todo + const state = new VirtualFieldState({ useDirty: true }) + expect(state.getChanged()).toEqual({}) + state.dirtys.visible = true + expect(state.getChanged()).toEqual({ visible: true }) + state.dirtys = { display: true } + expect(state.getChanged()).toEqual({ display: true }) +}) +test('hasChangedInSequence', () => { + const state = new VirtualFieldState({ useDirty: true }) + expect(state.hasChangedInSequence()).toEqual(false) + state.persistDirtys.visible = true + expect(state.hasChangedInSequence()).toEqual(true) + expect(state.hasChangedInSequence('visible')).toEqual(true) + state.persistDirtys.visible = false + expect(state.hasChangedInSequence()).toEqual(false) + expect(state.hasChangedInSequence('visible')).toEqual(false) + state.persistDirtys = {} + expect(state.hasChangedInSequence()).toEqual(false) + state.persistDirtys = { visible: true } + expect(state.hasChangedInSequence()).toEqual(true) + expect(state.hasChangedInSequence('visible')).toEqual(true) }) +test('getChangedInSequence', () => { + const state = new VirtualFieldState({ useDirty: true }) + expect(state.getChangedInSequence()).toEqual({}) + state.persistDirtys.visible = true + expect(state.getChangedInSequence()).toEqual({ visible: true }) + state.persistDirtys = { display: true } + expect(state.getChangedInSequence()).toEqual({ display: true }) +}) \ No newline at end of file diff --git a/packages/core/src/shared/model.ts b/packages/core/src/shared/model.ts index 6508c1c385e..9a2605c0b43 100644 --- a/packages/core/src/shared/model.ts +++ b/packages/core/src/shared/model.ts @@ -39,6 +39,7 @@ export const createStateModel = ( this.persistDirtys = {} this.dirtyNum = 0 this.batching = false + this.processing = false this.controller = new Factory(this.state, this.props) this.displayName = Factory.displayName this.state.displayName = this.displayName @@ -61,10 +62,11 @@ export const createStateModel = ( if (isFn(callback)) { return callback(this.getState()) } else { - if (!hasProxy || this.props.useDirty) { - if (isFn(this.controller.publishState)) { - return this.controller.publishState(this.state) - } + if (isFn(this.controller.publishState)) { + return this.controller.publishState(this.state) + } + + if (!hasProxy || this.props.useDirty) { return clone(this.state) } else { return produce(this.state, () => {}) @@ -85,7 +87,9 @@ export const createStateModel = ( if (!hasProxy || this.props.useDirty) { callback(this.state) } else { - this.state = produce(this.state, callback) + this.state = produce(this.state, (draft) => { + callback(draft) + }) } } } @@ -111,17 +115,15 @@ export const createStateModel = ( } const draftKeys = Object.keys(draft || {}) const stateKeys = Object.keys(this.state || {}) - each( - draftKeys.length > stateKeys.length ? draft : this.state, - (value, key) => { - if (!isEqual(value, draft[key])) { - this.state[key] = draft[key] - this.dirtys[key] = true - this.persistDirtys[key] = true - this.dirtyNum++ - } + + each(draftKeys.length > stateKeys.length ? draft : this.state, (value, key) => { + if (!isEqual(this.state[key], draft[key])) { + this.state[key] = draft[key] + this.dirtys[key] = true + this.persistDirtys[key] = true + this.dirtyNum++ } - ) + }) if (isFn(this.controller.dirtyCheck)) { const result = this.controller.dirtyCheck(this.dirtys) if (result !== undefined) { @@ -134,7 +136,7 @@ export const createStateModel = ( this.dirtys = {} this.dirtyNum = 0 } - } else { + } else { if (!this.batching) { this.dirtys = {} this.dirtyNum = 0 @@ -193,7 +195,7 @@ export const createStateModel = ( hasChangedInSequence = (key?: string) => key ? this.persistDirtys[key] - : Object.keys(this.persistDirtys || {}).length > 0 + : Object.keys(this.persistDirtys || {}).filter(k => this.persistDirtys[k] === true).length > 0 getChanged = () => this.dirtys diff --git a/packages/core/src/state/form.ts b/packages/core/src/state/form.ts index cfc05ade446..9a2010a0d7a 100644 --- a/packages/core/src/state/form.ts +++ b/packages/core/src/state/form.ts @@ -1,5 +1,5 @@ import { createStateModel } from '../shared/model' -import { toArr, clone, isEqual } from '@uform/shared' +import { toArr, clone, isEqual, isValid } from '@uform/shared' import { IFormState, IFormStateProps } from '../types' /** * 核心数据结构,描述Form级别状态 @@ -47,6 +47,9 @@ export const FormState = createStateModel( draft.invalid = false draft.valid = true } + if (!isValid(draft.props)) { + draft.props = prevState.props + } if (isEqual(draft.values, draft.initialValues)) { draft.pristine = true } else { @@ -57,10 +60,10 @@ export const FormState = createStateModel( } else if (draft.validating === false) { draft.loading = false } - if (draft.mounted === true) { + if (draft.mounted === true && draft.mounted !== prevState.mounted) { draft.unmounted = false } - if (draft.unmounted === true) { + if (draft.unmounted === true && draft.unmounted !== prevState.unmounted) { draft.mounted = false } } From 86dd3873f2749f6f69217a6bb2d2fdff7d14f70a Mon Sep 17 00:00:00 2001 From: janryWang Date: Sun, 3 Nov 2019 20:24:31 +0800 Subject: [PATCH 14/99] fix(@uform/core): fix visible/display behave --- .../__snapshots__/index.spec.ts.snap | 34 ++++++++----------- packages/core/src/index.ts | 27 +++++++++------ packages/core/src/types.ts | 2 +- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap b/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap index a38a1040f8d..09f00018cff 100644 --- a/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap +++ b/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap @@ -893,7 +893,7 @@ Object { "path": "aa.bb", "props": Object {}, "unmounted": false, - "visible": true, + "visible": false, }, "aa.bb.cc": Object { "active": false, @@ -927,7 +927,7 @@ Object { "values": Array [ 123, ], - "visible": true, + "visible": false, "visited": false, "warnings": Array [], }, @@ -1072,7 +1072,7 @@ Object { "values": Array [ 123, ], - "visible": true, + "visible": false, "visited": false, "warnings": Array [], }, @@ -1179,7 +1179,7 @@ Object { "cc": 123, }, ], - "visible": true, + "visible": false, "visited": false, "warnings": Array [], }, @@ -1215,7 +1215,7 @@ Object { "values": Array [ 123, ], - "visible": true, + "visible": false, "visited": false, "warnings": Array [], }, @@ -2670,7 +2670,7 @@ Object { "values": Array [ 123, ], - "visible": true, + "visible": false, "visited": false, "warnings": Array [], }, @@ -2706,7 +2706,7 @@ Object { "values": Array [ 222, ], - "visible": true, + "visible": false, "visited": false, "warnings": Array [], }, @@ -2735,11 +2735,7 @@ Object { "unmounted": false, "valid": true, "validating": false, - "values": Object { - "aa": Object { - "bb": "123", - }, - }, + "values": Object {}, "warnings": Array [], }, "aa": Object { @@ -2800,11 +2796,11 @@ Object { "initialized": true, "invalid": false, "loading": false, - "modified": true, + "modified": false, "mounted": false, "name": "aa.bb", "path": "aa.bb", - "pristine": false, + "pristine": true, "props": Object {}, "required": false, "ruleErrors": Array [], @@ -2815,11 +2811,11 @@ Object { "unmounted": false, "valid": true, "validating": false, - "value": "123", + "value": 123, "values": Array [ - "123", + 123, ], - "visible": true, + "visible": false, "visited": false, "warnings": Array [], }, @@ -2855,7 +2851,7 @@ Object { "values": Array [ 222, ], - "visible": true, + "visible": false, "visited": false, "warnings": Array [], }, @@ -2950,7 +2946,7 @@ Object { "initialized": true, "invalid": false, "loading": false, - "modified": true, + "modified": false, "mounted": false, "name": "aa.bb", "path": "aa.bb", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ce29957fd20..6c60c722038 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -169,26 +169,33 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { }, true) } } - if (visibleChanged) { - //visible不能遍历子节点控制其visible,visible只对当前节点生效 - if (!published.visible) { - deleteFormValuesIn(path, true) - } else { - setFormValuesIn(path, published.value) + if (displayChanged || visibleChanged) { + if (visibleChanged) { + if (!published.visible) { + deleteFormValuesIn(path, true) + } else { + setFormValuesIn(path, published.value) + } } - } - if (displayChanged) { graph.eachChildren( path, childState => { childState.setState((state: IFieldState) => { - state.display = published.display + if (visibleChanged) { + state.visible = published.visible + } + if (displayChanged) { + state.display = published.display + } }) }, false ) } - if (unmountedChanged) { + if ( + unmountedChanged && + (published.display !== false || published.visible === false) + ) { if (published.unmounted) { deleteFormValuesIn(path, true) } else { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 8521b4a0071..b001e805e96 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -272,7 +272,7 @@ export interface IModel extends Subscribable { dirtys: StateDirtyMap persistDirtys: StateDirtyMap batching: boolean - processFlag: number + stackCount: number controller: StateModel batch: (callback?: () => void) => void getState: (callback?: (state: S) => any) => any From 696bcc6a3d5ffe83396347c4d16cc98e2e4589ce Mon Sep 17 00:00:00 2001 From: janryWang Date: Sun, 3 Nov 2019 20:30:50 +0800 Subject: [PATCH 15/99] refactor(@uform/core): merge v1 --- packages/core/src/shared/model.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/shared/model.ts b/packages/core/src/shared/model.ts index 8759e880614..7c3d60a6cbe 100644 --- a/packages/core/src/shared/model.ts +++ b/packages/core/src/shared/model.ts @@ -40,7 +40,6 @@ export const createStateModel = ( this.dirtyNum = 0 this.stackCount = 0 this.batching = false - this.processing = false this.controller = new Factory(this.state, this.props) this.displayName = Factory.displayName this.state.displayName = this.displayName From 56835f9e6471a3c076162281d96d4eecf263fbcf Mon Sep 17 00:00:00 2001 From: janryWang Date: Sun, 3 Nov 2019 20:42:07 +0800 Subject: [PATCH 16/99] refactor(@uform/core): remove processing test case --- packages/core/src/__tests__/model.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/__tests__/model.spec.ts b/packages/core/src/__tests__/model.spec.ts index fe313c00331..788f9e575bb 100644 --- a/packages/core/src/__tests__/model.spec.ts +++ b/packages/core/src/__tests__/model.spec.ts @@ -34,7 +34,6 @@ test('createStateModel', () => { expect(state1.persistDirtys).toEqual({}) expect(state1.dirtyNum).toEqual(0) expect(state1.batching).toEqual(false) - expect(state1.processing).toEqual(false) expect(state1.displayName).toEqual(displayName) expect(state1.controller).toEqual({ state: { displayName, ...defaultState }, From c3e42d4fdb8f890aff459576fe94d286c56ff3ef Mon Sep 17 00:00:00 2001 From: zirkleTsing Date: Sun, 3 Nov 2019 23:03:36 +0800 Subject: [PATCH 17/99] test(@uform/react-schema-renderer): add old test case --- .../src/__tests__/actions.spec.tsx | 77 ++ .../src/__tests__/context.spec.tsx | 58 ++ .../src/__tests__/destruct.spec.tsx | 66 ++ .../src/__tests__/display.spec.tsx | 149 ++++ .../src/__tests__/dynamic.spec.tsx | 688 ++++++++++++++++++ .../src/__tests__/editable.spec.tsx | 103 +++ 6 files changed, 1141 insertions(+) create mode 100644 packages/react-schema-renderer/src/__tests__/actions.spec.tsx create mode 100644 packages/react-schema-renderer/src/__tests__/context.spec.tsx create mode 100644 packages/react-schema-renderer/src/__tests__/destruct.spec.tsx create mode 100644 packages/react-schema-renderer/src/__tests__/display.spec.tsx create mode 100644 packages/react-schema-renderer/src/__tests__/dynamic.spec.tsx create mode 100644 packages/react-schema-renderer/src/__tests__/editable.spec.tsx diff --git a/packages/react-schema-renderer/src/__tests__/actions.spec.tsx b/packages/react-schema-renderer/src/__tests__/actions.spec.tsx new file mode 100644 index 00000000000..639eebc3fb5 --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/actions.spec.tsx @@ -0,0 +1,77 @@ +import React, { Fragment } from 'react' +import { + registerFormField, + connect, + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, + createFormActions +} from '../index' + +import { render, wait } from '@testing-library/react' + +beforeEach(() => { + registerFormField('string', connect()(props => )) +}) + +test('createFormActions', async () => { + const actions = createFormActions() + const TestComponent = () => ( + { + $('onFormInit').subscribe(() => { + setFieldState('aaa', state => { + state.value = 'change value of aaa field onFormInit' + }) + }) + }} + > + + + + + + ) + + const { queryByTestId } = render() + expect(queryByTestId('inputA').getAttribute('value')).toEqual('change value of aaa field onFormInit') + await actions.setFormState(state => (state.values = { aaa: 123 })) + await wait(() => { + expect(queryByTestId('inputA').getAttribute('value')).toEqual('123') + }) + await actions.setFieldState('aaa', state => (state.value = 'hello world')) + await wait(() => { + expect(queryByTestId('inputA').getAttribute('value')).toEqual('hello world') + }) + const VALUE_A = 'value of aaa field' + const VALUE_B = 'value of bbb field' + const schemaData = [ + { name: 'aaa', value: VALUE_A }, + { name: 'bbb', value: VALUE_B } + ] + const updateQueue = [] + schemaData.forEach(({ name, value }) => { + updateQueue.push( + actions.setFieldState(name, state => { + state.value = value + }) + ) + }) + await Promise.all(updateQueue) + await wait(() => { + expect(queryByTestId('inputA').getAttribute('value')).toEqual(VALUE_A) + expect(queryByTestId('inputB').getAttribute('value')).toEqual(VALUE_B) + }) +}) \ No newline at end of file diff --git a/packages/react-schema-renderer/src/__tests__/context.spec.tsx b/packages/react-schema-renderer/src/__tests__/context.spec.tsx new file mode 100644 index 00000000000..49598f4d67b --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/context.spec.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import { + registerFormField, + connect, + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, + createFormActions, + FormProvider, + FormConsumer, +} from '../index' +import { render, fireEvent, act, wait } from '@testing-library/react' + +const sleep = async (timeout) => { + const noop = () => {} + await wait(noop, timeout) +} + +beforeEach(() => { + registerFormField( + 'string', + connect()(props => ) + ) +}) + +test('submit by form consumer', async () => { + const actions = createFormActions() + const TestComponent = () => ( + + { + await sleep(200) + }} + > + + + + {({ submit, status }) => { + if (status === 'submitting') { + return
    Submitting
    + } else { + return + } + }} +
    +
    + ) + + const { queryByText } = render() + act(() => { + fireEvent.click(queryByText('Submit')) + }) + // await sleep(30) + // expect(queryByText('Submitting')).toBeVisible() + // await sleep(300) + // expect(queryByText('Submitting')).toBeNull() + // expect(queryByText('Submit')).not.toBeUndefined() +}) diff --git a/packages/react-schema-renderer/src/__tests__/destruct.spec.tsx b/packages/react-schema-renderer/src/__tests__/destruct.spec.tsx new file mode 100644 index 00000000000..a8370e0e006 --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/destruct.spec.tsx @@ -0,0 +1,66 @@ +import React, { Fragment } from 'react' +import { + connect, + registerFormField, + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field +} from '../index' +import { toArr } from '@uform/shared' +import { render, wait } from '@testing-library/react' + +beforeEach(() => { + registerFormField( + 'string', + connect()(props => ) + ) + + registerFormField('array', props => { + const { value, renderField } = props + return ( + + {toArr(value).map((item, index) => { + return ( +
    + {renderField(index)} +
    + ) + })} +
    + ) + }) +}) + +test('destruct with initial values', async () => { + const TestComponent = () => { + return ( + + + + ) + } + + const { queryByText } = render() + wait(() => { + expect(queryByText('123321')).toBeNull() + }) +}) + +test('destruct with initial values in array', async () => { + const TestComponent = () => { + return ( + + + + + + + + ) + } + + const { queryByText } = render() + + await wait(() => { + expect(queryByText('123321')).toBeNull() + }) +}) diff --git a/packages/react-schema-renderer/src/__tests__/display.spec.tsx b/packages/react-schema-renderer/src/__tests__/display.spec.tsx new file mode 100644 index 00000000000..75928251c7d --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/display.spec.tsx @@ -0,0 +1,149 @@ +import React, { Fragment } from 'react' +import { + registerFormField, + connect, + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field +} from '../index' +import { render, fireEvent, wait } from '@testing-library/react' + +beforeEach(() => { + registerFormField('string', connect()(props =>
    {props.value}
    )) +}) + +test('display is false will remove react node', async () => { + const TestComponent = () => { + return ( + { + $('onFormInit').subscribe(() => { + setFieldState('aa', state => { + state.display = false + }) + }) + }} + > + + + ) + } + + const { queryByText } = render() + await wait(() => { + expect(queryByText('123321')).toBeNull() + }) +}) + +test('display is false will remove react children node', async () => { + const TestComponent = () => { + return ( + { + $('onFormInit').subscribe(() => { + setFieldState('obj', state => { + state.display = false + }) + }) + }} + > + + + + + ) + } + + const { queryByText } = render() + wait(() => { + expect(queryByText('123321')).toBeNull() + }) +}) + +test('display is false will not remove value(include default value)', async () => { + const onSubmitHandler = jest.fn() + const TestComponent = () => { + return ( + { + $('onFieldChange', 'bb').subscribe(({ value }) => { + if (value === '123') { + setFieldState('obj', state => { + state.display = false + }) + } + }) + }} + > + + + + + + + + + ) + } + + const { queryByText } = render() + + await wait(() => { + expect(queryByText('123321')).toBeNull() + }) + fireEvent.click(queryByText('Submit')) + await wait(() => { + expect(onSubmitHandler).toHaveBeenCalledWith({ + obj: { aa: '123321' }, + bb: '123' + }) + }) +}) + +test('display is false will not validate(include children)', async () => { + const onSubmitHandler = jest.fn() + const onValidateFailedHandler = jest.fn() + const TestComponent = () => { + return ( + { + $('onFieldChange', 'bb').subscribe(({ value }) => { + if (value === '123') { + setFieldState('obj', state => { + state.display = false + }) + } + }) + }} + > + + + + + + + + + ) + } + + const { queryByText } = render() + + await wait(() => { + expect(queryByText('123321')).toBeNull() + }) + fireEvent.click(queryByText('Submit')) + await wait(() => { + expect(onSubmitHandler).toHaveBeenCalledWith({ + obj: { aa: '123321' }, + bb: '123' + }) + expect(onValidateFailedHandler).toHaveBeenCalledTimes(0) + }) +}) + +// display 有问题 \ No newline at end of file diff --git a/packages/react-schema-renderer/src/__tests__/dynamic.spec.tsx b/packages/react-schema-renderer/src/__tests__/dynamic.spec.tsx new file mode 100644 index 00000000000..e64524dda47 --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/dynamic.spec.tsx @@ -0,0 +1,688 @@ +import React, { Fragment, useEffect, useState } from 'react' +import { + registerFormField, + connect, + FormPath, + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, + createFormActions, + createVirtualBox +} from '../index' +import { toArr } from '@uform/shared' +import { render, fireEvent, wait, act } from '@testing-library/react' + +let FormCard +beforeEach(() => { + jest.setTimeout(10000) + registerFormField( + 'string', + connect()(props => ( + + )) + ) + + registerFormField( + 'radio', + connect()(props => + props.dataSource.map(item => ( + + )) + ) + ) + + registerFormField('container', props => { + const { value, mutators, renderField } = props + return ( + + {toArr(value).map((item, index) => { + return ( +
    + {renderField(index)} + +
    + ) + })} + +
    + ) + }) + + FormCard = createVirtualBox('card', ({ children }) => { + return
    card content{children}
    + }) +}) + +test('dynaimc add field', async () => { + const actions = createFormActions() + const TestComponent = () => { + return ( + { + $('onFormInit').subscribe(() => { + setFieldState(FormPath.match('container.*.bb') as any, state => { + state.visible = false + }) + }) + + $('onFieldChange', FormPath.match('container.0.aa')).subscribe( + state => { + if (state.value == '123') { + setFieldState( + FormPath.transform(state.name, /\d/, $1 => { + return `container.${$1}.bb` + }), + state => { + state.visible = true + } + ) + } + } + ) + }} + > + + + + + + + + ) + } + + const { queryAllByTestId, queryByText } = render() + await wait(() => { + expect(queryAllByTestId('item').length).toBe(0) + expect(queryAllByTestId('input').length).toBe(0) + }) + + await wait(() => { + fireEvent.click(queryByText('Add Field')) + }) + await wait(() => { + expect(queryAllByTestId('item').length).toBe(1) + expect(queryAllByTestId('input').length).toBe(1) + }) + + await actions.setFieldState('container.0.bb', state => { + state.visible = true + }) + await wait(() => { + expect(queryAllByTestId('item').length).toBe(1) + expect(queryAllByTestId('input').length).toBe(2) + }) + await actions.setFieldState('container.0.bb', state => { + state.visible = false + }) + await wait(() => { + expect(queryAllByTestId('item').length).toBe(1) + expect(queryAllByTestId('input').length).toBe(1) + }) + await actions.setFieldState('container.0.aa', state => { + state.value = '123' + }) + await wait(() => { + expect(queryAllByTestId('item').length).toBe(1) + expect(queryAllByTestId('input').length).toBe(2) + }) +}) + +test('dynaimc add field with initialValue', async () => { + const actions = createFormActions() + const TestComponent = () => { + return ( + { + $('onFormInit').subscribe(() => { + setFieldState(FormPath.match('container.*.bb') as any, state => { + state.visible = false + }) + }) + + $('onFieldChange', FormPath.match('container.*.aa')).subscribe( + state => { + if (state.value) { + if (state.value === '123') { + setFieldState( + FormPath.transform(state.name, /\d/, $1 => { + return `container.${$1}.bb` + }), + state => { + state.visible = true + } + ) + } else { + setFieldState( + FormPath.transform(state.name, /\d/, $1 => { + return `container.${$1}.bb` + }), + state => { + state.visible = false + } + ) + } + } + } + ) + }} + > + + + + + + + + ) + } + + const { queryAllByTestId, queryByText } = render() + await wait(() => { + expect(queryAllByTestId('item').length).toBe(1) + expect(queryAllByTestId('input').length).toBe(1) + }) + fireEvent.click(queryByText('Add Field')) + await wait(() => { + expect(queryAllByTestId('item').length).toBe(2) + expect(queryAllByTestId('input').length).toBe(2) + }) + await actions.setFieldState('container.0.bb', state => { + state.visible = true + }) + await wait(() => { + expect(queryAllByTestId('item').length).toBe(2) + expect(queryAllByTestId('input').length).toBe(3) + }) + await actions.setFieldState('container.0.bb', state => { + state.visible = false + }) + await wait(() => { + expect(queryAllByTestId('item').length).toBe(2) + expect(queryAllByTestId('input').length).toBe(2) + }) + await actions.setFieldState('container.0.aa', state => { + state.value = '123' + }) + await wait(() => { + expect(queryAllByTestId('item').length).toBe(2) + expect(queryAllByTestId('input').length).toBe(3) + }) + await actions.setFieldState('container.0.aa', state => { + state.value = '321' + }) + await wait(() => { + expect(queryAllByTestId('item').length).toBe(2) + expect(queryAllByTestId('input').length).toBe(2) + }) +}) + +test('dynaimc add field with initialValue in virtualbox', async () => { + const actions = createFormActions() + const submitHandler = jest.fn() + const TestComponent = () => { + return ( + { + $('onFormInit').subscribe(() => { + setFieldState(FormPath.match('container.*.bb') as any, state => { + state.visible = false + }) + }) + + $('onFieldChange', FormPath.match('container.*.aa')).subscribe( + state => { + if (state.value) { + if (state.value === '123') { + setFieldState( + FormPath.transform(state.name, /\d/, $1 => { + return `container.${$1}.bb` + }), + state => { + state.visible = true + } + ) + } else { + setFieldState( + FormPath.transform(state.name, /\d/, $1 => { + return `container.${$1}.bb` + }), + state => { + state.visible = false + } + ) + } + } + } + ) + }} + > + + + + + + + + + + + + + ) + } + + const { queryAllByTestId, queryByText } = render() + await wait(() => { + expect(queryAllByTestId('item').length).toBe(1) + expect(queryAllByTestId('input').length).toBe(1) + }) + fireEvent.click(queryByText('Add Field')) + await wait(() => { + expect(queryAllByTestId('item').length).toBe(2) + expect(queryAllByTestId('input').length).toBe(2) + }) + await actions.setFieldState('container.0.bb', state => { + state.visible = true + }) + await wait(() => { + expect(queryAllByTestId('item').length).toBe(2) + expect(queryAllByTestId('input').length).toBe(3) + }) + await actions.setFieldState('container.0.bb', state => { + state.visible = false + }) + await wait(() => { + expect(queryAllByTestId('item').length).toBe(2) + expect(queryAllByTestId('input').length).toBe(2) + }) + await actions.setFieldState('container.0.aa', state => { + state.value = '123' + }) + await wait(() => { + expect(queryAllByTestId('item').length).toBe(2) + expect(queryAllByTestId('input').length).toBe(3) + }) + await actions.setFieldState('container.0.aa', state => { + state.value = '321' + }) + await wait(() => { + expect(queryAllByTestId('item').length).toBe(2) + expect(queryAllByTestId('input').length).toBe(2) + }) + fireEvent.click(queryByText('Submit')) + await wait(() => { + expect(submitHandler).toHaveBeenCalledWith({ + container: [{ aa: '321' }, { aa: '' }] + }) + }) +}) + +test('dynamic remove field', async () => { + const submitHandler = jest.fn() + const validateFaildHandler = jest.fn() + const TestComponent = () => { + return ( + + + + + + + + + + + + + + ) + } + + const { queryAllByTestId, queryByText } = render() + await wait(() => { + fireEvent.click(queryByText('Add Field')) + }) + await wait(() => { + fireEvent.click(queryByText('Remove Field')) + }) + await wait(() => { + fireEvent.click(queryByText('Submit')) + }) + await wait(() => { + expect(submitHandler).toHaveBeenCalledTimes(1) + expect(validateFaildHandler).toHaveBeenCalledTimes(0) + }) + fireEvent.click(queryByText('Add Field')) + await wait(() => { + expect(queryAllByTestId('item').length).toBe(1) + expect(queryAllByTestId('input').length).toBe(2) + }) + await wait(() => { + fireEvent.click(queryByText('Remove Field')) + fireEvent.click(queryByText('Submit')) + }) + await wait(() => { + expect(submitHandler).toHaveBeenCalledTimes(2) + expect(validateFaildHandler).toHaveBeenCalledTimes(0) + }) +}) + +test('dynamic default value', async () => { + const TestComponent = () => { + return ( + + + + + + + + + + + + + + ) + } + + const { queryAllByTestId } = render() + expect(queryAllByTestId('item').length).toBe(1) + expect(queryAllByTestId('input').length).toBe(2) +}) + +test('invalid schema', async () => { + const TestComponent = () => { + const [schema, setSchema] = useState() + useEffect(() => { + setTimeout(() => { + act(() => { + setSchema({ + type: 'object', + properties: { + container: { + type: 'array', + default: [{}], + 'x-component': 'container', + properties: {}, + items: { + type: 'object', + properties: { + aa: { + type: 'string' + }, + bb: { + type: 'string' + } + } + } + } + } + }) + }) + }, 30) + }, []) + return ( + + + + ) + } + const { queryByText, queryAllByTestId } = render() + await wait(() => { + fireEvent.click(queryByText('Add Field')) + }) + await wait(() => { + expect(queryAllByTestId('item').length).toBe(2) + expect(queryAllByTestId('input').length).toBe(4) + }) +}) + +test('dynamic change functions onChange/onReset/onSubmit/onValidateFailed', async () => { + const actions = createFormActions() + const TestComponent = () => { + const [state, setState] = useState<{[key: string]: string}>({ testA: '123' }) + const constState = { ...state } + useEffect(() => { + setTimeout(() => { + act(() => setState({ testA: `${Math.random()}` })) + }, 10) + }, []) + console.log('outer constState:', constState.testA) + return ( + + { + // why + console.log('inner constState:', constState.testA) + if (constState.testA !== '123') { + act(() => setState({ testB: '456' })) + } + }} + onReset={() => { + if (constState.testA !== '123') { + act(() => setState({ testC: '456' })) + } + }} + onSubmit={() => { + if (constState.testA !== '123') { + act(() => setState({ testD: '456' })) + } + }} + onValidateFailed={() => { + if (constState.testA !== '123') { + act(() => setState({ testE: '456' })) + } + }} + > + + + + + + +
    valueB-{constState.testB}
    +
    valueC-{constState.testC}
    +
    valueD-{constState.testD}
    +
    valueE-{constState.testE}
    +
    + ) + } + const { queryByTestId, queryAllByText, queryByText } = render() + await wait(() => { + fireEvent.click(queryByTestId('radio-a2')) + }) + await wait(() => { + // onChange + expect(queryAllByText('valueB-456').length).toBe(1) + }) + await actions.reset() + await wait(() => { + // onReset + expect(queryAllByText('valueC-456').length).toBe(1) + }) + fireEvent.click(queryByText('Submit')) + await wait(() => { + // onValidateFailed + expect(queryAllByText('valueE-456').length).toBe(1) + }) + fireEvent.click(queryByTestId('radio-b2')) + fireEvent.click(queryByText('Submit')) + await wait(() => { + // onSubmit + expect(queryAllByText('valueD-456').length).toBe(1) + }) +}) + +test('dynamic remove field and relationship needs to be retained', async () => { + const TestComponent = () => { + return ( + { + $('onFieldChange', 'container.*.bb').subscribe(async ({ value, name }) => { + const siblingName = FormPath.transform(name, /\d+/, $d => { + return `container.${$d}.aa` + }) + await setFieldState(siblingName, state => { + state.visible = value !== '123' + }) + }) + }} + > + + + + + + + + + + + + + ) + } + + const { queryAllByTestId, queryByText, queryAllByText } = render( + + ) + await wait(() => { + expect(queryAllByTestId('input').length).toBe(2) + }) + let removes + await wait(() => { + removes = queryAllByText('Remove Field') + }) + fireEvent.click(removes[removes.length - 1]) + await wait(() => { + removes = queryAllByText('Remove Field') + }) + fireEvent.click(removes[removes.length - 1]) + await wait(() => { + expect(queryAllByTestId('input').length).toBe(0) + }) + await wait(() => { + fireEvent.click(queryByText('Add Field')) + }) + await wait(() => { + fireEvent.click(queryByText('Add Field')) + }) + await wait(() => { + expect(queryAllByTestId('input').length).toBe(4) + expect(queryAllByTestId('input')[0].getAttribute('value')).toBe('') + expect(queryAllByTestId('input')[1].getAttribute('value')).toBe('') + }) +}) + +test('after deleting a component should not be sync an default value', async () => { + const TestComponent = () => { + return ( + { + $('onFieldChange', 'container.*.bb').subscribe(({ value, name }) => { + const siblingName = FormPath.transform(name, /\d+/, $d => { + return `container.${$d}.aa` + }) + setFieldState(siblingName, state => { + state.visible = value === '123' + }) + }) + }} + > + + + + + + + + + + + ) + } + + const { queryAllByTestId, queryByText, queryAllByText } = render( + + ) + await wait(() => { + expect(queryAllByTestId('input').length).toBe(4) + }) + let removes + await wait(() => { + removes = queryAllByText('Remove Field') + }) + fireEvent.click(removes[removes.length - 1]) + await wait(() => { + removes = queryAllByText('Remove Field') + }) + fireEvent.click(removes[removes.length - 1]) + await wait(() => { + expect(queryAllByTestId('input').length).toBe(0) + }) + fireEvent.click(queryByText('Add Field')) + fireEvent.click(queryByText('Add Field')) + await wait(() => { + expect(queryAllByTestId('input').length).toBe(2) + expect(queryAllByTestId('input')[0].getAttribute('value')).toBe('') + expect(queryAllByTestId('input')[1].getAttribute('value')).toBe('') + + }) +}) \ No newline at end of file diff --git a/packages/react-schema-renderer/src/__tests__/editable.spec.tsx b/packages/react-schema-renderer/src/__tests__/editable.spec.tsx new file mode 100644 index 00000000000..c4e08f18e8d --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/editable.spec.tsx @@ -0,0 +1,103 @@ +import React, { Fragment } from 'react' +import { + registerFormField, + connect, + // FormPath, + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, + createFormActions, + // createVirtualBox, + registerFieldMiddleware +} from '../index' +import { toArr } from '@uform/shared' +import { render, wait, /* fireEvent, act */ } from '@testing-library/react' + +registerFieldMiddleware(Field => { + return props => { + if (typeof props.editable === 'boolean' && props.name !== '') { + if (!props.editable) return
    empty
    + } + return ( +
    + {props.schema.title} + + {props.errors && props.errors.length ? ( +
    {props.errors}
    + ) : ( + '' + )} +
    + ) + } +}) + +beforeEach(() => { + jest.setTimeout(10000) + registerFormField( + 'string', + connect()(props => ) + ) + registerFormField('array', props => { + const { value, mutators, renderField } = props + return ( + + {toArr(value).map((item, index) => { + return ( +
    + {renderField(index)} +
    + ) + })} + +
    + ) + }) +}) + +// test('update editable by setFieldState', async () => { +// const actions = createFormActions() +// const TestComponent = () => ( +// { +// $('onFormInit').subscribe(() => { +// setFieldState('aaa', state => { +// state.props.title = 'text' +// state.rules = [ +// { +// required: true, +// message: 'field is required' +// } +// ] +// state.props.editable = false +// }) +// }) +// }} +// > +// +// +// +// +// +// ) + +// const { queryByText } = render() +// await wait(() => { +// expect(queryByText('text')).toBeNull() +// }) +// await actions.setFieldState('aaa', state => { +// state.editable = true +// }) +// await wait(() => { +// expect(queryByText('text')).toBeVisible() +// }) +// }) From 51c04998987a78d8fbe4c4abca45698debcf948a Mon Sep 17 00:00:00 2001 From: zirkleTsing Date: Sun, 3 Nov 2019 23:18:57 +0800 Subject: [PATCH 18/99] test(@uform/react-schema-renderer): add old test case --- package.json | 1 + .../src/__tests__/editable.spec.tsx | 78 +++++++++---------- scripts/global.js | 9 --- scripts/global.ts | 10 +++ scripts/jest.base.js | 2 +- 5 files changed, 51 insertions(+), 49 deletions(-) delete mode 100644 scripts/global.js create mode 100644 scripts/global.ts diff --git a/package.json b/package.json index 20829c6663f..e0723d33d9b 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@babel/preset-react": "^7.0.0", "@babel/register": "^7.0.0", "@babel/runtime-corejs3": "^7.2.0", + "@testing-library/jest-dom": "^4.2.3", "@testing-library/react": "^8.0.0", "@types/jest": "^24.0.18", "@types/node": "^12.6.8", diff --git a/packages/react-schema-renderer/src/__tests__/editable.spec.tsx b/packages/react-schema-renderer/src/__tests__/editable.spec.tsx index c4e08f18e8d..4d76c0bef37 100644 --- a/packages/react-schema-renderer/src/__tests__/editable.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/editable.spec.tsx @@ -61,43 +61,43 @@ beforeEach(() => { }) }) -// test('update editable by setFieldState', async () => { -// const actions = createFormActions() -// const TestComponent = () => ( -// { -// $('onFormInit').subscribe(() => { -// setFieldState('aaa', state => { -// state.props.title = 'text' -// state.rules = [ -// { -// required: true, -// message: 'field is required' -// } -// ] -// state.props.editable = false -// }) -// }) -// }} -// > -// -// -// -// -// -// ) +test('update editable by setFieldState', async () => { + const actions = createFormActions() + const TestComponent = () => ( + { + $('onFormInit').subscribe(() => { + setFieldState('aaa', state => { + state.props.title = 'text' + state.rules = [ + { + required: true, + message: 'field is required' + } + ] + state.props.editable = false + }) + }) + }} + > + + + + + + ) -// const { queryByText } = render() -// await wait(() => { -// expect(queryByText('text')).toBeNull() -// }) -// await actions.setFieldState('aaa', state => { -// state.editable = true -// }) -// await wait(() => { -// expect(queryByText('text')).toBeVisible() -// }) -// }) + const { queryByText } = render() + await wait(() => { + expect(queryByText('text')).toBeNull() + }) + await actions.setFieldState('aaa', state => { + state.editable = true + }) + await wait(() => { + expect(queryByText('text')).toBeVisible() + }) +}) diff --git a/scripts/global.js b/scripts/global.js deleted file mode 100644 index 089db379a6b..00000000000 --- a/scripts/global.js +++ /dev/null @@ -1,9 +0,0 @@ -import prettyFormat from 'pretty-format' - -global.prettyFormat = prettyFormat - -global.sleep = time => { - return new Promise(resolve => setTimeout(resolve, time)) -} - -global.requestAnimationFrame = fn => setTimeout(fn) diff --git a/scripts/global.ts b/scripts/global.ts new file mode 100644 index 00000000000..0b28d7f2bf5 --- /dev/null +++ b/scripts/global.ts @@ -0,0 +1,10 @@ +import prettyFormat from 'pretty-format' +import '@testing-library/jest-dom/extend-expect' + +global['prettyFormat'] = prettyFormat + +global['sleep'] = time => { + return new Promise(resolve => setTimeout(resolve, time)) +} + +global['requestAnimationFrame'] = fn => setTimeout(fn) diff --git a/scripts/jest.base.js b/scripts/jest.base.js index 79e5ea9ce32..3487b2e06e1 100644 --- a/scripts/jest.base.js +++ b/scripts/jest.base.js @@ -26,7 +26,7 @@ module.exports = { setupFilesAfterEnv: [ require.resolve('jest-dom/extend-expect'), require.resolve('@testing-library/react/cleanup-after-each'), - './scripts/global.js' + './scripts/global.ts' ], moduleNameMapper: process.env.TEST_ENV === 'production' ? undefined : alias, globals: { From e9333042a27119599e9751aa50ba736a0a19d18b Mon Sep 17 00:00:00 2001 From: "jinc.cjc" Date: Mon, 4 Nov 2019 12:16:15 +0800 Subject: [PATCH 19/99] feat: onFieldChange types --- packages/core/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ce29957fd20..e3f943a2822 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -146,7 +146,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { } } - function onFieldChange({ field, path }) { + function onFieldChange({ field, path }: { field: IField; path: FormPath }) { return (published: IFieldState) => { const valueChanged = field.hasChanged('value') const initialValueChanged = field.hasChanged('initialValue') From dc4fa80c2935e9bf84812606306397a1d84b4f14 Mon Sep 17 00:00:00 2001 From: "jinc.cjc" Date: Mon, 4 Nov 2019 15:34:10 +0800 Subject: [PATCH 20/99] fix: [onFieldChange] types From 6421b7f3cd1546155af839403541ec620654117a Mon Sep 17 00:00:00 2001 From: Janry Date: Tue, 5 Nov 2019 13:24:10 +0800 Subject: [PATCH 21/99] test(@uform/react-schema-renderer): fix test case uncompat bugs (#369) --- package.json | 1 + .../core/src/__tests__/field.state.spec.ts | 46 +--- .../core/src/__tests__/form.state.spec.ts | 43 +-- packages/core/src/__tests__/model.spec.ts | 87 +----- .../core/src/__tests__/vfield.state.spec.ts | 43 +-- packages/core/src/index.ts | 156 ++++++----- packages/core/src/shared/model.ts | 64 +++-- packages/core/src/types.ts | 29 +- .../src/__old_tests__/dynamic.spec.js | 2 +- .../src/__tests__/actions.spec.tsx | 32 ++- .../src/__tests__/context.spec.tsx | 21 +- .../src/__tests__/destruct.spec.tsx | 11 +- .../src/__tests__/display.spec.tsx | 39 ++- .../src/__tests__/dynamic.spec.tsx | 256 ++++++++---------- .../src/__tests__/editable.spec.tsx | 22 +- .../src/components/SchemaField.tsx | 4 +- .../src/components/SchemaForm.tsx | 3 +- .../src/shared/registry.ts | 1 + .../src/shared/schema.ts | 5 + .../react/src/components/FormConsumer.tsx | 23 +- packages/react/src/components/FormSpy.tsx | 28 +- packages/react/src/hooks/useField.ts | 2 +- packages/react/src/hooks/useForm.ts | 21 +- packages/react/src/shared.ts | 4 +- packages/shared/src/deprecate.ts | 11 +- 25 files changed, 406 insertions(+), 548 deletions(-) diff --git a/package.json b/package.json index 3d0e137edfb..6869d966dd9 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "typescript": "^3.6.2", "unified": "^7.1.0", "user-event": "^1.4.4", + "wait-for-expect": "^3.0.1", "webpack": "^4.35.3" }, "config": { diff --git a/packages/core/src/__tests__/field.state.spec.ts b/packages/core/src/__tests__/field.state.spec.ts index ed9a530b730..329b075a788 100644 --- a/packages/core/src/__tests__/field.state.spec.ts +++ b/packages/core/src/__tests__/field.state.spec.ts @@ -381,6 +381,7 @@ test('getSourceState', () => { expect(cb).toBeCalledTimes(2) expect(cb).toBeCalledWith(state.state) }) + test('setSourceState', () => { const state = new FieldState({ useDirty: false }) const cb1 = (draft) => draft.change = true @@ -390,47 +391,18 @@ test('setSourceState', () => { state.unsafe_setSourceState(cb1) expect(state.unsafe_getSourceState()).toEqual({ ...prevState1, change: true }) }) -test('hasChanged', () => { + +test('isDirty', () => { const state = new FieldState({ useDirty: true }) expect(state.dirtyNum).toEqual(0) - expect(state.hasChanged()).toEqual(false) + expect(state.isDirty()).toEqual(false) state.dirtyNum = 1 - expect(state.hasChanged()).toEqual(true) + expect(state.isDirty()).toEqual(true) state.dirtyNum = 0 - expect(state.hasChanged()).toEqual(false) - state.dirtys.validating = true - expect(state.hasChanged()).toEqual(false) - expect(state.hasChanged('validating')).toEqual(true) -}) -test('getChanged', () => { - const state = new FieldState({ useDirty: true }) - expect(state.getChanged()).toEqual({}) + expect(state.isDirty()).toEqual(false) state.dirtys.validating = true - expect(state.getChanged()).toEqual({ validating: true }) - state.dirtys = { value: true } - expect(state.getChanged()).toEqual({ value: true }) -}) -test('hasChangedInSequence', () => { - const state = new FieldState({ useDirty: true }) - expect(state.hasChangedInSequence()).toEqual(false) - state.persistDirtys.validating = true - expect(state.hasChangedInSequence()).toEqual(true) - expect(state.hasChangedInSequence('validating')).toEqual(true) - state.persistDirtys.validating = false - expect(state.hasChangedInSequence()).toEqual(false) - expect(state.hasChangedInSequence('validating')).toEqual(false) - state.persistDirtys = {} - expect(state.hasChangedInSequence()).toEqual(false) - state.persistDirtys = { validating: true } - expect(state.hasChangedInSequence()).toEqual(true) - expect(state.hasChangedInSequence('validating')).toEqual(true) -}) -test('getChangedInSequence', () => { - const state = new FieldState({ useDirty: true }) - expect(state.getChangedInSequence()).toEqual({}) - state.persistDirtys.validating = true - expect(state.getChangedInSequence()).toEqual({ validating: true }) - state.persistDirtys = { value: true } - expect(state.getChangedInSequence()).toEqual({ value: true }) + expect(state.isDirty()).toEqual(false) + expect(state.isDirty('validating')).toEqual(true) }) + diff --git a/packages/core/src/__tests__/form.state.spec.ts b/packages/core/src/__tests__/form.state.spec.ts index 791c0244c9f..05bcdb21926 100644 --- a/packages/core/src/__tests__/form.state.spec.ts +++ b/packages/core/src/__tests__/form.state.spec.ts @@ -199,46 +199,15 @@ test('setSourceState', () => { state.unsafe_setSourceState(cb1) expect(state.unsafe_getSourceState()).toEqual({ ...prevState1, change: true }) }) -test('hasChanged', () => { +test('isDirty', () => { const state = new FormState({ useDirty: true }) expect(state.dirtyNum).toEqual(0) - expect(state.hasChanged()).toEqual(false) + expect(state.isDirty()).toEqual(false) state.dirtyNum = 1 - expect(state.hasChanged()).toEqual(true) + expect(state.isDirty()).toEqual(true) state.dirtyNum = 0 - expect(state.hasChanged()).toEqual(false) + expect(state.isDirty()).toEqual(false) state.dirtys.validating = true - expect(state.hasChanged()).toEqual(false) - expect(state.hasChanged('validating')).toEqual(true) -}) -test('getChanged', () => { - const state = new FormState({ useDirty: true }) - expect(state.getChanged()).toEqual({}) - state.dirtys.validating = true - expect(state.getChanged()).toEqual({ validating: true }) - state.dirtys = { values: true } - expect(state.getChanged()).toEqual({ values: true }) -}) -test('hasChangedInSequence', () => { - const state = new FormState({ useDirty: true }) - expect(state.hasChangedInSequence()).toEqual(false) - state.persistDirtys.validating = true - expect(state.hasChangedInSequence()).toEqual(true) - expect(state.hasChangedInSequence('validating')).toEqual(true) - state.persistDirtys.validating = false - expect(state.hasChangedInSequence()).toEqual(false) - expect(state.hasChangedInSequence('validating')).toEqual(false) - state.persistDirtys = {} - expect(state.hasChangedInSequence()).toEqual(false) - state.persistDirtys = { validating: true } - expect(state.hasChangedInSequence()).toEqual(true) - expect(state.hasChangedInSequence('validating')).toEqual(true) -}) -test('getChangedInSequence', () => { - const state = new FormState({ useDirty: true }) - expect(state.getChangedInSequence()).toEqual({}) - state.persistDirtys.validating = true - expect(state.getChangedInSequence()).toEqual({ validating: true }) - state.persistDirtys = { values: true } - expect(state.getChangedInSequence()).toEqual({ values: true }) + expect(state.isDirty()).toEqual(false) + expect(state.isDirty('validating')).toEqual(true) }) diff --git a/packages/core/src/__tests__/model.spec.ts b/packages/core/src/__tests__/model.spec.ts index 788f9e575bb..8a8e90c270e 100644 --- a/packages/core/src/__tests__/model.spec.ts +++ b/packages/core/src/__tests__/model.spec.ts @@ -31,7 +31,6 @@ test('createStateModel', () => { expect(state1.state).toEqual({ displayName, ...defaultState }) expect(state1.props).toEqual({ ...defaultProps, ...params }) expect(state1.dirtys).toEqual({}) - expect(state1.persistDirtys).toEqual({}) expect(state1.dirtyNum).toEqual(0) expect(state1.batching).toEqual(false) expect(state1.displayName).toEqual(displayName) @@ -151,49 +150,19 @@ describe('proxy model', () => { state.unsafe_setSourceState(cb1) expect(state.unsafe_getSourceState()).toEqual({ ...prevState1, change: true }) }) - test('hasChanged', () => { + test('isDirty', () => { const state = new StateModel({ useDirty: false }) expect(state.dirtyNum).toEqual(0) - expect(state.hasChanged()).toEqual(false) + expect(state.isDirty()).toEqual(false) state.dirtyNum = 1 - expect(state.hasChanged()).toEqual(true) + expect(state.isDirty()).toEqual(true) state.dirtyNum = 0 - expect(state.hasChanged()).toEqual(false) - state.dirtys.change = true - expect(state.hasChanged()).toEqual(false) - expect(state.hasChanged('change')).toEqual(true) - }) - test('getChanged', () => { - const state = new StateModel({ useDirty: false }) - expect(state.getChanged()).toEqual({}) + expect(state.isDirty()).toEqual(false) state.dirtys.change = true - expect(state.getChanged()).toEqual({ change: true }) - state.dirtys = { changeObj: true } - expect(state.getChanged()).toEqual({ changeObj: true }) - }) - test('hasChangedInSequence', () => { - const state = new StateModel({ useDirty: false }) - expect(state.hasChangedInSequence()).toEqual(false) - state.persistDirtys.change = true - expect(state.hasChangedInSequence()).toEqual(true) - expect(state.hasChangedInSequence('change')).toEqual(true) - state.persistDirtys.change = false - expect(state.hasChangedInSequence()).toEqual(false) - expect(state.hasChangedInSequence('change')).toEqual(false) - state.persistDirtys = {} - expect(state.hasChangedInSequence()).toEqual(false) - state.persistDirtys = { change: true } - expect(state.hasChangedInSequence()).toEqual(true) - expect(state.hasChangedInSequence('change')).toEqual(true) - }) - test('getChangedInSequence', () => { - const state = new StateModel({ useDirty: false }) - expect(state.getChangedInSequence()).toEqual({}) - state.persistDirtys.change = true - expect(state.getChangedInSequence()).toEqual({ change: true }) - state.persistDirtys = { changeObj: true } - expect(state.getChangedInSequence()).toEqual({ changeObj: true }) + expect(state.isDirty()).toEqual(false) + expect(state.isDirty('change')).toEqual(true) }) + }) describe('dirty model', () => { @@ -303,44 +272,14 @@ describe('dirty model', () => { test('hasChanged', () => { const state = new StateModel({ useDirty: true }) expect(state.dirtyNum).toEqual(0) - expect(state.hasChanged()).toEqual(false) + expect(state.isDirty()).toEqual(false) state.dirtyNum = 1 - expect(state.hasChanged()).toEqual(true) + expect(state.isDirty()).toEqual(true) state.dirtyNum = 0 - expect(state.hasChanged()).toEqual(false) + expect(state.isDirty()).toEqual(false) state.dirtys.change = true - expect(state.hasChanged()).toEqual(false) - expect(state.hasChanged('change')).toEqual(true) - }) - test('getChanged', () => { - const state = new StateModel({ useDirty: true }) - expect(state.getChanged()).toEqual({}) - state.dirtys.change = true - expect(state.getChanged()).toEqual({ change: true }) - state.dirtys = { changeObj: true } - expect(state.getChanged()).toEqual({ changeObj: true }) - }) - test('hasChangedInSequence', () => { - const state = new StateModel({ useDirty: true }) - expect(state.hasChangedInSequence()).toEqual(false) - state.persistDirtys.change = true - expect(state.hasChangedInSequence()).toEqual(true) - expect(state.hasChangedInSequence('change')).toEqual(true) - state.persistDirtys.change = false - expect(state.hasChangedInSequence()).toEqual(false) - expect(state.hasChangedInSequence('change')).toEqual(false) - state.persistDirtys = {} - expect(state.hasChangedInSequence()).toEqual(false) - state.persistDirtys = { change: true } - expect(state.hasChangedInSequence()).toEqual(true) - expect(state.hasChangedInSequence('change')).toEqual(true) - }) - test('getChangedInSequence', () => { - const state = new StateModel({ useDirty: true }) - expect(state.getChangedInSequence()).toEqual({}) - state.persistDirtys.change = true - expect(state.getChangedInSequence()).toEqual({ change: true }) - state.persistDirtys = { changeObj: true } - expect(state.getChangedInSequence()).toEqual({ changeObj: true }) + expect(state.isDirty()).toEqual(false) + expect(state.isDirty('change')).toEqual(true) }) + }) diff --git a/packages/core/src/__tests__/vfield.state.spec.ts b/packages/core/src/__tests__/vfield.state.spec.ts index 61102052af2..970cd451d5a 100644 --- a/packages/core/src/__tests__/vfield.state.spec.ts +++ b/packages/core/src/__tests__/vfield.state.spec.ts @@ -133,46 +133,15 @@ test('setSourceState', () => { state.unsafe_setSourceState(cb1) expect(state.unsafe_getSourceState()).toEqual({ ...prevState1, change: true }) }) -test('hasChanged', () => { +test('isDirty', () => { const state = new VirtualFieldState({ useDirty: true }) expect(state.dirtyNum).toEqual(0) - expect(state.hasChanged()).toEqual(false) + expect(state.isDirty()).toEqual(false) state.dirtyNum = 1 - expect(state.hasChanged()).toEqual(true) + expect(state.isDirty()).toEqual(true) state.dirtyNum = 0 - expect(state.hasChanged()).toEqual(false) + expect(state.isDirty()).toEqual(false) state.dirtys.visible = true - expect(state.hasChanged()).toEqual(false) - expect(state.hasChanged('visible')).toEqual(true) + expect(state.isDirty()).toEqual(false) + expect(state.isDirty('visible')).toEqual(true) }) -test('getChanged', () => { - const state = new VirtualFieldState({ useDirty: true }) - expect(state.getChanged()).toEqual({}) - state.dirtys.visible = true - expect(state.getChanged()).toEqual({ visible: true }) - state.dirtys = { display: true } - expect(state.getChanged()).toEqual({ display: true }) -}) -test('hasChangedInSequence', () => { - const state = new VirtualFieldState({ useDirty: true }) - expect(state.hasChangedInSequence()).toEqual(false) - state.persistDirtys.visible = true - expect(state.hasChangedInSequence()).toEqual(true) - expect(state.hasChangedInSequence('visible')).toEqual(true) - state.persistDirtys.visible = false - expect(state.hasChangedInSequence()).toEqual(false) - expect(state.hasChangedInSequence('visible')).toEqual(false) - state.persistDirtys = {} - expect(state.hasChangedInSequence()).toEqual(false) - state.persistDirtys = { visible: true } - expect(state.hasChangedInSequence()).toEqual(true) - expect(state.hasChangedInSequence('visible')).toEqual(true) -}) -test('getChangedInSequence', () => { - const state = new VirtualFieldState({ useDirty: true }) - expect(state.getChangedInSequence()).toEqual({}) - state.persistDirtys.visible = true - expect(state.getChangedInSequence()).toEqual({ visible: true }) - state.persistDirtys = { display: true } - expect(state.getChangedInSequence()).toEqual({ display: true }) -}) \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6c60c722038..5ae8607ba71 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -43,7 +43,9 @@ import { export * from './shared/lifecycle' export * from './types' -export const createForm = (options: IFormCreatorOptions = {}): IForm => { +export function createForm( + options: IFormCreatorOptions = {} +): IForm { function onGraphChange({ type, payload }) { heart.publish(LifeCycleTypes.ON_FORM_GRAPH_CHANGE, graph) if (type === 'GRAPH_NODE_WILL_UNMOUNT') { @@ -53,12 +55,12 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { function onFormChange(published: IFormState) { heart.publish(LifeCycleTypes.ON_FORM_CHANGE, state) - const valuesChanged = state.hasChanged('values') - const initialValuesChanged = state.hasChanged('initialValues') - const unmountedChanged = state.hasChanged('unmounted') - const mountedChanged = state.hasChanged('mounted') - const initializedChanged = state.hasChanged('initialized') - const editableChanged = state.hasChanged('editable') + const valuesChanged = state.isDirty('values') + const initialValuesChanged = state.isDirty('initialValues') + const unmountedChanged = state.isDirty('unmounted') + const mountedChanged = state.isDirty('mounted') + const initializedChanged = state.isDirty('initialized') + const editableChanged = state.isDirty('editable') if (valuesChanged || initialValuesChanged) { /** * 影子更新:不会触发具体字段的onChange,如果不这样处理,会导致任何值变化都会导致整树rerender @@ -147,22 +149,22 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { } function onFieldChange({ field, path }) { - return (published: IFieldState) => { - const valueChanged = field.hasChanged('value') - const initialValueChanged = field.hasChanged('initialValue') - const visibleChanged = field.hasChanged('visible') - const displayChanged = field.hasChanged('display') - const unmountedChanged = field.hasChanged('unmounted') - const mountedChanged = field.hasChanged('mounted') - const initializedChanged = field.hasChanged('initialized') - const warningsChanged = field.hasChanged('warnings') - const errorsChanges = field.hasChanged('errors') + return (published: IFieldState) => { + const valueChanged = field.isDirty('value') + const initialValueChanged = field.isDirty('initialValue') + const visibleChanged = field.isDirty('visible') + const displayChanged = field.isDirty('display') + const unmountedChanged = field.isDirty('unmounted') + const mountedChanged = field.isDirty('mounted') + const initializedChanged = field.isDirty('initialized') + const warningsChanged = field.isDirty('warnings') + const errorsChanges = field.isDirty('errors') if (initializedChanged) { heart.publish(LifeCycleTypes.ON_FIELD_INIT, field) const isEmptyValue = !isValid(published.value) const isEmptyInitialValue = !isValid(published.initialValue) if (isEmptyValue || isEmptyInitialValue) { - field.setState((state: IFieldState) => { + field.setState((state: IFieldState) => { if (isEmptyValue) state.value = getFormValuesIn(state.name) if (isEmptyInitialValue) state.initialValue = getFormInitialValuesIn(state.name) @@ -180,7 +182,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { graph.eachChildren( path, childState => { - childState.setState((state: IFieldState) => { + childState.setState((state: IFieldState) => { if (visibleChanged) { state.visible = published.visible } @@ -229,12 +231,12 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { } function onVirtualFieldChange({ field, path }) { - return (published: IVirtualFieldState) => { - const visibleChanged = field.hasChanged('visible') - const displayChanged = field.hasChanged('display') - const unmountedChanged = field.hasChanged('unmounted') - const mountedChanged = field.hasChanged('mounted') - const initializedChnaged = field.hasChanged('initialized') + return (published: IVirtualFieldState) => { + const visibleChanged = field.isDirty('visible') + const displayChanged = field.isDirty('display') + const unmountedChanged = field.isDirty('unmounted') + const mountedChanged = field.isDirty('mounted') + const initializedChnaged = field.isDirty('initialized') if (initializedChnaged) { heart.publish(LifeCycleTypes.ON_FIELD_INIT, field) @@ -242,25 +244,31 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { if (visibleChanged) { graph.eachChildren(path, childState => { - childState.setState((state: IVirtualFieldState) => { - state.visible = published.visible - }) + childState.setState( + (state: IVirtualFieldState) => { + state.visible = published.visible + } + ) }) } if (displayChanged) { graph.eachChildren(path, childState => { - childState.setState((state: IVirtualFieldState) => { - state.display = published.display - }) + childState.setState( + (state: IVirtualFieldState) => { + state.display = published.display + } + ) }) } if (unmountedChanged) { graph.eachChildren(path, childState => { - childState.setState((state: IVirtualFieldState) => { - state.unmounted = published.unmounted - }) + childState.setState( + (state: IVirtualFieldState) => { + state.unmounted = published.unmounted + } + ) }) } if (mountedChanged && published.mounted) { @@ -289,7 +297,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { notify: onVirtualFieldChange({ field, path: nodePath }) } field.batch(() => { - field.setState((state: IVirtualFieldState) => { + field.setState((state: IVirtualFieldState) => { state.initialized = true state.props = props }) @@ -335,7 +343,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { } heart.publish(LifeCycleTypes.ON_FIELD_WILL_INIT, field) field.batch(() => { - field.setState((state: IFieldState) => { + field.setState((state: IFieldState) => { const formValue = getFormValuesIn(dataPath) const formInitialValue = getFormInitialValuesIn(dataPath) state.initialized = true @@ -371,7 +379,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { }, 60) validate(value, rules).then(({ errors, warnings }) => { clearTimeout((field as any).validateTimer) - field.setState((state: IFieldState) => { + field.setState((state: IFieldState) => { state.validating = false state.ruleErrors = errors state.ruleWarnings = warnings @@ -493,7 +501,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { } function setValue(...values: any[]) { - field.setState((state: IFieldState) => { + field.setState((state: IFieldState) => { state.value = values[0] state.values = values }) @@ -504,7 +512,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { function removeValue(key: string | number) { const name = field.unsafe_getSourceState(state => state.name) env.removeNodes[name] = true - field.setState((state: IFieldState) => { + field.setState((state: IFieldState) => { state.value = undefined state.values = [] }, true) @@ -524,17 +532,17 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { return values[0] }, focus() { - field.setState((state: IFieldState) => { + field.setState((state: IFieldState) => { state.active = true state.visited = true }) }, blur() { - field.setState((state: IFieldState) => { + field.setState((state: IFieldState) => { state.active = false }) }, - push(value: any) { + push(value?: any) { const arr = toArr(getValue()).slice() arr.push(value) setValue(arr) @@ -637,7 +645,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { let result: Promise leadingUpdate(() => { graph.eachChildren(field => { - field.setState((state: IFieldState) => { + field.setState((state: IFieldState) => { state.modified = false state.ruleErrors = [] state.ruleWarnings = [] @@ -686,11 +694,11 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { ): Promise { // 重复提交,返回前一次的promise if (state.getState(state => state.submitting)) return env.submittingTask + heart.publish(LifeCycleTypes.ON_FORM_SUBMIT_START, state) onSubmit = onSubmit || options.onSubmit state.setState(state => { state.submitting = true }) - heart.publish(LifeCycleTypes.ON_FORM_SUBMIT_START, state) env.submittingTask = validate() .then(validated => { const { errors } = validated @@ -748,7 +756,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { state.setState(state => { state.validating = false }) - if (isFn(options.onValidateFailed)) { + if (isFn(options.onValidateFailed) && payload.errors.length) { options.onValidateFailed(payload) } heart.publish(LifeCycleTypes.ON_FORM_VALIDATE_END, state) @@ -773,7 +781,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { callbacks.forEach(callback => { field.setState(callback) }) - if (!path.isWildMatchPattern && !path.isMatchPattern) { + if (!pattern.isWildMatchPattern && !pattern.isMatchPattern) { env.taskQueue.splice(index, 1) env.taskQueue.forEach(({ pattern }, index) => { if (pattern.toString() === path.toString()) { @@ -787,7 +795,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { function setFieldState( path: FormPathPattern, - callback?: (state: IFieldState) => void + callback?: (state: IFieldState) => void ) { if (!isFn(callback)) return let matchCount = 0 @@ -841,7 +849,7 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { function getFieldState( path: FormPathPattern, - callback?: (state: IFieldState) => any + callback?: (state: IFieldState) => any ) { const field = graph.select(path) return field && field.getState(callback) @@ -854,34 +862,40 @@ export const createForm = (options: IFormCreatorOptions = {}): IForm => { } function setFormGraph(nodes: {}) { - each(nodes, (node: IFieldState | IVirtualFieldState, key) => { - let nodeState: any - if (graph.exist(key)) { - nodeState = graph.get(key) - nodeState.unsafe_setSourceState(state => { - Object.assign(state, node) - }) - } else { - if (node.displayName === 'VirtualFieldState') { - nodeState = registerVirtualField({ - path: key - }) - nodeState.unsafe_setSourceState(state => { - Object.assign(state, node) - }) - } else if (node.displayName === 'FieldState') { - nodeState = registerField({ - path: key - }) + each( + nodes, + ( + node: IFieldState | IVirtualFieldState, + key + ) => { + let nodeState: any + if (graph.exist(key)) { + nodeState = graph.get(key) nodeState.unsafe_setSourceState(state => { Object.assign(state, node) }) + } else { + if (node.displayName === 'VirtualFieldState') { + nodeState = registerVirtualField({ + path: key + }) + nodeState.unsafe_setSourceState(state => { + Object.assign(state, node) + }) + } else if (node.displayName === 'FieldState') { + nodeState = registerField({ + path: key + }) + nodeState.unsafe_setSourceState(state => { + Object.assign(state, node) + }) + } + } + if (nodeState) { + nodeState.notify(state.getState()) } } - if (nodeState) { - nodeState.notify(state.getState()) - } - }) + ) } function shadowUpdate(callback: () => void) { diff --git a/packages/core/src/shared/model.ts b/packages/core/src/shared/model.ts index 7c3d60a6cbe..c2a7b6ff6f4 100644 --- a/packages/core/src/shared/model.ts +++ b/packages/core/src/shared/model.ts @@ -23,7 +23,7 @@ export const createStateModel = ( public displayName?: string public dirtyNum: number public dirtys: StateDirtyMap - public persistDirtys: StateDirtyMap + public prevState: State public batching: boolean public stackCount: number public controller: StateModel @@ -31,12 +31,12 @@ export const createStateModel = ( constructor(defaultProps: DefaultProps) { super() this.state = { ...Factory.defaultState } + this.prevState = { ...Factory.defaultState } this.props = { ...Factory.defaultProps, ...defaultProps } this.dirtys = {} - this.persistDirtys = {} this.dirtyNum = 0 this.stackCount = 0 this.batching = false @@ -66,7 +66,7 @@ export const createStateModel = ( return this.controller.publishState(this.state) } - if (!hasProxy || this.props.useDirty) { + if (!hasProxy || this.props.useDirty) { return clone(this.state) } else { return produce(this.state, () => {}) @@ -87,7 +87,7 @@ export const createStateModel = ( if (!hasProxy || this.props.useDirty) { callback(this.state) } else { - this.state = produce(this.state, (draft) => { + this.state = produce(this.state, draft => { callback(draft) }) } @@ -99,16 +99,13 @@ export const createStateModel = ( silent = false ) => { if (isFn(callback)) { + this.stackCount++ if (!hasProxy || this.props.useDirty) { const draft = this.getState() if (!this.batching) { this.dirtys = {} this.dirtyNum = 0 } - if (!this.stackCount) { - this.persistDirtys = {} - } - this.stackCount++ callback(draft) if (isFn(this.controller.computeState)) { this.controller.computeState(draft, this.state) @@ -116,14 +113,16 @@ export const createStateModel = ( const draftKeys = Object.keys(draft || {}) const stateKeys = Object.keys(this.state || {}) - each(draftKeys.length > stateKeys.length ? draft : this.state, (value, key) => { - if (!isEqual(this.state[key], draft[key])) { - this.state[key] = draft[key] - this.dirtys[key] = true - this.persistDirtys[key] = true - this.dirtyNum++ + each( + draftKeys.length > stateKeys.length ? draft : this.state, + (value, key) => { + if (!isEqual(this.state[key], draft[key])) { + this.state[key] = draft[key] + this.dirtys[key] = true + this.dirtyNum++ + } } - }) + ) if (isFn(this.controller.dirtyCheck)) { const result = this.controller.dirtyCheck(this.dirtys) if (result !== undefined) { @@ -131,12 +130,15 @@ export const createStateModel = ( } } if (this.dirtyNum > 0 && !silent) { - if (this.batching) return + if (this.batching) { + this.stackCount-- + return + } this.notify(this.getState()) this.dirtys = {} this.dirtyNum = 0 } - } else { + } else { if (!this.batching) { this.dirtys = {} this.dirtyNum = 0 @@ -155,12 +157,10 @@ export const createStateModel = ( if (op === 'replace') { if (!isEqual(this.state[path[0]], value)) { this.dirtys[path[0]] = true - this.persistDirtys[path[0]] = true this.dirtyNum++ } } else { this.dirtys[path[0]] = true - this.persistDirtys[path[0]] = true this.dirtyNum++ } }) @@ -173,7 +173,10 @@ export const createStateModel = ( } } if (this.dirtyNum > 0 && !silent) { - if (this.batching) return + if (this.batching) { + this.stackCount-- + return + } this.notify(this.getState()) this.dirtys = {} this.dirtyNum = 0 @@ -181,28 +184,29 @@ export const createStateModel = ( //2. 自己监听自己,自己修改自己的状态,希望触发onFieldChange } } + this.stackCount-- if (!this.stackCount) { - this.persistDirtys = {} + this.prevState = clone(this.state) } } } /** * 当前操作的变化情况 */ - hasChanged = (key?: string) => + isDirty = (key?: string) => key ? this.dirtys[key] === true : this.dirtyNum > 0 + + getDirtyInfo = () => this.dirtys + /** * *在一组操作过程中的变化情况 */ - hasChangedInSequence = (key?: string) => - key - ? this.persistDirtys[key] - : Object.keys(this.persistDirtys || {}).filter(k => this.persistDirtys[k] === true).length > 0 - - getChanged = () => this.dirtys - - getChangedInSequence = () => this.persistDirtys + hasChanged = (key?: string) => { + return key + ? !isEqual(this.prevState[key], this.state[key]) + : !isEqual(this.prevState, this.state) + } } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index b001e805e96..deb95f9cf5e 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -105,7 +105,7 @@ export interface IStateModelFactory { displayName?: string } -export interface IFieldState { +export interface IFieldState { displayName?: string name: string path: string @@ -137,11 +137,11 @@ export interface IFieldState { required: boolean mounted: boolean unmounted: boolean - props: {} + props: FieldProps } export type FieldStateDirtyMap = StateDirtyMap -export interface IFieldStateProps { +export interface IFieldStateProps { path?: FormPathPattern nodePath?: FormPathPattern dataPath?: FormPathPattern @@ -149,7 +149,7 @@ export interface IFieldStateProps { value?: any values?: any[] initialValue?: any - props?: {} + props?: FieldProps rules?: ValidatePatternRules[] required?: boolean editable?: boolean @@ -173,7 +173,7 @@ export const isVirtualFieldState = ( export const isStateModel = (target: any): target is IModel => target && isFn(target.getState) -export interface IFormState { +export interface IFormState { pristine: boolean valid: boolean invalid: boolean @@ -188,7 +188,7 @@ export interface IFormState { initialValues: {} mounted: boolean unmounted: boolean - props: {} + props: FormProps } export type FormStateDirtyMap = StateDirtyMap @@ -209,7 +209,7 @@ export interface IFormCreatorOptions extends IFormStateProps { onValidateFailed?: (validated: IFormValidateResult) => void } -export interface IVirtualFieldState { +export interface IVirtualFieldState { name: string path: string displayName?: string @@ -218,16 +218,16 @@ export interface IVirtualFieldState { display: boolean mounted: boolean unmounted: boolean - props: {} + props: FieldProps } export type VirtualFieldStateDirtyMap = StateDirtyMap -export interface IVirtualFieldStateProps { +export interface IVirtualFieldStateProps { path?: FormPathPattern dataPath?: FormPathPattern nodePath?: FormPathPattern name?: string - props?: {} + props?: FieldProps onChange?: (fieldState: IVirtualField) => void } @@ -251,7 +251,7 @@ export interface IMutators { change(...values: any[]): any focus(): void blur(): void - push(value: any): any[] + push(value?: any): any[] pop(): any[] insert(index: number, value: any): any[] remove(index: number | string): any @@ -270,7 +270,7 @@ export interface IModel extends Subscribable { displayName?: string dirtyNum: number dirtys: StateDirtyMap - persistDirtys: StateDirtyMap + prevState: S batching: boolean stackCount: number controller: StateModel @@ -280,9 +280,8 @@ export interface IModel extends Subscribable { unsafe_getSourceState: (callback?: (state: S) => any) => any unsafe_setSourceState: (callback?: (state: S) => void) => void hasChanged: (key?: string) => boolean - hasChangedInSequence: (key?: string) => boolean - getChanged: () => StateDirtyMap - getChangedInSequence: (key?: string) => StateDirtyMap + isDirty: (key?: string) => boolean + getDirtyInfo: () => StateDirtyMap } export type IField = IModel diff --git a/packages/react-schema-renderer/src/__old_tests__/dynamic.spec.js b/packages/react-schema-renderer/src/__old_tests__/dynamic.spec.js index 00b98d15129..b15f9c09975 100644 --- a/packages/react-schema-renderer/src/__old_tests__/dynamic.spec.js +++ b/packages/react-schema-renderer/src/__old_tests__/dynamic.spec.js @@ -560,7 +560,7 @@ test('dynamic change functions onChange/onReset/onSubmit/onValidateFailed', asyn await sleep(33) // onChange expect(queryAllByText('valueB-456').length).toBe(1) - actions.reset() + actions.reset({ validate: false }) await sleep(33) // onReset expect(queryAllByText('valueC-456').length).toBe(1) diff --git a/packages/react-schema-renderer/src/__tests__/actions.spec.tsx b/packages/react-schema-renderer/src/__tests__/actions.spec.tsx index 639eebc3fb5..532558e3a27 100644 --- a/packages/react-schema-renderer/src/__tests__/actions.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/actions.spec.tsx @@ -10,7 +10,10 @@ import { import { render, wait } from '@testing-library/react' beforeEach(() => { - registerFormField('string', connect()(props => )) + registerFormField( + 'string', + connect()(props => ) + ) }) test('createFormActions', async () => { @@ -46,15 +49,15 @@ test('createFormActions', async () => { ) const { queryByTestId } = render() - expect(queryByTestId('inputA').getAttribute('value')).toEqual('change value of aaa field onFormInit') - await actions.setFormState(state => (state.values = { aaa: 123 })) - await wait(() => { - expect(queryByTestId('inputA').getAttribute('value')).toEqual('123') - }) - await actions.setFieldState('aaa', state => (state.value = 'hello world')) - await wait(() => { - expect(queryByTestId('inputA').getAttribute('value')).toEqual('hello world') - }) + expect(queryByTestId('inputA').getAttribute('value')).toEqual( + 'change value of aaa field onFormInit' + ) + actions.setFormState(state => (state.values = { aaa: 123 })) + await wait() + expect(queryByTestId('inputA').getAttribute('value')).toEqual('123') + actions.setFieldState('aaa', state => (state.value = 'hello world')) + await wait() + expect(queryByTestId('inputA').getAttribute('value')).toEqual('hello world') const VALUE_A = 'value of aaa field' const VALUE_B = 'value of bbb field' const schemaData = [ @@ -70,8 +73,7 @@ test('createFormActions', async () => { ) }) await Promise.all(updateQueue) - await wait(() => { - expect(queryByTestId('inputA').getAttribute('value')).toEqual(VALUE_A) - expect(queryByTestId('inputB').getAttribute('value')).toEqual(VALUE_B) - }) -}) \ No newline at end of file + await wait() + expect(queryByTestId('inputA').getAttribute('value')).toEqual(VALUE_A) + expect(queryByTestId('inputB').getAttribute('value')).toEqual(VALUE_B) +}) diff --git a/packages/react-schema-renderer/src/__tests__/context.spec.tsx b/packages/react-schema-renderer/src/__tests__/context.spec.tsx index 49598f4d67b..003fcc4d0be 100644 --- a/packages/react-schema-renderer/src/__tests__/context.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/context.spec.tsx @@ -6,13 +6,14 @@ import { SchemaMarkupField as Field, createFormActions, FormProvider, - FormConsumer, + FormConsumer } from '../index' import { render, fireEvent, act, wait } from '@testing-library/react' -const sleep = async (timeout) => { - const noop = () => {} - await wait(noop, timeout) +const sleep = timeout => { + return new Promise(resolve => { + setTimeout(resolve, timeout) + }) } beforeEach(() => { @@ -29,7 +30,7 @@ test('submit by form consumer', async () => { { - await sleep(200) + await sleep(100) }} > @@ -50,9 +51,9 @@ test('submit by form consumer', async () => { act(() => { fireEvent.click(queryByText('Submit')) }) - // await sleep(30) - // expect(queryByText('Submitting')).toBeVisible() - // await sleep(300) - // expect(queryByText('Submitting')).toBeNull() - // expect(queryByText('Submit')).not.toBeUndefined() + await wait() + expect(queryByText('Submitting')).toBeVisible() + await sleep(1000) + expect(queryByText('Submitting')).toBeNull() + expect(queryByText('Submit')).not.toBeUndefined() }) diff --git a/packages/react-schema-renderer/src/__tests__/destruct.spec.tsx b/packages/react-schema-renderer/src/__tests__/destruct.spec.tsx index a8370e0e006..dfed90fc481 100644 --- a/packages/react-schema-renderer/src/__tests__/destruct.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/destruct.spec.tsx @@ -40,9 +40,8 @@ test('destruct with initial values', async () => { } const { queryByText } = render() - wait(() => { - expect(queryByText('123321')).toBeNull() - }) + await wait() + expect(queryByText('123321')).toBeNull() }) test('destruct with initial values in array', async () => { @@ -59,8 +58,6 @@ test('destruct with initial values in array', async () => { } const { queryByText } = render() - - await wait(() => { - expect(queryByText('123321')).toBeNull() - }) + await wait() + expect(queryByText('123321')).toBeNull() }) diff --git a/packages/react-schema-renderer/src/__tests__/display.spec.tsx b/packages/react-schema-renderer/src/__tests__/display.spec.tsx index 75928251c7d..46f94dfb973 100644 --- a/packages/react-schema-renderer/src/__tests__/display.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/display.spec.tsx @@ -54,9 +54,8 @@ test('display is false will remove react children node', async () => { } const { queryByText } = render() - wait(() => { - expect(queryByText('123321')).toBeNull() - }) + await wait() + expect(queryByText('123321')).toBeNull() }) test('display is false will not remove value(include default value)', async () => { @@ -89,15 +88,13 @@ test('display is false will not remove value(include default value)', async () = const { queryByText } = render() - await wait(() => { - expect(queryByText('123321')).toBeNull() - }) + await wait() + expect(queryByText('123321')).toBeNull() fireEvent.click(queryByText('Submit')) - await wait(() => { - expect(onSubmitHandler).toHaveBeenCalledWith({ - obj: { aa: '123321' }, - bb: '123' - }) + await wait() + expect(onSubmitHandler).toHaveBeenCalledWith({ + obj: { aa: '123321' }, + bb: '123' }) }) @@ -109,7 +106,7 @@ test('display is false will not validate(include children)', async () => { { $('onFieldChange', 'bb').subscribe(({ value }) => { if (value === '123') { @@ -132,18 +129,14 @@ test('display is false will not validate(include children)', async () => { } const { queryByText } = render() - - await wait(() => { - expect(queryByText('123321')).toBeNull() - }) + await wait() + expect(queryByText('123321')).toBeNull() fireEvent.click(queryByText('Submit')) - await wait(() => { - expect(onSubmitHandler).toHaveBeenCalledWith({ - obj: { aa: '123321' }, - bb: '123' - }) - expect(onValidateFailedHandler).toHaveBeenCalledTimes(0) + await wait() + expect(onSubmitHandler).toHaveBeenCalledWith({ + obj: { aa: '123321' }, + bb: '123' }) + expect(onValidateFailedHandler).toHaveBeenCalledTimes(0) }) -// display 有问题 \ No newline at end of file diff --git a/packages/react-schema-renderer/src/__tests__/dynamic.spec.tsx b/packages/react-schema-renderer/src/__tests__/dynamic.spec.tsx index e64524dda47..5bebf9da96c 100644 --- a/packages/react-schema-renderer/src/__tests__/dynamic.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/dynamic.spec.tsx @@ -11,9 +11,14 @@ import { import { toArr } from '@uform/shared' import { render, fireEvent, wait, act } from '@testing-library/react' +const sleep = timeout => { + return new Promise(resolve => { + setTimeout(resolve, timeout) + }) +} + let FormCard beforeEach(() => { - jest.setTimeout(10000) registerFormField( 'string', connect()(props => ( @@ -61,10 +66,7 @@ beforeEach(() => { +
    +
    + ) + + const { queryByText } = render() + await wait(); + expect(queryByText('text')).toBeNull() + actions.setFieldState('aaa', state => { + state.editable = true + }) + await wait(); + expect(queryByText('text')).toBeVisible() +}) + +test('update editable in controlled', async () => { + let updateEditable + const TestComponent = () => { + const [editable, _updateEditable] = useState(true) + updateEditable = _updateEditable + return ( + + + + ) + } + + const { queryByText } = render() + await wait(); + expect(queryByText('text')).toBeVisible() + act(() => updateEditable(false)) + await wait(); + expect(queryByText('text')).toBeNull() + act(() => updateEditable(true)) + await wait(); + expect(queryByText('text')).toBeVisible() +}) + +test('editable with x-props', async () => { + const actions = createFormActions() + const TestComponent = () => { + return ( + + + + ) + } + + const { queryByText } = render() + await wait(); + expect(queryByText('text')).toBeNull() + actions.setFieldState('aaa', state => { + state.editable = true + }) + await wait(); + expect(queryByText('text')).toBeVisible() +}) + +test('editable with x-props in array field', async () => { + const actions = createFormActions() + const TestComponent = () => { + return ( + + + + + + + + ) + } + + const { queryByText } = render() + await wait(); + expect(queryByText('empty')).toBeVisible() + actions.setFieldState('array.0.aa', state => { + state.editable = true + }) + await wait(); + expect(queryByText('empty')).toBeNull() +}) + +test('editable with x-props is affected by global editable', async () => { + const actions = createFormActions() + const TestComponent = () => { + return ( + + + + + + + + ) + } + + const { queryByText } = render() + await wait(); + expect(queryByText('empty')).toBeNull() + actions.setFieldState('array.0.aa', state => { + state.editable = false + }) + await wait(); + expect(queryByText('empty')).toBeVisible() +}) + +test('editable conflicts that global editable props with setFieldState', async () => { + const TestComponent = () => { + return ( + { + $('onFieldChange', 'ccc').subscribe(() => { + setFieldState('bbb', state => { + state.editable = true + }) + }) + }} + > + + + + + + + ) + } + + const { queryByTestId } = render() + await wait(); + expect(queryByTestId('this is aaa')).toBeVisible() + expect(queryByTestId('this is bbb')).toBeVisible() + expect(queryByTestId('this is ccc')).toBeVisible() + fireEvent.change(queryByTestId('this is ccc'), { target: { value: '123' } }) + await wait(); + expect(queryByTestId('this is bbb')).toBeVisible() + + fireEvent.change(queryByTestId('this is ccc'), { target: { value: '321' } }) + await wait(); + expect(queryByTestId('this is bbb')).toBeVisible() +}) + +test('editable conflicts that props editable props with setFieldState', async () => { + const TestComponent = () => { + return ( + { + $('onFieldChange', 'ccc').subscribe(() => { + setFieldState('bbb', state => { + state.editable = true + }) + }) + }} + > + + + + + + + ) + } + + const { queryByTestId } = render() + await wait(); + expect(queryByTestId('this is aaa')).toBeVisible() + expect(queryByTestId('this is bbb')).toBeVisible() + expect(queryByTestId('this is ccc')).toBeVisible() + fireEvent.change(queryByTestId('this is ccc'), { target: { value: '123' } }) + await wait(); + expect(queryByTestId('this is bbb')).toBeVisible() + fireEvent.change(queryByTestId('this is ccc'), { target: { value: '321' } }) + await wait(); + expect(queryByTestId('this is bbb')).toBeVisible() +}) + + +test('editable conflicts that x-props editable props with setFieldState', async () => { + const TestComponent = () => { + return ( + { + $('onFieldChange', 'ccc').subscribe(() => { + setFieldState('bbb', state => { + state.editable = true + }) + }) + }} + > + + + + + + + ) + } + + const { queryByTestId } = render() + await wait(); + expect(queryByTestId('this is aaa')).toBeVisible() + expect(queryByTestId('this is bbb')).toBeVisible() + expect(queryByTestId('this is ccc')).toBeVisible() + fireEvent.change(queryByTestId('this is ccc'), { target: { value: '123' } }) + await wait(); + expect(queryByTestId('this is bbb')).toBeVisible() + fireEvent.change(queryByTestId('this is ccc'), { target: { value: '321' } }) + await wait(); + expect(queryByTestId('this is bbb')).toBeVisible() +}) diff --git a/packages/react-schema-renderer/src/__tests__/effects.spec.tsx b/packages/react-schema-renderer/src/__tests__/effects.spec.tsx new file mode 100644 index 00000000000..165ea46b602 --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/effects.spec.tsx @@ -0,0 +1,295 @@ +import React, { Fragment, useState, useEffect } from 'react' +import { + registerFormField, + connect, + FormPath, + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, + createFormActions, + // createVirtualBox, + registerFieldMiddleware +} from '../index' +// import { toArr } from '@uform/shared' +import { render, wait, act, fireEvent } from '@testing-library/react' +import { filter } from 'rxjs/operators' + +registerFieldMiddleware(Field => { + return props => { + return ( +
    + {props.schema.title} + + {props.errors && props.errors.length ? ( +
    {props.errors}
    + ) : ( + '' + )} +
    + ) + } +}) +registerFormField( + 'string', + connect()(props => props.disabled + ? Disabled + : ) +) + +test('onFormInit setFieldState', async () => { + const actions = createFormActions() + const TestComponent = () => ( + { + $('onFormInit').subscribe(() => { + setFieldState('aaa', state => { + state.props.title = 'text' + state.rules = [ + { + required: true, + message: 'field is required' + } + ] + }) + }) + }} + > + + + + + + ) + + const { getByText, getByTestId, queryByText } = render() + + await wait(); + expect(queryByText('text')).toBeVisible() + await wait(); + fireEvent.click(getByTestId('btn')) + await wait(); + expect(getByText('field is required')).toBeVisible() + await wait(); + actions.setFieldState('aaa', state => { + state.rules = [] + }) + await wait(); + fireEvent.click(getByTestId('btn')) + await wait(); + expect(queryByText('field is required')).toBeNull() +}) + +test('init triggers', async () => { + const callback = jest.fn() + const TestComponent = () => { + return ( + { + $('onFieldChange', 'aaa').subscribe(callback) + }} + > + + + + + + ) + } + + render() + await wait(); + expect(callback).toHaveBeenCalledTimes(1) +}) + +test('onFieldChange will trigger with initialValues', async () => { + const callback = jest.fn() + const TestComponent = () => { + const [values, setValues] = useState({}) + useEffect(() => { + setTimeout(() => { + act(() => { + setValues({ + aaa: 123 + }) + }) + }) + }, []) + return ( + { + $('onFieldChange', 'aaa').subscribe(callback) + }} + > + + + + + + ) + } + + render() + await wait(); + expect(callback).toHaveBeenCalledTimes(2) + expect(callback.mock.calls[0][0].value).toBe(undefined) + expect(callback.mock.calls[1][0].value).toBe(123) +}) + +test('setFieldState x-props with onFormInit', async () => { + const TestComponent = () => { + return ( + { + $('onFormInit').subscribe(() => { + setFieldState('aaa', state => { + state.props['x-props'].disabled = true + }) + }) + }} + > + + + + + + ) + } + + const { queryByText } = render() + await wait(); + expect(queryByText('Disabled')).toBeVisible() +}) + +test('getFieldState with onFieldChange', async () => { + let aaValue + const TestComponent = () => { + return ( + console.log(values)} + initialValues={{ obj: { aa: 123 } }} + effects={($, { getFieldState }) => { + $('onFieldChange', 'obj.aa').subscribe(() => { + aaValue = getFieldState('obj', state => state.value.aa) + }) + }} + > + + + + + ) + } + const { queryByTestId } = render() + await wait(); + fireEvent.change(queryByTestId('this is aa'), { target: { value: '333' } }) + await wait(); + expect(aaValue).toBe('333') +}) + +test('set errors in effects', async () => { + const callback = jest.fn() + const TestComponent = () => { + return ( + { + $('onFormInit').subscribe(() => { + setFieldState('aaa', state => { + state.errors = ['validate failed'] + }) + }) + }} + onSubmit={callback} + > + + + + + + ) + } + + const { queryByTestId } = render() + await wait(); + fireEvent.click(queryByTestId('btn')) + await wait(); + expect(callback).toHaveBeenCalledTimes(0) +}) + +test('setFieldState from buffer', async () => { + const callback = jest.fn() + const TestComponent = () => { + return ( + { + $('onFormInit').subscribe(() => { + setFieldState(FormPath.match('*') as any, state => { + (state as any).title = '1123' + }) + }) + $('onFieldChange', 'kkk').subscribe(() => { + setFieldState(FormPath.match('dd.*') as any, state => { + state.visible = false + }) + }) + }} + onSubmit={callback} + > + + + + + + + + ) + } + + const { queryByTestId } = render() + await wait(); + expect(queryByTestId('test')).toBeVisible() +}) + +test('filter first onFieldChange', async () => { + const sub1 = jest.fn() + const sub2 = jest.fn() + const TestComponent = () => { + return ( + { + $('onFieldChange', 'aaa') + .pipe(filter(state => !state.pristine)) + .subscribe(sub1) + $('onFieldChange', 'aaa').subscribe(sub2) + }} + > + + + ) + } + + render() + await wait(); + expect(sub1).toHaveBeenCalledTimes(0) + expect(sub2).toHaveBeenCalledTimes(1) +}) \ No newline at end of file diff --git a/packages/react-schema-renderer/src/__tests__/mutators.spec.tsx b/packages/react-schema-renderer/src/__tests__/mutators.spec.tsx new file mode 100644 index 00000000000..e179d3ada97 --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/mutators.spec.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { + registerFormField, + connect, + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, + createFormActions, +} from '../index' +import { render, fireEvent, wait } from '@testing-library/react' + +registerFormField( + 'mutator', + connect()(props => ( +
    + + {props.value && props.value[0] && props.value[0].aaa} +
    + )) +) + +test('update value by ref', async () => { + const actions = createFormActions() + const TestComponent = () => ( + + + + ) + + const { queryByText } = render() + await wait() + expect(queryByText('123')).toBeVisible() + fireEvent.click(queryByText('Change Value')) + await wait() + expect(queryByText('321')).toBeVisible() +}) diff --git a/packages/react-schema-renderer/src/__tests__/schema_form.spec.tsx b/packages/react-schema-renderer/src/__tests__/schema_form.spec.tsx new file mode 100644 index 00000000000..9a6538db7d5 --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/schema_form.spec.tsx @@ -0,0 +1,61 @@ +import React, { Fragment } from 'react' +import { + registerFormField, + connect, + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, +} from '../index' +import { render, fireEvent, wait } from '@testing-library/react' + +registerFormField( + 'string', + connect()(props => ( + + )) +) + +test('Increase lastValidateValue value processing during initialization', async () => { + const inpueFieldValidate = jest.fn() + const requriedFieldValidate = jest.fn() + + const TestComponent = () => ( + + + { + inpueFieldValidate() + return '' + } + } + /> + { + requriedFieldValidate() + return '' + }} + /> + + + ) + + const { getByTestId } = render() + fireEvent.change(getByTestId('inpueField'), { target: { value: 1111 } }) + await wait(); + expect(inpueFieldValidate).toHaveBeenCalledTimes(1) + expect(requriedFieldValidate).toHaveBeenCalledTimes(0) + + fireEvent.change(getByTestId('requriedField'), { target: { value: 2222 } }) + await wait(); + expect(inpueFieldValidate).toHaveBeenCalledTimes(1) + expect(requriedFieldValidate).toHaveBeenCalledTimes(1) +}) diff --git a/packages/react-schema-renderer/src/__tests__/validate_relations.spec.tsx b/packages/react-schema-renderer/src/__tests__/validate_relations.spec.tsx new file mode 100644 index 00000000000..73d1f278c68 --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/validate_relations.spec.tsx @@ -0,0 +1,76 @@ +import React, { Fragment } from 'react' +import { + registerFormField, + connect, + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, + createFormActions, + registerFieldMiddleware +} from '../index' +import { render, fireEvent, wait } from '@testing-library/react' + +registerFieldMiddleware(Field => { + return props => { + const index = props.schema['x-props'] && props.schema['x-props'].index + return ( +
    + +
    {props.errors}
    +
    + ) + } +}) +registerFormField( + 'string', + connect()(props => ( + + )) +) + +test('setFieldState will trigger validate', async () => { + const handleSubmit = jest.fn() + const handleValidateFailed = jest.fn() + const actions = createFormActions() + const TestComponent = () => ( + { + $('onFieldChange', 'text-1').subscribe(({ value }) => { + if (value) { + actions.setFieldState('text-2', state => { + state.value = value + }) + } + }) + }} + onSubmit={handleSubmit} + onValidateFailed={handleValidateFailed} + > + + + + + + + ) + + const { getByTestId } = render() + + fireEvent.click(getByTestId('btn')) + await wait(); + expect(handleSubmit).toHaveBeenCalledTimes(0) + expect(handleValidateFailed).toHaveBeenCalledTimes(1) + expect(getByTestId('test-errors-2')).toHaveTextContent('text-2 is required') + fireEvent.change(getByTestId('test-input-1'), { target: { value: '123' } }) + await wait(); + expect(getByTestId('test-input-2')).toHaveAttribute('value', '123') + expect(getByTestId('test-errors-2')).not.toHaveTextContent( + 'text-2 is required' + ) +}) From ff454e9b0261eed95cc10299b3079b854d9e7669 Mon Sep 17 00:00:00 2001 From: Janry Date: Wed, 6 Nov 2019 13:02:33 +0800 Subject: [PATCH 23/99] test(@uform/react-schema-renderer): fix test case uncompat bugs (#373) --- packages/core/src/index.ts | 13 +-- packages/core/src/state/field.ts | 13 ++- .../src/__tests__/dynamic.spec.tsx | 43 +++++----- .../src/__tests__/editable.spec.tsx | 45 +++++------ .../src/__tests__/effects.spec.tsx | 48 +++++------ .../src/__tests__/validate_relations.spec.tsx | 4 +- ...{schema_form.spec.tsx => x-rules.spec.tsx} | 19 +++-- .../src/components/SchemaForm.tsx | 6 +- .../src/shared/schema.ts | 2 +- packages/react/src/components/Field.tsx | 29 +------ packages/react/src/hooks/useField.ts | 34 ++++++-- packages/react/src/hooks/useForm.ts | 80 ++++++++----------- 12 files changed, 161 insertions(+), 175 deletions(-) rename packages/react-schema-renderer/src/__tests__/{schema_form.spec.tsx => x-rules.spec.tsx} (81%) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5ae8607ba71..d8217d000ab 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -361,7 +361,7 @@ export function createForm( state.props = props state.required = required state.rules = rules as any - state.editable = editable + state.selfEditable = editable state.formEditable = options.editable }) batchRunTaskQueue(field, nodePath) @@ -700,14 +700,17 @@ export function createForm( state.submitting = true }) env.submittingTask = validate() - .then(validated => { - const { errors } = validated - if (errors.length) { + .then(() => { + const validated = state.getState(({ errors, warnings }) => ({ + errors, + warnings + })) //因为要合并effectErrors/effectWarnings,所以不能直接读validate的结果 + if (validated.errors.length) { state.setState(state => { state.submitting = false }) heart.publish(LifeCycleTypes.ON_FORM_SUBMIT_END, state) - return Promise.reject(errors) + return Promise.reject(validated.errors) } if (isFn(onSubmit)) { return Promise.resolve( diff --git a/packages/core/src/state/field.ts b/packages/core/src/state/field.ts index 8af02bc6d15..405c7138c12 100644 --- a/packages/core/src/state/field.ts +++ b/packages/core/src/state/field.ts @@ -77,15 +77,14 @@ export const FieldState = createStateModel( readRules({ rules, required }: IFieldStateProps) { let newRules = isValid(rules) ? clone(toArr(rules)) : this.state.rules if (isValid(required)) { - if (required) { - if (!newRules.some(rule => rule && rule.required)) { - newRules.push({ required: true }) - } - } else { - newRules = newRules.filter(rule => rule && !rule.required) + if ( + required && + !newRules.some(rule => rule && rule.required !== undefined) + ) { + newRules.push({ required }) } } else { - required = newRules.some(rule => rule && rule.required) + required = newRules.some(rule => rule && rule.required === true) } return { rules: newRules, diff --git a/packages/react-schema-renderer/src/__tests__/dynamic.spec.tsx b/packages/react-schema-renderer/src/__tests__/dynamic.spec.tsx index 1e8f0dca35d..7bd0012d894 100644 --- a/packages/react-schema-renderer/src/__tests__/dynamic.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/dynamic.spec.tsx @@ -11,8 +11,10 @@ import { import { toArr } from '@uform/shared' import { render, fireEvent, wait, act } from '@testing-library/react' -const sleep = (time) => { - return wait(() => {}, { timeout: time }) +const sleep = timeout => { + return new Promise(resolve => { + setTimeout(resolve, timeout) + }) } let FormCard @@ -442,9 +444,9 @@ test('invalid schema', async () => { ) } const { queryByText, queryAllByTestId } = render() - await wait(); + await sleep(33) fireEvent.click(queryByText('Add Field')) - await wait(); + await wait() expect(queryAllByTestId('item').length).toBe(2) expect(queryAllByTestId('input').length).toBe(4) }) @@ -474,7 +476,6 @@ test('dynamic change functions onChange/onReset/onSubmit/onValidateFailed', asyn onReset={() => { if (constState.testA !== '123') { act(() => setState({ testC: '456' })) - console.log('执行重置') } }} onSubmit={() => { @@ -482,8 +483,7 @@ test('dynamic change functions onChange/onReset/onSubmit/onValidateFailed', asyn act(() => setState({ testD: '456' })) } }} - onValidateFailed={(p) => { - console.log(p) + onValidateFailed={p => { if (constState.testA !== '123') { act(() => setState({ testE: '456' })) } @@ -518,12 +518,11 @@ test('dynamic change functions onChange/onReset/onSubmit/onValidateFailed', asyn ) } const { queryByTestId, queryByText } = render() - - await sleep(33) + await sleep(100) fireEvent.click(queryByTestId('radio-a2')) await wait() expect(queryByText('valueB-456')).toBeVisible() - actions.reset({validate:false}) + actions.reset({ validate: false }) await wait() expect(queryByText('valueC-456')).toBeVisible() fireEvent.click(queryByText('Submit')) @@ -573,22 +572,22 @@ test('dynamic remove field and relationship needs to be retained', async () => { const { queryAllByTestId, queryByText, queryAllByText } = render( ) - await wait(); + await wait() expect(queryAllByTestId('input').length).toBe(2) let removes - await wait(); + await wait() removes = queryAllByText('Remove Field') fireEvent.click(removes[removes.length - 1]) - await wait(); + await wait() removes = queryAllByText('Remove Field') fireEvent.click(removes[removes.length - 1]) - await wait(); + await wait() expect(queryAllByTestId('input').length).toBe(0) - await wait(); + await wait() fireEvent.click(queryByText('Add Field')) - await wait(); + await wait() fireEvent.click(queryByText('Add Field')) - await wait(); + await wait() expect(queryAllByTestId('input').length).toBe(4) expect(queryAllByTestId('input')[0].getAttribute('value')).toBe('') expect(queryAllByTestId('input')[1].getAttribute('value')).toBe('') @@ -628,20 +627,20 @@ test('after deleting a component should not be sync an default value', async () const { queryAllByTestId, queryByText, queryAllByText } = render( ) - await wait(); + await wait() expect(queryAllByTestId('input').length).toBe(4) let removes - await wait(); + await wait() removes = queryAllByText('Remove Field') fireEvent.click(removes[removes.length - 1]) - await wait(); + await wait() removes = queryAllByText('Remove Field') fireEvent.click(removes[removes.length - 1]) - await wait(); + await wait() expect(queryAllByTestId('input').length).toBe(0) fireEvent.click(queryByText('Add Field')) fireEvent.click(queryByText('Add Field')) - await wait(); + await wait() expect(queryAllByTestId('input').length).toBe(2) expect(queryAllByTestId('input')[0].getAttribute('value')).toBe('') expect(queryAllByTestId('input')[1].getAttribute('value')).toBe('') diff --git a/packages/react-schema-renderer/src/__tests__/editable.spec.tsx b/packages/react-schema-renderer/src/__tests__/editable.spec.tsx index b5a23e256bd..cc10fc9361a 100644 --- a/packages/react-schema-renderer/src/__tests__/editable.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/editable.spec.tsx @@ -6,7 +6,6 @@ import { SchemaMarkupForm as SchemaForm, SchemaMarkupField as Field, createFormActions, - // createVirtualBox, registerFieldMiddleware } from '../index' import { toArr } from '@uform/shared' @@ -89,10 +88,9 @@ test('update editable by setFieldState', async () => {
    ) - const { queryByText,baseElement } = render() + const { queryByText } = render() await wait() expect(queryByText('text')).toBeNull() - console.log(baseElement.innerHTML) actions.setFieldState('aaa', state => { state.editable = true }) @@ -124,12 +122,12 @@ test('update editable by setFieldState with initalState is not editable', async ) const { queryByText } = render() - await wait(); + await wait() expect(queryByText('text')).toBeNull() actions.setFieldState('aaa', state => { state.editable = true }) - await wait(); + await wait() expect(queryByText('text')).toBeVisible() }) @@ -146,13 +144,13 @@ test('update editable in controlled', async () => { } const { queryByText } = render() - await wait(); + await wait() expect(queryByText('text')).toBeVisible() act(() => updateEditable(false)) - await wait(); + await wait() expect(queryByText('text')).toBeNull() act(() => updateEditable(true)) - await wait(); + await wait() expect(queryByText('text')).toBeVisible() }) @@ -172,12 +170,12 @@ test('editable with x-props', async () => { } const { queryByText } = render() - await wait(); + await wait() expect(queryByText('text')).toBeNull() actions.setFieldState('aaa', state => { state.editable = true }) - await wait(); + await wait() expect(queryByText('text')).toBeVisible() }) @@ -204,12 +202,12 @@ test('editable with x-props in array field', async () => { } const { queryByText } = render() - await wait(); + await wait() expect(queryByText('empty')).toBeVisible() actions.setFieldState('array.0.aa', state => { state.editable = true }) - await wait(); + await wait() expect(queryByText('empty')).toBeNull() }) @@ -237,12 +235,12 @@ test('editable with x-props is affected by global editable', async () => { } const { queryByText } = render() - await wait(); + await wait() expect(queryByText('empty')).toBeNull() actions.setFieldState('array.0.aa', state => { state.editable = false }) - await wait(); + await wait() expect(queryByText('empty')).toBeVisible() }) @@ -281,16 +279,16 @@ test('editable conflicts that global editable props with setFieldState', async ( } const { queryByTestId } = render() - await wait(); + await wait() expect(queryByTestId('this is aaa')).toBeVisible() expect(queryByTestId('this is bbb')).toBeVisible() expect(queryByTestId('this is ccc')).toBeVisible() fireEvent.change(queryByTestId('this is ccc'), { target: { value: '123' } }) - await wait(); + await wait() expect(queryByTestId('this is bbb')).toBeVisible() fireEvent.change(queryByTestId('this is ccc'), { target: { value: '321' } }) - await wait(); + await wait() expect(queryByTestId('this is bbb')).toBeVisible() }) @@ -329,19 +327,18 @@ test('editable conflicts that props editable props with setFieldState', async () } const { queryByTestId } = render() - await wait(); + await wait() expect(queryByTestId('this is aaa')).toBeVisible() expect(queryByTestId('this is bbb')).toBeVisible() expect(queryByTestId('this is ccc')).toBeVisible() fireEvent.change(queryByTestId('this is ccc'), { target: { value: '123' } }) - await wait(); + await wait() expect(queryByTestId('this is bbb')).toBeVisible() fireEvent.change(queryByTestId('this is ccc'), { target: { value: '321' } }) - await wait(); + await wait() expect(queryByTestId('this is bbb')).toBeVisible() }) - test('editable conflicts that x-props editable props with setFieldState', async () => { const TestComponent = () => { return ( @@ -376,14 +373,14 @@ test('editable conflicts that x-props editable props with setFieldState', async } const { queryByTestId } = render() - await wait(); + await wait() expect(queryByTestId('this is aaa')).toBeVisible() expect(queryByTestId('this is bbb')).toBeVisible() expect(queryByTestId('this is ccc')).toBeVisible() fireEvent.change(queryByTestId('this is ccc'), { target: { value: '123' } }) - await wait(); + await wait() expect(queryByTestId('this is bbb')).toBeVisible() fireEvent.change(queryByTestId('this is ccc'), { target: { value: '321' } }) - await wait(); + await wait() expect(queryByTestId('this is bbb')).toBeVisible() }) diff --git a/packages/react-schema-renderer/src/__tests__/effects.spec.tsx b/packages/react-schema-renderer/src/__tests__/effects.spec.tsx index 165ea46b602..13d7f05f61b 100644 --- a/packages/react-schema-renderer/src/__tests__/effects.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/effects.spec.tsx @@ -6,10 +6,8 @@ import { SchemaMarkupForm as SchemaForm, SchemaMarkupField as Field, createFormActions, - // createVirtualBox, registerFieldMiddleware } from '../index' -// import { toArr } from '@uform/shared' import { render, wait, act, fireEvent } from '@testing-library/react' import { filter } from 'rxjs/operators' @@ -30,9 +28,13 @@ registerFieldMiddleware(Field => { }) registerFormField( 'string', - connect()(props => props.disabled - ? Disabled - : ) + connect()(props => + props.disabled ? ( + Disabled + ) : ( + + ) + ) ) test('onFormInit setFieldState', async () => { @@ -65,19 +67,19 @@ test('onFormInit setFieldState', async () => { const { getByText, getByTestId, queryByText } = render() - await wait(); + await wait() expect(queryByText('text')).toBeVisible() - await wait(); + await wait() fireEvent.click(getByTestId('btn')) - await wait(); - expect(getByText('field is required')).toBeVisible() - await wait(); + await wait() + expect(getByText('This field is required')).toBeVisible() + await wait() actions.setFieldState('aaa', state => { state.rules = [] }) - await wait(); + await wait() fireEvent.click(getByTestId('btn')) - await wait(); + await wait() expect(queryByText('field is required')).toBeNull() }) @@ -101,7 +103,7 @@ test('init triggers', async () => { } render() - await wait(); + await wait() expect(callback).toHaveBeenCalledTimes(1) }) @@ -136,7 +138,7 @@ test('onFieldChange will trigger with initialValues', async () => { } render() - await wait(); + await wait() expect(callback).toHaveBeenCalledTimes(2) expect(callback.mock.calls[0][0].value).toBe(undefined) expect(callback.mock.calls[1][0].value).toBe(123) @@ -165,7 +167,7 @@ test('setFieldState x-props with onFormInit', async () => { } const { queryByText } = render() - await wait(); + await wait() expect(queryByText('Disabled')).toBeVisible() }) @@ -194,9 +196,9 @@ test('getFieldState with onFieldChange', async () => { ) } const { queryByTestId } = render() - await wait(); + await wait() fireEvent.change(queryByTestId('this is aa'), { target: { value: '333' } }) - await wait(); + await wait() expect(aaValue).toBe('333') }) @@ -225,9 +227,9 @@ test('set errors in effects', async () => { } const { queryByTestId } = render() - await wait(); + await wait() fireEvent.click(queryByTestId('btn')) - await wait(); + await wait() expect(callback).toHaveBeenCalledTimes(0) }) @@ -239,7 +241,7 @@ test('setFieldState from buffer', async () => { effects={($, { setFieldState }) => { $('onFormInit').subscribe(() => { setFieldState(FormPath.match('*') as any, state => { - (state as any).title = '1123' + ;(state as any).title = '1123' }) }) $('onFieldChange', 'kkk').subscribe(() => { @@ -266,7 +268,7 @@ test('setFieldState from buffer', async () => { } const { queryByTestId } = render() - await wait(); + await wait() expect(queryByTestId('test')).toBeVisible() }) @@ -289,7 +291,7 @@ test('filter first onFieldChange', async () => { } render() - await wait(); + await wait() expect(sub1).toHaveBeenCalledTimes(0) expect(sub2).toHaveBeenCalledTimes(1) -}) \ No newline at end of file +}) diff --git a/packages/react-schema-renderer/src/__tests__/validate_relations.spec.tsx b/packages/react-schema-renderer/src/__tests__/validate_relations.spec.tsx index 73d1f278c68..bcabfebfc20 100644 --- a/packages/react-schema-renderer/src/__tests__/validate_relations.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/validate_relations.spec.tsx @@ -66,11 +66,11 @@ test('setFieldState will trigger validate', async () => { await wait(); expect(handleSubmit).toHaveBeenCalledTimes(0) expect(handleValidateFailed).toHaveBeenCalledTimes(1) - expect(getByTestId('test-errors-2')).toHaveTextContent('text-2 is required') + expect(getByTestId('test-errors-2')).toHaveTextContent('This field is required') fireEvent.change(getByTestId('test-input-1'), { target: { value: '123' } }) await wait(); expect(getByTestId('test-input-2')).toHaveAttribute('value', '123') expect(getByTestId('test-errors-2')).not.toHaveTextContent( - 'text-2 is required' + 'This field is required' ) }) diff --git a/packages/react-schema-renderer/src/__tests__/schema_form.spec.tsx b/packages/react-schema-renderer/src/__tests__/x-rules.spec.tsx similarity index 81% rename from packages/react-schema-renderer/src/__tests__/schema_form.spec.tsx rename to packages/react-schema-renderer/src/__tests__/x-rules.spec.tsx index 9a6538db7d5..6b0d7a3e5f2 100644 --- a/packages/react-schema-renderer/src/__tests__/schema_form.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/x-rules.spec.tsx @@ -3,7 +3,7 @@ import { registerFormField, connect, SchemaMarkupForm as SchemaForm, - SchemaMarkupField as Field, + SchemaMarkupField as Field } from '../index' import { render, fireEvent, wait } from '@testing-library/react' @@ -22,7 +22,7 @@ test('Increase lastValidateValue value processing during initialization', async { inpueFieldValidate() return '' - } - } + }} /> ) fireEvent.change(getByTestId('inpueField'), { target: { value: 1111 } }) - await wait(); + await wait() expect(inpueFieldValidate).toHaveBeenCalledTimes(1) expect(requriedFieldValidate).toHaveBeenCalledTimes(0) - fireEvent.change(getByTestId('requriedField'), { target: { value: 2222 } }) - await wait(); - expect(inpueFieldValidate).toHaveBeenCalledTimes(1) - expect(requriedFieldValidate).toHaveBeenCalledTimes(1) + // fireEvent.change(getByTestId('requriedField'), { target: { value: 2222 } }) + // await wait(); + // expect(inpueFieldValidate).toHaveBeenCalledTimes(1) + // expect(requriedFieldValidate).toHaveBeenCalledTimes(1) }) diff --git a/packages/react-schema-renderer/src/components/SchemaForm.tsx b/packages/react-schema-renderer/src/components/SchemaForm.tsx index 0491af586e0..a244ac43d31 100644 --- a/packages/react-schema-renderer/src/components/SchemaForm.tsx +++ b/packages/react-schema-renderer/src/components/SchemaForm.tsx @@ -26,9 +26,9 @@ export const SchemaForm: React.FC = props => { formComponent, { ...formComponentProps, - onSubmit: (e) => { - if(e && e.preventDefault) e.preventDefault(); - form.submit() + onSubmit: e => { + if (e && e.preventDefault) e.preventDefault() + form.submit().catch(e => console.warn(e)) }, onReset: () => { form.reset({ validate: false, forceClear: false }) diff --git a/packages/react-schema-renderer/src/shared/schema.ts b/packages/react-schema-renderer/src/shared/schema.ts index 31f19b5de77..1055b85f3e4 100644 --- a/packages/react-schema-renderer/src/shared/schema.ts +++ b/packages/react-schema-renderer/src/shared/schema.ts @@ -230,7 +230,7 @@ export class Schema implements ISchema { return this.required } } - getExtendsEditable() { + getExtendsEditable(): boolean { if (isValid(this.editable)) { return this.editable } else if (isValid(this['x-props'] && this['x-props'].editable)) { diff --git a/packages/react/src/components/Field.tsx b/packages/react/src/components/Field.tsx index 872c65ab120..2045eeb5c23 100644 --- a/packages/react/src/components/Field.tsx +++ b/packages/react/src/components/Field.tsx @@ -1,34 +1,7 @@ import React from 'react' import { useField } from '../hooks/useField' import { isFn } from '@uform/shared' -import { IMutators, IFieldState } from '@uform/core' import { IFieldProps } from '../types' -import { getValueFromEvent } from '../shared' - -const createFieldMutators = ( - mutators: IMutators, - props: IFieldProps, - state: IFieldState -): IMutators => { - return { - ...mutators, - change: (...args) => { - args[0] = isFn(props.getValueFromEvent) - ? props.getValueFromEvent(...args) - : args[0] - mutators.change(...args.map(event => getValueFromEvent(event))) - if (props.triggerType === 'onChange') { - mutators.validate() - } - }, - blur: () => { - mutators.blur() - if (props.triggerType === 'onBlur') { - mutators.validate() - } - } - } -} export const Field: React.FC = props => { const { state, props: innerProps, mutators, form } = useField(props) @@ -38,7 +11,7 @@ export const Field: React.FC = props => { form, state, props: innerProps, - mutators: createFieldMutators(mutators, props, state) + mutators }) } else { return {props.children} diff --git a/packages/react/src/hooks/useField.ts b/packages/react/src/hooks/useField.ts index 7ba15f39d55..200917f5671 100644 --- a/packages/react/src/hooks/useField.ts +++ b/packages/react/src/hooks/useField.ts @@ -1,12 +1,36 @@ import { useMemo, useEffect, useRef, useContext } from 'react' -import { each } from '@uform/shared' -import { IFieldStateProps, IFieldState, IForm, IField } from '@uform/core' -import { raf } from '../shared' +import { each, isFn } from '@uform/shared' +import { + IFieldStateProps, + IFieldState, + IForm, + IField, + IMutators +} from '@uform/core' +import { raf, getValueFromEvent } from '../shared' import { useDirty } from './useDirty' import { useForceUpdate } from './useForceUpdate' -import { IFieldHook } from '../types' +import { IFieldHook, IFieldProps } from '../types' import FormContext from '../context' +const extendMutators = (mutators: IMutators, props: IFieldProps): IMutators => { + return { + ...mutators, + change: (...args) => { + args[0] = isFn(props.getValueFromEvent) + ? props.getValueFromEvent(...args) + : args[0] + mutators.change(...args.map(event => getValueFromEvent(event))) + }, + blur: () => { + mutators.blur() + if (props.triggerType === 'onBlur') { + mutators.validate() + } + } + } +} + export const useField = ( options: IFieldStateProps & { triggerType?: 'onChange' | 'onBlur' } ): IFieldHook => { @@ -45,7 +69,7 @@ export const useField = ( } }) initialized = true - return form.createMutators(ref.current.field) + return extendMutators(form.createMutators(ref.current.field), options) }, []) useEffect(() => { diff --git a/packages/react/src/hooks/useForm.ts b/packages/react/src/hooks/useForm.ts index a74f38db252..0c9915f2f1a 100644 --- a/packages/react/src/hooks/useForm.ts +++ b/packages/react/src/hooks/useForm.ts @@ -35,7 +35,6 @@ const useInternalForm = ( state.values = options.values } if (dirty.dirtys.initialValues && isValid(options.initialValues)) { - state.values = options.initialValues state.initialValues = options.initialValues } if (dirty.dirtys.editable && isValid(options.editable)) { @@ -77,54 +76,45 @@ export const useForm = < actions: actionsRef.current, effects: createFormEffects(props.effects, actionsRef.current) }) - const optionsRef = useRef({ - form: props.form, - values: props.value, - initialValues: props.initialValues, - useDirty: props.useDirty, - editable: props.editable, - validateFirst: props.validateFirst, - lifecycles: [ - new FormLifeCycle( - ({ type, payload }: { type: string; payload: IModel }) => { - dispatch.lazy(type, () => { - return isStateModel(payload) ? payload.getState() : payload - }) - if (type === LifeCycleTypes.ON_FORM_VALUES_CHANGE) { - if (optionsRef.current.onChange) { - optionsRef.current.onChange( - isStateModel(payload) - ? payload.getState((state: IFormState) => state.values) - : {} - ) - } - } - if (broadcast) { - broadcast.notify({ type, payload }) + const lifecycles = [ + new FormLifeCycle( + ({ type, payload }: { type: string; payload: IModel }) => { + dispatch.lazy(type, () => { + return isStateModel(payload) ? payload.getState() : payload + }) + if (type === LifeCycleTypes.ON_FORM_VALUES_CHANGE) { + if (optionsRef.current.onChange) { + optionsRef.current.onChange( + isStateModel(payload) + ? payload.getState((state: IFormState) => state.values) + : {} + ) } } - ), - new FormLifeCycle( - LifeCycleTypes.ON_FORM_WILL_INIT, - (payload: IModel, form: IForm) => { - const actions = { - ...form, - dispatch: form.notify - } - implementActions(actions) - if (broadcast) { - broadcast.setContext(actions) - } + if (broadcast) { + broadcast.notify({ type, payload }) } - ) - ] - }) - + } + ), + new FormLifeCycle( + LifeCycleTypes.ON_FORM_WILL_INIT, + (payload: IModel, form: IForm) => { + const actions = { + ...form, + dispatch: form.notify + } + implementActions(actions) + if (broadcast) { + broadcast.setContext(actions) + } + } + ) + ] + const optionsRef = useRef({ ...props }) + Object.assign(optionsRef.current, props) + optionsRef.current.values = props.value + optionsRef.current.lifecycles = lifecycles const form = useInternalForm(optionsRef.current) - optionsRef.current.onChange = props.onChange - optionsRef.current.onReset = props.onReset - optionsRef.current.onSubmit = props.onSubmit - optionsRef.current.onValidateFailed = props.onValidateFailed return form } From 9cd727257926cdd9e3fa764481ad949dff68eb42 Mon Sep 17 00:00:00 2001 From: s0ngyee Date: Wed, 6 Nov 2019 20:41:39 +0800 Subject: [PATCH 24/99] feat(shared): add unit test (#374) * feat(shared): add unit test * chore(shared): remove debug code * chore(shared): update dependencies --- package.json | 2 + packages/shared/src/__tests__/index.spec.ts | 407 +++++++++++++++----- packages/shared/src/compare.ts | 9 +- 3 files changed, 322 insertions(+), 96 deletions(-) diff --git a/package.json b/package.json index 6869d966dd9..4490528b021 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "fs-extra": "^7.0.1", "ghooks": "^2.0.4", "glob": "^7.1.3", + "immutable": "^4.0.0-rc.12", "istanbul-api": "^2.1.1", "istanbul-lib-coverage": "^2.0.3", "jest": "^24.1.0", @@ -95,6 +96,7 @@ "lerna": "^3.10.1", "lint-staged": "^8.2.1", "majo": "^0.7.1", + "moment": "^2.24.0", "onchange": "^5.2.0", "prettier": "^1.18.2", "pretty-format": "^24.0.0", diff --git a/packages/shared/src/__tests__/index.spec.ts b/packages/shared/src/__tests__/index.spec.ts index 8b0cfc66ae2..cd1795db081 100644 --- a/packages/shared/src/__tests__/index.spec.ts +++ b/packages/shared/src/__tests__/index.spec.ts @@ -1,116 +1,341 @@ +import moment from 'moment' +import { Map as ImmutableMap } from 'immutable' import { isEqual } from '../compare' -import { toArr, every, some, findIndex, find, includes } from '../array' +import { toArr, every, some, findIndex, find, includes, map, reduce } from '../array' import { clone } from '../clone' +import { lowercase } from '../case' +import { deprecate } from '../deprecate' +import { isValid, isEmpty } from '../isEmpty' +import { stringLength } from '../string' +import { Subscribable } from '../subscribable' +describe('array', () => { + test('toArr', () => { + expect(isEqual(toArr([123]), [123])).toBeTruthy() + expect(isEqual(toArr(123), [123])).toBeTruthy() + expect(isEqual(toArr(null), [])).toBeTruthy() + }) -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, key) => `${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() + }) }) -test('clone form data', () => { - var dd = new Map() - dd.set('aaa', { bb: 123 }) - var a = { - aa: 123123, - bb: [{ bb: 111 }, { bb: 222 }], - cc: () => { - // eslint-disable-next-line no-console - console.log('123') - }, - dd - } - var 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).toBeFalsy() - expect(a.dd.get('aaa') === cloned.dd.get('aaa')).toBeTruthy() - expect(a.cc === cloned.cc).toBeTruthy() +describe('case', () => { + test('lowercase', () => { + expect(lowercase('SOME_UPPER_CASE_TEXT')).toEqual('some_upper_case_text') + expect(lowercase('')).toEqual('') + }) }) -test('filter equal', () => { - var a = { - aa: { - bb: 123 +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() + expect(isEqual([{ k1: 'v1' }, { k2: 'v2' }], [{ k1: 'v1' }], (_, key) => key !== 'k1')).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://uformjs.org/') + const urlB = new URL('https://www.taobao.com') + const urlC = new URL('https://uformjs.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() +}) + +describe('clone and compare', () => { + test('clone form data', () => { + var dd = new Map() + dd.set('aaa', { bb: 123 }) + // var ee = new WeakMap() + // ee.set({}, 1) + // var ff = new WeakSet() + // ff.add({}) + // var gg = new Set() + // gg.add(3) + + var a = { + aa: 123123, + bb: [{ bb: 111 }, { bb: 222 }], + cc: () => { + // eslint-disable-next-line no-console + console.log('123') + }, + dd, + // ee, + // ff, + // gg + } + var 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).toBeFalsy() + 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() + }) + + test('filter equal', () => { + var a = { + aa: { + bb: 123 + } } - } - var b = { - aa: { - bb: 123 + var b = { + aa: { + bb: 123 + } } - } + + expect(isEqual(a, b)).toBeTruthy() + expect(isEqual(a, b, (_, key) => key !== 'aa')).toBeTruthy() + }) + + test('filter clone', () => { + var a = { + aa: { + bb: 123 + }, + cc: { + dd: [1, 3, 4, 5] + } + } + + var b = clone(a, (_, key) => key !== 'aa') + + expect(a.aa === b.aa).toBeTruthy() + expect(a.cc === b.cc).toBeFalsy() + expect(isEqual(a.cc, b.cc)).toBeTruthy() + }) +}) + +describe('deprecate', () => { + test('deprecate', () => { + const test = jest.fn(() => { + console.log('### 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) - expect(isEqual(a, b)).toBeTruthy() - expect(isEqual(a, b, (_, key) => key !== 'aa')).toBeTruthy() + + // arguments - empty string + const testDeprecatedFn2 = jest.fn(() => deprecate('Some.Deprecated.Api')) + testDeprecatedFn2() + expect(testDeprecatedFn2).toHaveBeenCalledTimes(1) + }) }) -test('filter clone', () => { - var a = { - aa: { - bb: 123 - }, - cc: { - dd: [1, 3, 4, 5] +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.log(payload) } - } + expect(isEmpty(emptyFunc)).toBeTruthy() + expect(isEmpty(nonEmptyFunc)).toBeFalsy() - var b = clone(a, (_, key) => key !== 'aa') + // val - arrays + expect(isEmpty([])).toBeTruthy() + expect(isEmpty([1, 2, 3, 4, 5])).toBeFalsy() + expect(isEmpty([0, undefined, null, ''])).toBeTruthy() - expect(a.aa === b.aa).toBeTruthy() - expect(a.cc === b.cc).toBeFalsy() - expect(isEqual(a.cc, b.cc)).toBeTruthy() -}) + // val - errors + expect(isEmpty(new Error())).toBeTruthy() + expect(isEmpty(new Error('some error'))).toBeFalsy() + // val - objects + expect(isEmpty(new File(['foo'], 'filename.txt', { type: 'text/plain' }))).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() -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() + expect(isEmpty(Symbol())) + }) }) -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() +describe('string', () => { + test('stringLength', () => { + expect(stringLength('🦄some text')).toEqual(10) + }) }) -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() -}) +describe('shared Subscribable', () => { + test('Subscribable', () => { + const cb = jest.fn(payload => payload) -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() -}) + // 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' }) -test('includes', () => { - const value = [1, 2, 3, 4, 5] - expect(includes(value, 3)).toBeTruthy() - expect(includes(value, 6)).toBeFalsy() + 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.log(payload) + return false + }) + objWithCustomNotify.subscription = { + notify: customNotify + } + objWithCustomNotify.subscribe(cb) + objWithCustomNotify.notify({ key3: 'val3'}) + expect(customNotify).toBeCalledTimes(1) + }) }) diff --git a/packages/shared/src/compare.ts b/packages/shared/src/compare.ts index b295f41e299..e309881fccb 100644 --- a/packages/shared/src/compare.ts +++ b/packages/shared/src/compare.ts @@ -44,10 +44,6 @@ function equal(a: any, b: any, filter?: Filter) { const immutableB = b && b.toJS if (immutableA !== immutableB) return false if (immutableA) return a.is ? a.is(b) : a === b - const schemaA = a && a.toJSON - const schemaB = b && b.toJSON - if (schemaA !== schemaB) return false - if (schemaA && schemaB) return equal(a.toJSON(), b.toJSN(), filter) const dateA = a instanceof Date const dateB = b instanceof Date if (dateA !== dateB) { @@ -56,7 +52,10 @@ function equal(a: any, b: any, filter?: Filter) { if (dateA && dateB) { return a.getTime() === b.getTime() } - + const schemaA = a && a.toJSON + const schemaB = b && b.toJSON + if (schemaA !== schemaB) return false + if (schemaA && schemaB) return equal(a.toJSON(), b.toJSON(), filter) const regexpA = a instanceof RegExp const regexpB = b instanceof RegExp if (regexpA !== regexpB) { From dfec008ae88c28bb8c2163b38ae8cf5bc4db53c8 Mon Sep 17 00:00:00 2001 From: Janry Date: Wed, 6 Nov 2019 23:25:39 +0800 Subject: [PATCH 25/99] refactor: improve test case (#375) * test(@uform/react-schema-renderer): fix test case uncompat bugs * test(@uform/react-schema-renderer): improve test case * refactor(@uform/react-schema-renderer): improve test case * refactor(@uform/react-schema-renderer): improve test case * refactor(@uform/react-schema-renderer): improve test case --- packages/core/src/index.ts | 5 +- packages/core/src/types.ts | 1 + .../src/__old_tests__/actions.spec.js | 85 --- .../src/__old_tests__/context.spec.js | 53 -- .../src/__old_tests__/destruct.spec.js | 63 -- .../src/__old_tests__/display.spec.js | 132 ---- .../src/__old_tests__/dynamic.spec.js | 683 ------------------ .../src/__old_tests__/editable.spec.js | 377 ---------- .../src/__old_tests__/effects.spec.js | 280 ------- .../src/__old_tests__/mutators.spec.js | 41 -- .../src/__old_tests__/schema_form.spec.js | 60 -- .../src/__old_tests__/traverse.spec.js | 57 -- .../src/__old_tests__/utils.spec.js | 13 - .../__old_tests__/validate_relations.spec.js | 73 -- .../src/__tests__/field.spec.tsx | 21 - .../src/__tests__/form.spec.tsx | 21 - .../src/__tests__/json-schema.spec.tsx | 196 ++++- ...e_relations.spec.tsx => validate.spec.tsx} | 0 .../src/__tests__/x-rules.spec.tsx | 60 -- packages/react/src/hooks/useForm.ts | 17 +- packages/react/src/types.ts | 5 +- 21 files changed, 201 insertions(+), 2042 deletions(-) delete mode 100644 packages/react-schema-renderer/src/__old_tests__/actions.spec.js delete mode 100644 packages/react-schema-renderer/src/__old_tests__/context.spec.js delete mode 100644 packages/react-schema-renderer/src/__old_tests__/destruct.spec.js delete mode 100644 packages/react-schema-renderer/src/__old_tests__/display.spec.js delete mode 100644 packages/react-schema-renderer/src/__old_tests__/dynamic.spec.js delete mode 100644 packages/react-schema-renderer/src/__old_tests__/editable.spec.js delete mode 100644 packages/react-schema-renderer/src/__old_tests__/effects.spec.js delete mode 100644 packages/react-schema-renderer/src/__old_tests__/mutators.spec.js delete mode 100644 packages/react-schema-renderer/src/__old_tests__/schema_form.spec.js delete mode 100644 packages/react-schema-renderer/src/__old_tests__/traverse.spec.js delete mode 100644 packages/react-schema-renderer/src/__old_tests__/utils.spec.js delete mode 100644 packages/react-schema-renderer/src/__old_tests__/validate_relations.spec.js delete mode 100644 packages/react-schema-renderer/src/__tests__/field.spec.tsx delete mode 100644 packages/react-schema-renderer/src/__tests__/form.spec.tsx rename packages/react-schema-renderer/src/__tests__/{validate_relations.spec.tsx => validate.spec.tsx} (100%) delete mode 100644 packages/react-schema-renderer/src/__tests__/x-rules.spec.tsx diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d8217d000ab..1fe4a94aea9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -43,7 +43,7 @@ import { export * from './shared/lifecycle' export * from './types' -export function createForm( +export function createForm( options: IFormCreatorOptions = {} ): IForm { function onGraphChange({ type, payload }) { @@ -120,6 +120,9 @@ export function createForm( }) }) if (valuesChanged) { + if (isFn(options.onChange)) { + options.onChange(published.values) + } heart.publish(LifeCycleTypes.ON_FORM_VALUES_CHANGE, state) } if (initialValuesChanged) { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index deb95f9cf5e..e5d736d7a35 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -204,6 +204,7 @@ export interface IFormCreatorOptions extends IFormStateProps { useDirty?: boolean validateFirst?: boolean editable?: boolean + onChange?: (values: IFormState['values']) => void onSubmit?: (values: IFormState['values']) => any | Promise onReset?: () => void onValidateFailed?: (validated: IFormValidateResult) => void diff --git a/packages/react-schema-renderer/src/__old_tests__/actions.spec.js b/packages/react-schema-renderer/src/__old_tests__/actions.spec.js deleted file mode 100644 index 2a0adb4d77c..00000000000 --- a/packages/react-schema-renderer/src/__old_tests__/actions.spec.js +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react' -import SchemaForm, { - Field, - registerFormField, - connect, - createAsyncFormActions -} from '../index' -import { render } from '@testing-library/react' - -beforeEach(() => { - registerFormField('string', connect()(props =>
    {props.value}
    )) -}) - -test('createFormActions', async () => { - const actions = createAsyncFormActions() - const TestComponent = () => ( - { - $('onFormInit').subscribe(() => { - setFieldState('aaa', state => { - state.value = 'change value of aaa field onFormInit' - }) - }) - }} - > - - - - ) - - const { queryByText } = render() - await sleep(33) - expect(queryByText('change value of aaa field onFormInit')).toBeVisible() - await actions.setFormState(state => (state.values = { aaa: 123 })) - expect(queryByText('123')).toBeVisible() - await actions.setFieldState('aaa', state => (state.value = 'hello world')) - expect(queryByText('hello world')).toBeVisible() - const schemaData = [ - { name: 'aaa', value: 'value of aaa field' }, - { name: 'bbb', value: 'value of bbb field' } - ] - const updateQueue = [] - schemaData.forEach(({ name, value }) => { - updateQueue.push( - actions.setFieldState(name, state => { - state.value = value - }) - ) - }) - await Promise.all(updateQueue) - expect(queryByText('value of aaa field')).toBeVisible() - expect(queryByText('value of bbb field')).toBeVisible() -}) - -test('setFormState', async () => { - const actions = createAsyncFormActions() - const TestComponent = () => ( - { - $('onFieldChange', 'aaa').subscribe(({ value }) => { - if (value) { - actions.setFormState(state => { - state.values.bbb = '123' - }) - } - }) - }} - > - - - - ) - - const { queryByText } = render() - await sleep(33) - expect(queryByText('123')).toBeNull() - await actions.setFieldState('aaa', state => { - state.value = 'hello' - }) - await sleep(33) - expect(queryByText('hello')).toBeVisible() - expect(queryByText('123')).toBeVisible() -}) diff --git a/packages/react-schema-renderer/src/__old_tests__/context.spec.js b/packages/react-schema-renderer/src/__old_tests__/context.spec.js deleted file mode 100644 index 219ed9bc378..00000000000 --- a/packages/react-schema-renderer/src/__old_tests__/context.spec.js +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react' -import SchemaForm, { - Field, - registerFormField, - connect, - FormProvider, - FormConsumer, - createFormActions -} from '../index' -import { render, fireEvent, act } from '@testing-library/react' - -beforeEach(() => { - registerFormField( - 'string', - connect()(props => ) - ) -}) - -test('submit by form consumer', async () => { - const actions = createFormActions() - const TestComponent = () => ( - - { - await sleep(200) - }} - > - - - - {({ status, submit }) => { - if (status === 'submitting') { - return
    Submitting
    - } else { - return - } - }} -
    -
    - ) - - const { queryByText } = render() - await sleep(33) - act(() => { - fireEvent.click(queryByText('Submit')) - }) - await sleep(33) - expect(queryByText('Submitting')).toBeVisible() - await sleep(300) - expect(queryByText('Submitting')).toBeNull() - expect(queryByText('Submit')).toBeVisible() -}) diff --git a/packages/react-schema-renderer/src/__old_tests__/destruct.spec.js b/packages/react-schema-renderer/src/__old_tests__/destruct.spec.js deleted file mode 100644 index 871b83bef73..00000000000 --- a/packages/react-schema-renderer/src/__old_tests__/destruct.spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import React, { Fragment } from 'react' -import SchemaForm, { Field, registerFormField, connect } from '../index' -import { toArr } from '@uform/shared' -import { render } from '@testing-library/react' - -registerFormField('string', connect()(props =>
    {props.value}
    )) - -registerFormField('array', props => { - const { value, mutators, renderField } = props - return ( - - {toArr(value).map((item, index) => { - return ( -
    - {renderField(index)} -
    - ) - })} - -
    - ) -}) - -test('destruct with initial values', async () => { - const TestComponent = () => { - return ( - - - - ) - } - - const { queryByText } = render() - - await sleep(33) - expect(queryByText('123321')).toBeVisible() -}) - -test('destruct with initial values in array', async () => { - const TestComponent = () => { - return ( - - - - - - - - ) - } - - const { queryByText } = render() - - await sleep(33) - expect(queryByText('123321')).toBeVisible() -}) diff --git a/packages/react-schema-renderer/src/__old_tests__/display.spec.js b/packages/react-schema-renderer/src/__old_tests__/display.spec.js deleted file mode 100644 index d95f7f283dc..00000000000 --- a/packages/react-schema-renderer/src/__old_tests__/display.spec.js +++ /dev/null @@ -1,132 +0,0 @@ -import React from 'react' -import SchemaForm, { Field, registerFormField, connect } from '../index' -import { render, fireEvent } from '@testing-library/react' - -registerFormField('string', connect()(props =>
    {props.value}
    )) - -test('display is false will remove react node', async () => { - const TestComponent = () => { - return ( - { - $('onFormInit').subscribe(() => { - setFieldState('aa', state => { - state.display = false - }) - }) - }} - > - - - ) - } - - const { queryByText } = render() - - await sleep(33) - expect(queryByText('123321')).toBeNull() -}) - -test('display is false will remove react children node', async () => { - const TestComponent = () => { - return ( - { - $('onFormInit').subscribe(() => { - setFieldState('obj', state => { - state.display = false - }) - }) - }} - > - - - - - ) - } - - const { queryByText } = render() - - await sleep(33) - expect(queryByText('123321')).toBeNull() -}) - -test('display is false will not remove value(include default value)', async () => { - const onSubmitHandler = jest.fn() - const TestComponent = () => { - return ( - { - $('onFieldChange', 'bb').subscribe(({ value }) => { - if (value === '123') { - setFieldState('obj', state => { - state.display = false - }) - } - }) - }} - > - - - - - - - ) - } - - const { queryByText } = render() - - await sleep(33) - expect(queryByText('123321')).toBeNull() - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(onSubmitHandler).toHaveBeenCalledWith({ - obj: { aa: '123321' }, - bb: '123' - }) -}) - -test('display is false will not validate(include children)', async () => { - const onSubmitHandler = jest.fn() - const onValidateFailedHandler = jest.fn() - const TestComponent = () => { - return ( - { - $('onFieldChange', 'bb').subscribe(({ value }) => { - if (value === '123') { - setFieldState('obj', state => { - state.display = false - }) - } - }) - }} - > - - - - - - - ) - } - - const { queryByText } = render() - - await sleep(33) - expect(queryByText('123321')).toBeNull() - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(onSubmitHandler).toHaveBeenCalledWith({ - obj: { aa: '123321' }, - bb: '123' - }) - expect(onValidateFailedHandler).toHaveBeenCalledTimes(0) -}) diff --git a/packages/react-schema-renderer/src/__old_tests__/dynamic.spec.js b/packages/react-schema-renderer/src/__old_tests__/dynamic.spec.js deleted file mode 100644 index b15f9c09975..00000000000 --- a/packages/react-schema-renderer/src/__old_tests__/dynamic.spec.js +++ /dev/null @@ -1,683 +0,0 @@ -import React, { Fragment, useEffect, useState } from 'react' -import SchemaForm, { - Field, - registerFormField, - connect, - FormPath, - createFormActions, - createVirtualBox -} from '../index' -import { toArr } from '@uform/shared' -import { render, fireEvent, act } from '@testing-library/react' - -let FormCard - -registerFormField( - 'string', - connect()(props => ( - - )) -) - -registerFormField( - 'radio', - connect()(props => - props.dataSource.map(item => ( - - )) - ) -) - -registerFormField('container', props => { - const { value, mutators, renderField } = props - return ( - - {toArr(value).map((item, index) => { - return ( -
    - {renderField(index)} - -
    - ) - })} - -
    - ) -}) - -FormCard = createVirtualBox('card', ({ children }) => { - return
    card content{children}
    -}) - -test('dynaimc add field', async () => { - const actions = createFormActions() - const TestComponent = () => { - return ( - { - $('onFormInit').subscribe(() => { - setFieldState(FormPath.match('container.*.bb'), state => { - state.visible = false - }) - }) - - $('onFieldChange', FormPath.match('container.*.aa')).subscribe( - state => { - if (state.value === '123') { - setFieldState( - FormPath.transform(state.name, /\d/, $1 => { - return `container.${$1}.bb` - }), - state => { - state.visible = true - } - ) - } - } - ) - }} - > - - - - - - - - ) - } - - const { queryAllByTestId, queryByText } = render() - await sleep(33) - fireEvent.click(queryByText('Add Field')) - await sleep(33) - expect(queryAllByTestId('item').length).toBe(1) - expect(queryAllByTestId('input').length).toBe(1) - actions.setFieldState('container.0.bb', state => { - state.visible = true - }) - await sleep(33) - expect(queryAllByTestId('item').length).toBe(1) - expect(queryAllByTestId('input').length).toBe(2) - actions.setFieldState('container.0.bb', state => { - state.visible = false - }) - await sleep(33) - expect(queryAllByTestId('item').length).toBe(1) - expect(queryAllByTestId('input').length).toBe(1) - actions.setFieldState('container.0.aa', state => { - state.value = '123' - }) - await sleep(33) - expect(queryAllByTestId('item').length).toBe(1) - expect(queryAllByTestId('input').length).toBe(2) -}) - -test('dynaimc add field with initialValue', async () => { - const actions = createFormActions() - const TestComponent = () => { - return ( - { - $('onFormInit').subscribe(() => { - setFieldState(FormPath.match('container.*.bb'), state => { - state.visible = false - }) - }) - - $('onFieldChange', FormPath.match('container.*.aa')).subscribe( - state => { - if (state.value) { - if (state.value === '123') { - setFieldState( - FormPath.transform(state.name, /\d/, $1 => { - return `container.${$1}.bb` - }), - state => { - state.visible = true - } - ) - } else { - setFieldState( - FormPath.transform(state.name, /\d/, $1 => { - return `container.${$1}.bb` - }), - state => { - state.visible = false - } - ) - } - } - } - ) - }} - > - - - - - - - - ) - } - - const { queryAllByTestId, queryByText } = render() - await sleep(33) - expect(queryAllByTestId('item').length).toBe(1) - expect(queryAllByTestId('input').length).toBe(1) - fireEvent.click(queryByText('Add Field')) - await sleep(33) - expect(queryAllByTestId('item').length).toBe(2) - expect(queryAllByTestId('input').length).toBe(2) - actions.setFieldState('container.0.bb', state => { - state.visible = true - }) - await sleep(33) - expect(queryAllByTestId('item').length).toBe(2) - expect(queryAllByTestId('input').length).toBe(3) - actions.setFieldState('container.0.bb', state => { - state.visible = false - }) - await sleep(33) - expect(queryAllByTestId('item').length).toBe(2) - expect(queryAllByTestId('input').length).toBe(2) - actions.setFieldState('container.0.aa', state => { - state.value = '123' - }) - await sleep(33) - expect(queryAllByTestId('item').length).toBe(2) - expect(queryAllByTestId('input').length).toBe(3) - actions.setFieldState('container.0.aa', state => { - state.value = '321' - }) - await sleep(33) - expect(queryAllByTestId('item').length).toBe(2) - expect(queryAllByTestId('input').length).toBe(2) -}) - -test('dynaimc add field with initialValue in virtualbox', async () => { - const actions = createFormActions() - const submitHandler = jest.fn() - const TestComponent = () => { - return ( - { - $('onFormInit').subscribe(() => { - setFieldState(FormPath.match('container.*.bb'), state => { - state.visible = false - }) - }) - - $('onFieldChange', FormPath.match('container.*.aa')).subscribe( - state => { - if (state.value) { - if (state.value === '123') { - setFieldState( - FormPath.transform(state.name, /\d/, $1 => { - return `container.${$1}.bb` - }), - state => { - state.visible = true - } - ) - } else { - setFieldState( - FormPath.transform(state.name, /\d/, $1 => { - return `container.${$1}.bb` - }), - state => { - state.visible = false - } - ) - } - } - } - ) - }} - > - - - - - - - - - - - ) - } - - const { queryAllByTestId, queryByText } = render() - await sleep(33) - expect(queryAllByTestId('item').length).toBe(1) - expect(queryAllByTestId('input').length).toBe(1) - fireEvent.click(queryByText('Add Field')) - await sleep(33) - expect(queryAllByTestId('item').length).toBe(2) - expect(queryAllByTestId('input').length).toBe(2) - actions.setFieldState('container.0.bb', state => { - state.visible = true - }) - await sleep(33) - expect(queryAllByTestId('item').length).toBe(2) - expect(queryAllByTestId('input').length).toBe(3) - actions.setFieldState('container.0.bb', state => { - state.visible = false - }) - await sleep(33) - expect(queryAllByTestId('item').length).toBe(2) - expect(queryAllByTestId('input').length).toBe(2) - actions.setFieldState('container.0.aa', state => { - state.value = '123' - }) - await sleep(33) - expect(queryAllByTestId('item').length).toBe(2) - expect(queryAllByTestId('input').length).toBe(3) - actions.setFieldState('container.0.aa', state => { - state.value = '321' - }) - await sleep(33) - expect(queryAllByTestId('item').length).toBe(2) - expect(queryAllByTestId('input').length).toBe(2) - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(submitHandler).toHaveBeenCalledWith({ - container: [{ aa: '321' }, undefined] - }) -}) - -test('dynamic remove field', async () => { - const submitHandler = jest.fn() - const validateFaildHandler = jest.fn() - const TestComponent = () => { - return ( - - - - - - - - - - - - ) - } - - const { queryAllByTestId, queryByText } = render() - await sleep(33) - fireEvent.click(queryByText('Add Field')) - await sleep(33) - fireEvent.click(queryByText('Remove Field')) - await sleep(33) - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(submitHandler).toHaveBeenCalledTimes(1) - expect(validateFaildHandler).toHaveBeenCalledTimes(0) - await sleep(33) - fireEvent.click(queryByText('Add Field')) - await sleep(33) - expect(queryAllByTestId('item').length).toBe(1) - expect(queryAllByTestId('input').length).toBe(2) - await sleep(33) - fireEvent.click(queryByText('Remove Field')) - await sleep(33) - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(submitHandler).toHaveBeenCalledTimes(2) - expect(validateFaildHandler).toHaveBeenCalledTimes(0) -}) - -test('dynamic default value', async () => { - const TestComponent = () => { - return ( - - - - - - - - - - - - ) - } - - const { queryAllByTestId } = render() - await sleep(33) - expect(queryAllByTestId('item').length).toBe(1) - expect(queryAllByTestId('input').length).toBe(2) -}) - -test('dynamic async default value', async () => { - const TestComponent = () => { - const [schema, setSchema] = useState() - useEffect(() => { - setTimeout(() => { - act(() => { - setSchema({ - type: 'object', - properties: { - container: { - type: 'array', - default: [{}], - 'x-component': 'container', - items: { - type: 'object', - properties: { - aa: { - type: 'string' - }, - bb: { - type: 'string' - } - } - } - } - } - }) - }) - }, 33) - }, []) - return ( - - - - ) - } - const { queryAllByTestId } = render() - await sleep(33) - expect(queryAllByTestId('item').length).toBe(1) - expect(queryAllByTestId('input').length).toBe(2) -}) - -test('invalid schema', async () => { - const TestComponent = () => { - const [schema, setSchema] = useState() - useEffect(() => { - setTimeout(() => { - act(() => { - setSchema({ - type: 'object', - properties: { - container: { - type: 'array', - default: [{}], - 'x-component': 'container', - properties: {}, - items: { - type: 'object', - properties: { - aa: { - type: 'string' - }, - bb: { - type: 'string' - } - } - } - } - } - }) - }) - }, 33) - }, []) - return ( - - - - ) - } - const { queryByText, queryAllByTestId } = render() - await sleep(33) - fireEvent.click(queryByText('Add Field')) - await sleep(33) - expect(queryAllByTestId('item').length).toBe(2) - expect(queryAllByTestId('input').length).toBe(4) -}) - -test('dynamic change functions onChange/onReset/onSubmit/onValidateFailed', async () => { - const actions = createFormActions() - const TestComponent = () => { - const [state, setState] = useState({ testA: '123' }) - const constState = { ...state } - useEffect(() => { - setTimeout(() => { - act(() => setState({ testA: `${Math.random()}` })) - }, 10) - }, []) - return ( - - { - if (constState.testA !== '123') { - act(() => setState({ testB: '456' })) - } - }} - onReset={() => { - if (constState.testA !== '123') { - act(() => setState({ testC: '456' })) - } - }} - onSubmit={() => { - if (constState.testA !== '123') { - act(() => setState({ testD: '456' })) - } - }} - onValidateFailed={() => { - if (constState.testA !== '123') { - act(() => setState({ testE: '456' })) - } - }} - > - - - - -
    valueB-{constState.testB}
    -
    valueC-{constState.testC}
    -
    valueD-{constState.testD}
    -
    valueE-{constState.testE}
    -
    - ) - } - const { queryByTestId, queryAllByText, queryByText } = render( - - ) - await sleep(33) - fireEvent.click(queryByTestId('radio-a2')) - await sleep(33) - // onChange - expect(queryAllByText('valueB-456').length).toBe(1) - actions.reset({ validate: false }) - await sleep(33) - // onReset - expect(queryAllByText('valueC-456').length).toBe(1) - fireEvent.click(queryByText('Submit')) - await sleep(33) - // onValidateFailed - expect(queryAllByText('valueE-456').length).toBe(1) - fireEvent.click(queryByTestId('radio-b2')) - await sleep(33) - fireEvent.click(queryByText('Submit')) - await sleep(33) - // onSubmit - expect(queryAllByText('valueD-456').length).toBe(1) -}) - -test('dynamic remove field and relationship needs to be retained', async () => { - const TestComponent = () => { - return ( - { - $('onFieldChange', 'container.*.bb').subscribe(({ value, name }) => { - const siblingName = FormPath.transform(name, /\d+/, $d => { - return `container.${$d}.aa` - }) - setFieldState(FormPath.match(siblingName), state => { - state.visible = value !== '123' - }) - }) - }} - > - - - - - - - - - - - ) - } - - const { queryAllByTestId, queryByText, queryAllByText } = render( - - ) - expect(queryAllByTestId('input').length).toBe(2) - let removes - await sleep(33) - removes = queryAllByText('Remove Field') - fireEvent.click(removes[removes.length - 1]) - await sleep(33) - removes = queryAllByText('Remove Field') - fireEvent.click(removes[removes.length - 1]) - await sleep(33) - expect(queryAllByTestId('input').length).toBe(0) - await sleep(33) - fireEvent.click(queryByText('Add Field')) - await sleep(33) - fireEvent.click(queryByText('Add Field')) - await sleep(33) - expect(queryAllByTestId('input').length).toBe(2) - expect(queryAllByTestId('input')[0].value).toBe('123') - expect(queryAllByTestId('input')[1].value).toBe('123') -}) - -test('after deleting a component should not be sync an default value', async () => { - const TestComponent = () => { - return ( - { - $('onFieldChange', 'container.*.bb').subscribe(({ value, name }) => { - const siblingName = FormPath.transform(name, /\d+/, $d => { - return `container.${$d}.aa` - }) - setFieldState(FormPath.match(siblingName), state => { - state.visible = value === '123' - }) - }) - }} - > - - - - - - - - - ) - } - - const { queryAllByTestId, queryByText, queryAllByText } = render( - - ) - expect(queryAllByTestId('input').length).toBe(4) - let removes - await sleep(33) - removes = queryAllByText('Remove Field') - fireEvent.click(removes[removes.length - 1]) - await sleep(33) - removes = queryAllByText('Remove Field') - fireEvent.click(removes[removes.length - 1]) - await sleep(33) - expect(queryAllByTestId('input').length).toBe(0) - await sleep(33) - fireEvent.click(queryByText('Add Field')) - await sleep(33) - fireEvent.click(queryByText('Add Field')) - await sleep(33) - expect(queryAllByTestId('input').length).toBe(2) - expect(queryAllByTestId('input')[0].value).toBe('') - expect(queryAllByTestId('input')[1].value).toBe('') -}) diff --git a/packages/react-schema-renderer/src/__old_tests__/editable.spec.js b/packages/react-schema-renderer/src/__old_tests__/editable.spec.js deleted file mode 100644 index 6b29fa53977..00000000000 --- a/packages/react-schema-renderer/src/__old_tests__/editable.spec.js +++ /dev/null @@ -1,377 +0,0 @@ -import React, { useState, Fragment } from 'react' -import SchemaForm, { - Field, - registerFormField, - connect, - registerFieldMiddleware, - createFormActions, - FormPath -} from '../index' -import { render, act, fireEvent } from '@testing-library/react' -import { toArr } from '@uform/shared' - -registerFieldMiddleware(Field => { - return props => { - if (typeof props.editable === 'boolean' && props.name !== '') { - if (!props.editable) return
    empty
    - } - return ( -
    - {props.schema.title} - - {props.errors && props.errors.length ? ( -
    {props.errors}
    - ) : ( - '' - )} -
    - ) - } -}) -registerFormField( - 'string', - connect()(props => ) -) -registerFormField('array', props => { - const { value, mutators, renderField } = props - return ( - - {toArr(value).map((item, index) => { - return ( -
    - {renderField(index)} -
    - ) - })} - -
    - ) -}) - -test('update editable by setFieldState', async () => { - const actions = createFormActions() - const TestComponent = () => ( - { - $('onFormInit').subscribe(() => { - setFieldState('aaa', state => { - state.props.title = 'text' - state.rules = [ - { - required: true, - message: 'field is required' - } - ] - state.props.editable = false - }) - }) - }} - > - - - - ) - - const { queryByText } = render() - - await sleep(33) - expect(queryByText('text')).toBeNull() - await sleep(33) - actions.setFieldState('aaa', state => { - state.editable = true - }) - await sleep(33) - expect(queryByText('text')).toBeVisible() -}) - -test('update editable by setFieldState with initalState is not editable', async () => { - const actions = createFormActions() - const TestComponent = () => ( - { - return false - }} - effects={($, { setFieldState }) => { - $('onFormInit').subscribe(() => { - setFieldState('aaa', state => { - state.props.title = 'text' - }) - }) - }} - > - - - - ) - - const { queryByText } = render() - - await sleep(33) - expect(queryByText('text')).toBeNull() - await sleep(33) - actions.setFieldState('aaa', state => { - state.editable = true - }) - await sleep(33) - expect(queryByText('text')).toBeVisible() -}) - -test('update editable in controlled', async () => { - let updateEditable - const TestComponent = () => { - const [editable, _updateEditable] = useState(true) - updateEditable = _updateEditable - return ( - - - - ) - } - - const { queryByText } = render() - await sleep(33) - expect(queryByText('text')).toBeVisible() - act(() => updateEditable(false)) - await sleep(33) - expect(queryByText('text')).toBeNull() - act(() => updateEditable(true)) - await sleep(33) - expect(queryByText('text')).toBeVisible() -}) - -test('editable with x-props', async () => { - const actions = createFormActions() - const TestComponent = () => { - return ( - - - - ) - } - - const { queryByText } = render() - await sleep(33) - expect(queryByText('text')).toBeNull() - actions.setFieldState('aaa', state => { - state.editable = true - }) - await sleep(33) - expect(queryByText('text')).toBeVisible() -}) - -test('editable with x-props in array field', async () => { - const actions = createFormActions() - const TestComponent = () => { - return ( - - - - - - - - ) - } - - const { queryByText } = render() - await sleep(33) - expect(queryByText('empty')).toBeVisible() - actions.setFieldState('array.0.aa', state => { - state.editable = true - }) - await sleep(33) - expect(queryByText('empty')).toBeNull() -}) - -test('editable with x-props is affected by global editable', async () => { - const actions = createFormActions() - const TestComponent = () => { - return ( - - - - - - - - ) - } - - const { queryByText } = render() - await sleep(33) - expect(queryByText('empty')).toBeNull() - actions.setFieldState('array.0.aa', state => { - state.editable = false - }) - await sleep(33) - expect(queryByText('empty')).toBeVisible() -}) - -test('editable conflicts that global editable props with setFieldState', async () => { - const TestComponent = () => { - return ( - { - $('onFieldChange', 'ccc').subscribe(() => { - setFieldState('bbb', state => { - state.editable = true - }) - }) - }} - > - - - - - ) - } - - const { queryByTestId } = render() - await sleep(33) - expect(queryByTestId('this is aaa')).toBeVisible() - expect(queryByTestId('this is bbb')).toBeVisible() - expect(queryByTestId('this is ccc')).toBeVisible() - fireEvent.change(queryByTestId('this is ccc'), { target: { value: '123' } }) - await sleep(33) - expect(queryByTestId('this is bbb')).toBeVisible() - fireEvent.change(queryByTestId('this is ccc'), { target: { value: '321' } }) - await sleep(33) - expect(queryByTestId('this is bbb')).toBeVisible() -}) - -test('editable conflicts that props editable props with setFieldState', async () => { - const TestComponent = () => { - return ( - { - $('onFieldChange', 'ccc').subscribe(() => { - setFieldState('bbb', state => { - state.editable = true - }) - }) - }} - > - - - - - ) - } - - const { queryByTestId } = render() - await sleep(33) - expect(queryByTestId('this is aaa')).toBeVisible() - expect(queryByTestId('this is bbb')).toBeVisible() - expect(queryByTestId('this is ccc')).toBeVisible() - fireEvent.change(queryByTestId('this is ccc'), { target: { value: '123' } }) - await sleep(33) - expect(queryByTestId('this is bbb')).toBeVisible() - fireEvent.change(queryByTestId('this is ccc'), { target: { value: '321' } }) - await sleep(33) - expect(queryByTestId('this is bbb')).toBeVisible() -}) - -test('editable conflicts that x-props editable props with setFieldState', async () => { - const TestComponent = () => { - return ( - { - $('onFieldChange', 'ccc').subscribe(() => { - setFieldState('bbb', state => { - state.editable = true - }) - }) - }} - > - - - - - ) - } - - const { queryByTestId } = render() - await sleep(33) - expect(queryByTestId('this is aaa')).toBeVisible() - expect(queryByTestId('this is bbb')).toBeVisible() - expect(queryByTestId('this is ccc')).toBeVisible() - fireEvent.change(queryByTestId('this is ccc'), { target: { value: '123' } }) - await sleep(33) - expect(queryByTestId('this is bbb')).toBeVisible() - fireEvent.change(queryByTestId('this is ccc'), { target: { value: '321' } }) - await sleep(33) - expect(queryByTestId('this is bbb')).toBeVisible() -}) diff --git a/packages/react-schema-renderer/src/__old_tests__/effects.spec.js b/packages/react-schema-renderer/src/__old_tests__/effects.spec.js deleted file mode 100644 index a9289d528dd..00000000000 --- a/packages/react-schema-renderer/src/__old_tests__/effects.spec.js +++ /dev/null @@ -1,280 +0,0 @@ -import React, { useState, useEffect } from 'react' -import SchemaForm, { - Field, - registerFormField, - connect, - registerFieldMiddleware, - createFormActions, - FormPath -} from '../index' -import { filter } from 'rxjs/operators' -import { render, fireEvent, act } from '@testing-library/react' - -registerFieldMiddleware(Field => { - return props => { - return ( -
    - {props.schema.title} - - {props.errors && props.errors.length ? ( -
    {props.errors}
    - ) : ( - '' - )} -
    - ) - } -}) -registerFormField( - 'string', - connect()(props => - props.disabled ? 'Disabled' : - ) -) - -test('onFormInit setFieldState', async () => { - const actions = createFormActions() - const TestComponent = () => ( - { - $('onFormInit').subscribe(() => { - setFieldState('aaa', state => { - state.props.title = 'text' - state.rules = [ - { - required: true, - message: 'field is required' - } - ] - }) - }) - }} - > - - - - ) - - const { getByText, getByTestId, queryByText } = render() - - await sleep(33) - expect(queryByText('text')).toBeVisible() - await sleep(33) - fireEvent.click(getByTestId('btn')) - await sleep(33) - expect(getByText('field is required')).toBeVisible() - await sleep(33) - actions.setFieldState('aaa', state => { - state.rules = [] - }) - await sleep(33) - fireEvent.click(getByTestId('btn')) - await sleep(33) - expect(queryByText('field is required')).toBeNull() -}) - -test('init triggers', async () => { - const callback = jest.fn() - const TestComponent = () => { - return ( - { - $('onFieldChange', 'aaa').subscribe(callback) - }} - > - - - - ) - } - - render() - await sleep(33) - expect(callback).toHaveBeenCalledTimes(1) -}) - -test('onFieldChange will trigger with initialValues', async () => { - const callback = jest.fn() - const TestComponent = () => { - const [values, setValues] = useState({}) - useEffect(() => { - setTimeout(() => { - act(() => { - setValues({ - aaa: 123 - }) - }) - }) - }, []) - return ( - { - $('onFieldChange', 'aaa').subscribe(callback) - }} - > - - - - ) - } - - render() - await sleep(33) - expect(callback).toHaveBeenCalledTimes(2) - expect(callback.mock.calls[0][0].value).toBe(undefined) - expect(callback.mock.calls[1][0].value).toBe(123) -}) - -test('setFieldState x-props with onFormInit', async () => { - const TestComponent = () => { - return ( - { - $('onFormInit').subscribe(() => { - setFieldState('aaa', state => { - state.props['x-props'].disabled = true - }) - }) - }} - > - - - - ) - } - - const { queryByText } = render() - await sleep(33) - expect(queryByText('Disabled')).toBeVisible() -}) - -test('getFieldState with onFieldChange', async () => { - let aaValue - const TestComponent = () => { - return ( - console.log(values)} - initialValues={{ obj: { aa: 123 } }} - effects={($, { getFieldState }) => { - $('onFieldChange', 'obj.aa').subscribe(() => { - aaValue = getFieldState('obj', state => state.value.aa) - }) - }} - > - - - - - ) - } - const { queryByTestId } = render() - await sleep(33) - fireEvent.change(queryByTestId('this is aa'), { target: { value: '333' } }) - await sleep(33) - expect(aaValue).toBe('333') -}) - -test('set errors in effects', async () => { - const callback = jest.fn() - const TestComponent = () => { - return ( - { - $('onFormInit').subscribe(() => { - setFieldState('aaa', state => { - state.errors = ['validate failed'] - }) - }) - }} - onSubmit={callback} - > - - - - ) - } - - const { queryByTestId } = render() - await sleep(33) - fireEvent.click(queryByTestId('btn')) - await sleep(33) - expect(callback).toHaveBeenCalledTimes(0) -}) - -test('setFieldState from buffer', async () => { - const callback = jest.fn() - const TestComponent = () => { - return ( - { - $('onFormInit').subscribe(() => { - setFieldState(FormPath.match('*'), state => { - state.title = '1123' - }) - }) - $('onFieldChange', 'kkk').subscribe(() => { - setFieldState(FormPath.match('dd.*'), state => { - state.visible = false - }) - }) - }} - onSubmit={callback} - > - - - - - - ) - } - - const { queryByTestId } = render() - await sleep(33) - expect(queryByTestId('test')).toBeVisible() -}) - -test('filter first onFieldChange', async () => { - const sub1 = jest.fn() - const sub2 = jest.fn() - const TestComponent = () => { - return ( - { - $('onFieldChange', 'aaa') - .pipe(filter(state => !state.pristine)) - .subscribe(sub1) - $('onFieldChange', 'aaa').subscribe(sub2) - }} - > - - - ) - } - - render() - await sleep(33) - expect(sub1).toHaveBeenCalledTimes(0) - expect(sub2).toHaveBeenCalledTimes(1) -}) diff --git a/packages/react-schema-renderer/src/__old_tests__/mutators.spec.js b/packages/react-schema-renderer/src/__old_tests__/mutators.spec.js deleted file mode 100644 index 511f62099e8..00000000000 --- a/packages/react-schema-renderer/src/__old_tests__/mutators.spec.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react' -import SchemaForm, { - Field, - registerFormField, - connect, - createFormActions -} from '../index' -import { render, fireEvent } from '@testing-library/react' - -registerFormField( - 'mutator', - connect()(props => ( -
    - - {props.value && props.value[0] && props.value[0].aaa} -
    - )) -) - -test('update value by ref', async () => { - const actions = createFormActions() - const TestComponent = () => ( - - - - ) - - const { queryByText } = render() - await sleep(33) - expect(queryByText('123')).toBeVisible() - fireEvent.click(queryByText('Change Value')) - await sleep(33) - expect(queryByText('321')).toBeVisible() -}) diff --git a/packages/react-schema-renderer/src/__old_tests__/schema_form.spec.js b/packages/react-schema-renderer/src/__old_tests__/schema_form.spec.js deleted file mode 100644 index 76463884e7c..00000000000 --- a/packages/react-schema-renderer/src/__old_tests__/schema_form.spec.js +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react' -import { render, fireEvent } from '@testing-library/react' - -import SchemaForm, { Field, registerFormField, connect } from '../index' - -registerFormField( - 'string', - connect()(props => ( - - )) -) - -test('Increase lastValidateValue value processing during initialization', async () => { - const inpueFieldValidate = jest.fn() - const requriedFieldValidate = jest.fn() - - const TestComponent = () => ( - - - new Promise(resolve => { - inpueFieldValidate() - - resolve() - }) - } - /> - - new Promise(resolve => { - requriedFieldValidate() - - resolve() - }) - } - /> - - ) - - const { getByTestId } = render() - fireEvent.change(getByTestId('inpueField'), { target: { value: 1111 } }) - await sleep(1000) - expect(inpueFieldValidate).toHaveBeenCalledTimes(1) - expect(requriedFieldValidate).toHaveBeenCalledTimes(0) - - fireEvent.change(getByTestId('requriedField'), { target: { value: 2222 } }) - await sleep(1000) - expect(inpueFieldValidate).toHaveBeenCalledTimes(1) - expect(requriedFieldValidate).toHaveBeenCalledTimes(1) -}) diff --git a/packages/react-schema-renderer/src/__old_tests__/traverse.spec.js b/packages/react-schema-renderer/src/__old_tests__/traverse.spec.js deleted file mode 100644 index b6dbaa947cd..00000000000 --- a/packages/react-schema-renderer/src/__old_tests__/traverse.spec.js +++ /dev/null @@ -1,57 +0,0 @@ -import React, { useState } from 'react' -import { render, act } from '@testing-library/react' -import SchemaForm, { - Field, - registerFormField, - connect, - createFormActions, - registerFormFieldPropsTransformer -} from '../index' - -registerFormField( - 'test-string', - connect()(props => ( - -
    {props.value}
    -
    {typeof props.value}
    -
    {props.extra}
    -
    - )) -) - -registerFormFieldPropsTransformer('test-string', schema => { - schema['x-props'] = { - extra: 'this is extra props' - } - return schema -}) - -registerFormFieldPropsTransformer('test-string', schema => { - schema['x-props'] = { - extra: 'this is extra props 2' - } - return schema -}) - -test('props traverse', async () => { - const actions = createFormActions() - - const Component = () => { - return ( - - - - ) - } - - const { getByTestId, baseElement } = render() - await sleep(33) - expect(getByTestId('extra-props').textContent).toEqual( - 'this is extra props 2' - ) - actions.setFieldState('foo', state => { - state.props['x-props'].extra = 'modify props' - }) - await sleep(33) - expect(getByTestId('extra-props').textContent).toEqual('modify props') -}) diff --git a/packages/react-schema-renderer/src/__old_tests__/utils.spec.js b/packages/react-schema-renderer/src/__old_tests__/utils.spec.js deleted file mode 100644 index 1bc38e2c250..00000000000 --- a/packages/react-schema-renderer/src/__old_tests__/utils.spec.js +++ /dev/null @@ -1,13 +0,0 @@ -import { isNum, filterSchemaPropertiesAndReactChildren } from '../utils' - -test('isNum', () => { - expect(isNum('123')).toBe(false) - expect(isNum(123)).toBe(true) -}) - -test('filterSchemaPropertiesAndReactChildren', () => { - expect(filterSchemaPropertiesAndReactChildren(null, 'properties')).toBe(false) - expect(filterSchemaPropertiesAndReactChildren(null, 'items')).toBe(false) - expect(filterSchemaPropertiesAndReactChildren(null, 'children')).toBe(false) - expect(filterSchemaPropertiesAndReactChildren(null, 'yes')).toBe(true) -}) diff --git a/packages/react-schema-renderer/src/__old_tests__/validate_relations.spec.js b/packages/react-schema-renderer/src/__old_tests__/validate_relations.spec.js deleted file mode 100644 index 65a84178897..00000000000 --- a/packages/react-schema-renderer/src/__old_tests__/validate_relations.spec.js +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react' -import SchemaForm, { - Field, - registerFormField, - connect, - registerFieldMiddleware, - createFormActions -} from '../index' -import { render, fireEvent } from '@testing-library/react' - -registerFieldMiddleware(Field => { - return props => { - const index = props.schema['x-props'] && props.schema['x-props'].index - return ( -
    - -
    {props.errors}
    -
    - ) - } -}) -registerFormField( - 'string', - connect()(props => ( - - )) -) - -test('setFieldState will trigger validate', async () => { - const handleSubmit = jest.fn() - const handleValidateFailed = jest.fn() - const actions = createFormActions() - const TestComponent = () => ( - { - $('onFieldChange', 'text-1').subscribe(({ value }) => { - if (value) { - actions.setFieldState('text-2', state => { - state.value = value - }) - } - }) - }} - onSubmit={handleSubmit} - onValidateFailed={handleValidateFailed} - > - - - - - ) - - const { getByTestId } = render() - - fireEvent.click(getByTestId('btn')) - await sleep(33) - expect(handleSubmit).toHaveBeenCalledTimes(0) - expect(handleValidateFailed).toHaveBeenCalledTimes(1) - expect(getByTestId('test-errors-2')).toHaveTextContent('text-2 is required') - fireEvent.change(getByTestId('test-input-1'), { target: { value: '123' } }) - await sleep(33) - expect(getByTestId('test-input-2')).toHaveAttribute('value', '123') - expect(getByTestId('test-errors-2')).not.toHaveTextContent( - 'text-2 is required' - ) -}) diff --git a/packages/react-schema-renderer/src/__tests__/field.spec.tsx b/packages/react-schema-renderer/src/__tests__/field.spec.tsx deleted file mode 100644 index cfbcecec706..00000000000 --- a/packages/react-schema-renderer/src/__tests__/field.spec.tsx +++ /dev/null @@ -1,21 +0,0 @@ -describe('test all apis',()=>{ - //todo - test('basic',()=>{ - //todo - }) -}) - -describe('major scenes',()=>{ - //todo - test('basic',()=>{ - //todo - }) -}) - - -describe('bugfix',()=>{ - //todo - test('basic',()=>{ - //todo - }) -}) \ No newline at end of file diff --git a/packages/react-schema-renderer/src/__tests__/form.spec.tsx b/packages/react-schema-renderer/src/__tests__/form.spec.tsx deleted file mode 100644 index cfbcecec706..00000000000 --- a/packages/react-schema-renderer/src/__tests__/form.spec.tsx +++ /dev/null @@ -1,21 +0,0 @@ -describe('test all apis',()=>{ - //todo - test('basic',()=>{ - //todo - }) -}) - -describe('major scenes',()=>{ - //todo - test('basic',()=>{ - //todo - }) -}) - - -describe('bugfix',()=>{ - //todo - test('basic',()=>{ - //todo - }) -}) \ No newline at end of file diff --git a/packages/react-schema-renderer/src/__tests__/json-schema.spec.tsx b/packages/react-schema-renderer/src/__tests__/json-schema.spec.tsx index cfbcecec706..2b9fc86e722 100644 --- a/packages/react-schema-renderer/src/__tests__/json-schema.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/json-schema.spec.tsx @@ -1,21 +1,201 @@ -describe('test all apis',()=>{ +import React, { Fragment } from 'react' +import { + registerFormField, + connect, + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field +} from '../index' +import { render, fireEvent, wait } from '@testing-library/react' + +registerFormField( + 'string', + connect()(props => ( + + )) +) + +describe('test all apis', () => { //todo - test('basic',()=>{ + test('title', () => { + //todo + }) + + test('type', () => { + //todo + }) + + test('description', () => { + //todo + }) + + test('default', () => { + //todo + }) + + test('enum', () => { + //todo + }) + + test('readyOnly', () => { + //todo + }) + + test('writeOnly', () => { + //todo + }) + + test('const', () => { + //todo + }) + + test('multipleOf', () => { + //todo + }) + + test('maximum', () => { + //todo + }) + + test('exclusiveMaximum', () => { + //todo + }) + + test('minimum', () => { + //todo + }) + + test('exclusiveMinimum', () => { + //todo + }) + + test('maxLength', () => { + //todo + }) + + test('minLength', () => { + //todo + }) + + test('pattern', () => { + //todo + }) + + test('maxItems', () => { + //todo + }) + + test('minItems', () => { + //todo + }) + test('uniqueItems', () => { + //todo + }) + + test('maxProperties', () => { + //todo + }) + + test('minProperties', () => { + //todo + }) + + test('required', () => { + //todo + }) + + test('format', () => { + //todo + }) + + test('properties', () => { + //todo + }) + + test('items', () => { + //todo + }) + + test('additionalItems', () => { + //todo + }) + + test('patternProperties', () => { //todo }) + + test('additionalProperties', () => {}) + + test('editable', () => {}) + + test('x-props', () => {}) + + test('x-component-props', () => {}) + + describe('x-rules', () => { + test('Increase lastValidateValue value processing during initialization', async () => { + const inpueFieldValidate = jest.fn() + const requriedFieldValidate = jest.fn() + + const TestComponent = () => ( + + + { + inpueFieldValidate() + return '' + }} + /> + { + requriedFieldValidate() + return '' + }} + /> + + + ) + + const { getByTestId } = render() + fireEvent.change(getByTestId('inpueField'), { target: { value: 1111 } }) + await wait() + expect(inpueFieldValidate).toHaveBeenCalledTimes(1) + expect(requriedFieldValidate).toHaveBeenCalledTimes(0) + fireEvent.change(getByTestId('requriedField'), { + target: { value: 2222 } + }) + await wait() + expect(inpueFieldValidate).toHaveBeenCalledTimes(1) + expect(requriedFieldValidate).toHaveBeenCalledTimes(1) + }) + }) + + test('x-index', () => {}) + + test('x-render', () => {}) + + test('x-effect', () => {}) }) -describe('major scenes',()=>{ +describe('major scenes', () => { //todo - test('basic',()=>{ + test('basic', () => { //todo }) }) - -describe('bugfix',()=>{ +describe('bugfix', () => { //todo - test('basic',()=>{ + test('basic', () => { //todo }) -}) \ No newline at end of file +}) diff --git a/packages/react-schema-renderer/src/__tests__/validate_relations.spec.tsx b/packages/react-schema-renderer/src/__tests__/validate.spec.tsx similarity index 100% rename from packages/react-schema-renderer/src/__tests__/validate_relations.spec.tsx rename to packages/react-schema-renderer/src/__tests__/validate.spec.tsx diff --git a/packages/react-schema-renderer/src/__tests__/x-rules.spec.tsx b/packages/react-schema-renderer/src/__tests__/x-rules.spec.tsx deleted file mode 100644 index 6b0d7a3e5f2..00000000000 --- a/packages/react-schema-renderer/src/__tests__/x-rules.spec.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { Fragment } from 'react' -import { - registerFormField, - connect, - SchemaMarkupForm as SchemaForm, - SchemaMarkupField as Field -} from '../index' -import { render, fireEvent, wait } from '@testing-library/react' - -registerFormField( - 'string', - connect()(props => ( - - )) -) - -test('Increase lastValidateValue value processing during initialization', async () => { - const inpueFieldValidate = jest.fn() - const requriedFieldValidate = jest.fn() - - const TestComponent = () => ( - - - { - inpueFieldValidate() - return '' - }} - /> - { - requriedFieldValidate() - return '' - }} - /> - - - ) - - const { getByTestId } = render() - fireEvent.change(getByTestId('inpueField'), { target: { value: 1111 } }) - await wait() - expect(inpueFieldValidate).toHaveBeenCalledTimes(1) - expect(requriedFieldValidate).toHaveBeenCalledTimes(0) - - // fireEvent.change(getByTestId('requriedField'), { target: { value: 2222 } }) - // await wait(); - // expect(inpueFieldValidate).toHaveBeenCalledTimes(1) - // expect(requriedFieldValidate).toHaveBeenCalledTimes(1) -}) diff --git a/packages/react/src/hooks/useForm.ts b/packages/react/src/hooks/useForm.ts index 0c9915f2f1a..19a75132182 100644 --- a/packages/react/src/hooks/useForm.ts +++ b/packages/react/src/hooks/useForm.ts @@ -6,8 +6,7 @@ import { FormLifeCycle, IForm, IModel, - isStateModel, - IFormState + isStateModel } from '@uform/core' import { useDirty } from './useDirty' import { useEva } from 'react-eva' @@ -82,15 +81,6 @@ export const useForm = < dispatch.lazy(type, () => { return isStateModel(payload) ? payload.getState() : payload }) - if (type === LifeCycleTypes.ON_FORM_VALUES_CHANGE) { - if (optionsRef.current.onChange) { - optionsRef.current.onChange( - isStateModel(payload) - ? payload.getState((state: IFormState) => state.values) - : {} - ) - } - } if (broadcast) { broadcast.notify({ type, payload }) } @@ -110,7 +100,10 @@ export const useForm = < } ) ] - const optionsRef = useRef({ ...props }) + const optionsRef = useRef({ + ...props, + initialValues: props.initialValues || props.defaultValue + }) Object.assign(optionsRef.current, props) optionsRef.current.values = props.value optionsRef.current.lifecycles = lifecycles diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 03e35bf4865..2bb219312ba 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -40,6 +40,7 @@ export interface IFormProps< EffectActions = {} > { value?: Value + defaultValue?: DefaultValue initialValues?: DefaultValue actions?: EffectActions effects?: IFormEffect @@ -141,7 +142,7 @@ export interface IFormActions { ): any getFormGraph(): IFormGraph setFormGraph(graph: IFormGraph): void - subscribe(callback?: FormHeartSubscriber): void + subscribe(callback?: FormHeartSubscriber): number unsubscribe(id: number): void notify: (type: string, payload: T) => void dispatch: (type: string, payload: T) => void @@ -170,7 +171,7 @@ export interface IFormAsyncActions { ): Promise getFormGraph(): Promise setFormGraph(graph: IFormGraph): Promise - subscribe(callback?: FormHeartSubscriber): Promise + subscribe(callback?: FormHeartSubscriber): Promise unsubscribe(id: number): Promise notify: (type: string, payload: T) => Promise dispatch: (type: string, payload: T) => void From cf3787b177099671cb5879d330b6ff39b4e30812 Mon Sep 17 00:00:00 2001 From: Janry Date: Thu, 7 Nov 2019 11:52:59 +0800 Subject: [PATCH 26/99] docs(@uform/core): add docs (#376) --- docs/Examples/next/Sample.md | 18 +++++++++++ packages/core/README.md | 61 +++++++++++++++++++----------------- packages/core/src/index.ts | 16 ---------- packages/react/src/shared.ts | 4 ++- 4 files changed, 53 insertions(+), 46 deletions(-) diff --git a/docs/Examples/next/Sample.md b/docs/Examples/next/Sample.md index ce53f8a746b..6cbcb12cc1e 100644 --- a/docs/Examples/next/Sample.md +++ b/docs/Examples/next/Sample.md @@ -14,6 +14,7 @@ import { FormButtonGroup, Submit, Reset, + filterChanged, createFormActions } from '@uform/next' import { Button } from '@alifd/next' @@ -35,6 +36,14 @@ ReactDOM.render( state.required = true }) }) + + $('onFormChange') + .pipe(filterChanged('values.hello')) + .subscribe(state => { + setFieldState('radio', state => { + state.value = '4' + }) + }) }} > 改变radio的值 +
    , diff --git a/packages/core/README.md b/packages/core/README.md index 2f71df748bb..7d8703f2c4a 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,30 +1,33 @@ # @uform/core -> UForm 内核包 - -## quick start - -```jsx -import { createForm, LifeCycleTypes, FormLifeCycle, FormPath } from './src' - -const form = createForm() -// form.registerField({ path: 'a', rules: ['number'] }) // string -// form.registerField({ path: 'b', rules: [() => ({ type: 'warning', message: 'warning msg' })] }) // CustomValidator -// form.registerField({ path: 'c', rules: [() => ({ type: 'error', message: 'warning msg' })] }) // CustomValidator -// form.registerField({ path: 'd', rules: [() => 'straight error msg'] }) // CustomValidator -// form.registerField({ path: 'e', rules: [{ required: true, message: 'desc msg' }] }) // ValidateDescription - -form.registerField({ path: 'a', rules: [(value) => { - console.log('==>valuevalue', value); - return value === undefined ? { type: 'error', message: 'a is required' } : null -}] }) -form.registerField({ path: 'b', rules: [(value) => { - return value === undefined ? { type: 'warning', message: 'b is required' } : null -}] }) -// form.setFieldValue('a', 1) -const result = form.validate(); -result.then(({ warnings, errors }) => { - console.log('warnings', warnings); - console.log('errors', errors); -}); - -``` \ No newline at end of file +### 设计理念 + +**Anything comes from Observable Grpah.** + +### 核心亮点 + +- 时间旅行,借助首创Observable Graph,可以记录任意时刻的全量状态,也可以将状态回滚至任意时刻,这样的能力在,重事务型应用与本地调试上可以发挥出最大价值 +- 高效更新,精确渲染,无需整树渲染 +- 内置immer.js,智能降级,无需关心浏览器兼容性 +- 更加完备的生命周期钩子 +- 更加完备的校验引擎 + - validateFirst 校验 + - warning 校验(不阻塞提交校验) + - 校验消息模板引擎(不影响国际化文案存储的复杂校验文案消息解决方案) + - 校验规则可扩展,正则校验库可扩展 +- 更加灵活的路径解析,匹配,求值,取值引擎 + - 批量匹配数据路径能力 + - 解构求值,解构取值能力 +- 提供了基础表单状态模型之外的状态管理能力 + +### 架构图 + +![](https://img.alicdn.com/tfs/TB18LXHlVP7gK0jSZFjXXc5aXXa-1428-926.png) + +### API + +### Interfaces + +### Examples + + + diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1fe4a94aea9..a2cc12975cd 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -9,7 +9,6 @@ import { FormPath, FormPathPattern, each, - deprecate, isObj } from '@uform/shared' import { @@ -951,24 +950,9 @@ export function createForm( setFormGraph, setFieldValue, unsafe_do_not_use_transform_data_path: transformDataPath, //eslint-disable-line - setValue: deprecate( - setFieldValue, - 'setValue', - 'Please use the setFieldValue.' - ), getFieldValue, - getValue: deprecate( - getFieldValue, - 'getValue', - 'Please use the getFieldValue.' - ), setFieldInitialValue, getFieldInitialValue, - getInitialValue: deprecate( - getFieldInitialValue, - 'getInitialValue', - 'Please use the getFieldInitialValue.' - ), subscribe: (callback?: FormHeartSubscriber) => { return heart.subscribe(callback) }, diff --git a/packages/react/src/shared.ts b/packages/react/src/shared.ts index 45e95b14ab4..04f6f3be80a 100644 --- a/packages/react/src/shared.ts +++ b/packages/react/src/shared.ts @@ -47,7 +47,9 @@ export const filterChanged = (key?: string) => { return filter(x => { if (!x) return true const old = caches[x.name] || {} - const result = key ? isEqual(x[key], old[key]) : isEqual(x, old) + const result = key + ? isEqual(FormPath.getIn(x, key), FormPath.getIn(old, key)) + : isEqual(x, old) caches[x.name] = x return !result }) From 4377180973affe0fa18457e8f1db72b3e7df4169 Mon Sep 17 00:00:00 2001 From: quirkyvar Date: Thu, 7 Nov 2019 18:17:00 +0800 Subject: [PATCH 27/99] feat: add silent option (#377) --- packages/core/src/__tests__/index.spec.ts | 43 +++++++++++++++++++++++ packages/core/src/index.ts | 17 ++++----- packages/core/src/types.ts | 5 +-- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/packages/core/src/__tests__/index.spec.ts b/packages/core/src/__tests__/index.spec.ts index c3d44c6e409..cf7e6682a5d 100644 --- a/packages/core/src/__tests__/index.spec.ts +++ b/packages/core/src/__tests__/index.spec.ts @@ -579,6 +579,31 @@ describe('setFormState', () => { props: { hello: 'world' } }) }) + + test('set with slient', async () => { + const fieldChange = jest.fn() + const formChange = jest.fn() + const form = createForm({ + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FIELD_CHANGE, fieldChange), + new FormLifeCycle(LifeCycleTypes.ON_FORM_CHANGE, formChange) + ] + }) + + form.registerField({ path: 'a' }) + + form.setFormState(state => { + state.values = { a: '1234' } + }) + expect(form.getFormState((state) => state.values)).toEqual({ a: '1234' }) + expect(fieldChange).toBeCalledTimes(2) + expect(formChange).toBeCalledTimes(2) + + form.setFormState((state) => state.values = { a: '5678' }, true) + expect(form.getFormState((state) => state.values)).toEqual({ a: '5678' }) + expect(formChange).toBeCalledTimes(2) + expect(fieldChange).toBeCalledTimes(2) + }) }) describe('getFormState', () => { @@ -621,6 +646,24 @@ describe('setFieldState', () => { expect(form.getFieldState('a')).toEqual(state) }) + test('set with slient', async () => { + const fieldChange = jest.fn() + const form = createForm({ + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FIELD_CHANGE, fieldChange), + ] + }) + form.registerField({ path: 'a' }) + form.getFieldState('a') + form.setFieldState('a', state => (state.value = '1234')) + expect(form.getFieldState('a', state => state.value)).toEqual('1234') + expect(fieldChange).toBeCalledTimes(2) + + form.setFieldState('a', state => (state.value = '5678'), true) + expect(form.getFieldState('a', state => state.value)).toEqual('5678') + expect(fieldChange).toBeCalledTimes(2) + }) + test('validating and loading', () => { const form = createForm() form.registerField({ diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a2cc12975cd..5f1c4d240d4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -769,9 +769,9 @@ export function createForm( }) } - function setFormState(callback?: (state: IFormState) => any) { + function setFormState(callback?: (state: IFormState) => any, silent?: boolean) { leadingUpdate(() => { - state.setState(callback) + state.setState(callback, silent) }) } @@ -800,13 +800,14 @@ export function createForm( function setFieldState( path: FormPathPattern, - callback?: (state: IFieldState) => void + callback?: (state: IFieldState) => void, + silent?: boolean ) { if (!isFn(callback)) return let matchCount = 0 let pattern = FormPath.getPath(path) graph.select(pattern, field => { - field.setState(callback) + field.setState(callback, silent) matchCount++ }) if (matchCount === 0 || pattern.isWildMatchPattern) { @@ -828,10 +829,10 @@ export function createForm( } } - function setFieldValue(path: FormPathPattern, value?: any) { + function setFieldValue(path: FormPathPattern, value?: any, silent?: boolean) { setFieldState(path, state => { state.value = value - }) + }, silent) } function getFieldValue(path?: FormPathPattern) { @@ -840,10 +841,10 @@ export function createForm( }) } - function setFieldInitialValue(path?: FormPathPattern, value?: any) { + function setFieldInitialValue(path?: FormPathPattern, value?: any, silent?: boolean) { setFieldState(path, state => { state.initialValue = value - }) + }, silent) } function getFieldInitialValue(path?: FormPathPattern) { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index e5d736d7a35..ad63a68e671 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -298,11 +298,12 @@ export interface IForm { clearErrors: (pattern?: FormPathPattern) => void reset(options?: IFormResetOptions): Promise validate(path?: FormPathPattern, options?: {}): Promise - setFormState(callback?: (state: IFormState) => any): void + setFormState(callback?: (state: IFormState) => any, silent?: boolean): void getFormState(callback?: (state: IFormState) => any): any setFieldState( path: FormPathPattern, - callback?: (state: IFieldState) => void + callback?: (state: IFieldState) => void, + silent?: boolean ): void getFieldState( path: FormPathPattern, From 3ec4f098c6e4a193c58889ad1ffd2f67c3ac1a8e Mon Sep 17 00:00:00 2001 From: "jinc.cjc" Date: Thu, 7 Nov 2019 19:36:55 +0800 Subject: [PATCH 28/99] feat: add actions test --- packages/react/src/__tests__/actions.spec.tsx | 156 +++++++++++++++++- packages/react/src/types.ts | 2 +- 2 files changed, 152 insertions(+), 6 deletions(-) diff --git a/packages/react/src/__tests__/actions.spec.tsx b/packages/react/src/__tests__/actions.spec.tsx index cfbcecec706..fd55384e139 100644 --- a/packages/react/src/__tests__/actions.spec.tsx +++ b/packages/react/src/__tests__/actions.spec.tsx @@ -1,8 +1,154 @@ -describe('test all apis',()=>{ - //todo - test('basic',()=>{ - //todo +import React from 'react' +import { + Form, + Field, + createFormActions, + // FormEffectHooks, + IFieldProps +} from '../index' +import { render } from '@testing-library/react' + +const Input: React.FC = props => ( + + {({ state, mutators }) => ( +
    + + {state.errors} + {state.warnings} +
    + )} +
    +) +const CreatTestValue = function ():string { + return Date.now().toString(32) +} +// const { onFieldValueChange$ } = FormEffectHooks + +// 'submit', +// 'reset', +// 'validate', +// 'setFormState', +// 'getFormState', +// 'setFieldState', +// 'getFieldState', +// 'getFormGraph', +// 'setFormGraph', +// 'subscribe', +// 'unsubscribe', +// 'notify', +// 'dispatch', +// 'setFieldValue', +// 'getFieldValue', +// 'setFieldInitialValue', +// 'getFieldInitialValue' +const actions = createFormActions() +const ExpectKey = "testKey" + +describe('test all apis', () => { + beforeAll(() => { + // 预处理操作 + render( +
    + +
    + ) + }) + test('field state', async () => { + + const ExpectValue = CreatTestValue() + // set state value + actions.setFieldState(ExpectKey, state => { + expect.assertions(1) + state.value = ExpectValue + }) + // get state value + actions.getFieldState(ExpectKey, state => { + expect.assertions(1) + expect(state.value).toEqual(ExpectValue) + }) }) + test('form state', async () => { + const ExpectValue = CreatTestValue() + // set state value + actions.setFormState(state => { + expect.assertions(1) + state.values[ExpectKey] = ExpectValue + }) + // get state value + actions.getFormState(state => { + expect.assertions(1) + expect(state.values[ExpectKey]).toEqual(ExpectValue) + }) + }) + test('form graph', async () => { + // set new graph + const ExpectValue = CreatTestValue() + actions.getFieldState(ExpectKey,state =>{ + expect.assertions(1) + actions.setFormGraph({ + [ExpectValue]: state + }) + expect(actions.getFormGraph()[ExpectValue]).toEqual(state) + }) + }) + // test('subscribe', async () => { + // // set new graph + // const ExpectType = CreatTestValue() + // const ExpectPayload = CreatTestValue() + // const ExpectID = actions.subscribe(({ + // type, + // payload + // })=>{ + // expect.assertions(1) + // expect(type).toBe(ExpectType) + // expect(payload).toBe(ExpectPayload) + // }) + // actions.notify(ExpectType,ExpectPayload) + // actions.unsubscribe(ExpectID) + // actions.notify(ExpectType,ExpectPayload) + // }) + + + // test('submit', async () => { + // const actions = createFormActions() + // const onSubmitHandler = jest.fn() + // const onValidateFailedHandler = jest.fn() + // render( + //
    { + // onFieldValueChange$('aaa').subscribe(fieldState => { + // $.setFieldState('aaa', state => { + // state.value = 'hello world' + // }) + // }) + // }} + // > + // + //
    + // ) + // try { + // await actions.submit() + // } catch (e) { + // expect(e).toEqual([{ path: 'aaa', messages: ['This field is required'] }]) + // } + // actions.setFieldState('aaa', state => { + // state.value = '123' + // }) + // await actions.submit() + // expect(onSubmitHandler).toBeCalledWith({ aaa: 'hello world' }) + + // }) }) describe('major scenes',()=>{ @@ -18,4 +164,4 @@ describe('bugfix',()=>{ test('basic',()=>{ //todo }) -}) \ No newline at end of file +}) diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 03e35bf4865..ff31532fd66 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -141,7 +141,7 @@ export interface IFormActions { ): any getFormGraph(): IFormGraph setFormGraph(graph: IFormGraph): void - subscribe(callback?: FormHeartSubscriber): void + subscribe(callback?: FormHeartSubscriber): number unsubscribe(id: number): void notify: (type: string, payload: T) => void dispatch: (type: string, payload: T) => void From a41315ad00d0ba04d157db63c406c75560f820b9 Mon Sep 17 00:00:00 2001 From: Janry Date: Thu, 7 Nov 2019 20:18:25 +0800 Subject: [PATCH 29/99] Revert "V1" (#379) --- packages/core/src/index.ts | 22 +-- packages/react/src/__tests__/actions.spec.tsx | 156 +----------------- 2 files changed, 16 insertions(+), 162 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3b68e6102cc..5f1c4d240d4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -150,17 +150,17 @@ export function createForm( } } -function onFieldChange({ field, path }: { field: IField; path: FormPath }) { - return (published: IFieldState) => { - const valueChanged = field.hasChanged('value') - const initialValueChanged = field.hasChanged('initialValue') - const visibleChanged = field.hasChanged('visible') - const displayChanged = field.hasChanged('display') - const unmountedChanged = field.hasChanged('unmounted') - const mountedChanged = field.hasChanged('mounted') - const initializedChanged = field.hasChanged('initialized') - const warningsChanged = field.hasChanged('warnings') - const errorsChanges = field.hasChanged('errors') + function onFieldChange({ field, path }) { + return (published: IFieldState) => { + const valueChanged = field.isDirty('value') + const initialValueChanged = field.isDirty('initialValue') + const visibleChanged = field.isDirty('visible') + const displayChanged = field.isDirty('display') + const unmountedChanged = field.isDirty('unmounted') + const mountedChanged = field.isDirty('mounted') + const initializedChanged = field.isDirty('initialized') + const warningsChanged = field.isDirty('warnings') + const errorsChanges = field.isDirty('errors') if (initializedChanged) { heart.publish(LifeCycleTypes.ON_FIELD_INIT, field) const isEmptyValue = !isValid(published.value) diff --git a/packages/react/src/__tests__/actions.spec.tsx b/packages/react/src/__tests__/actions.spec.tsx index fd55384e139..cfbcecec706 100644 --- a/packages/react/src/__tests__/actions.spec.tsx +++ b/packages/react/src/__tests__/actions.spec.tsx @@ -1,154 +1,8 @@ -import React from 'react' -import { - Form, - Field, - createFormActions, - // FormEffectHooks, - IFieldProps -} from '../index' -import { render } from '@testing-library/react' - -const Input: React.FC = props => ( - - {({ state, mutators }) => ( -
    - - {state.errors} - {state.warnings} -
    - )} -
    -) -const CreatTestValue = function ():string { - return Date.now().toString(32) -} -// const { onFieldValueChange$ } = FormEffectHooks - -// 'submit', -// 'reset', -// 'validate', -// 'setFormState', -// 'getFormState', -// 'setFieldState', -// 'getFieldState', -// 'getFormGraph', -// 'setFormGraph', -// 'subscribe', -// 'unsubscribe', -// 'notify', -// 'dispatch', -// 'setFieldValue', -// 'getFieldValue', -// 'setFieldInitialValue', -// 'getFieldInitialValue' -const actions = createFormActions() -const ExpectKey = "testKey" - -describe('test all apis', () => { - beforeAll(() => { - // 预处理操作 - render( -
    - -
    - ) - }) - test('field state', async () => { - - const ExpectValue = CreatTestValue() - // set state value - actions.setFieldState(ExpectKey, state => { - expect.assertions(1) - state.value = ExpectValue - }) - // get state value - actions.getFieldState(ExpectKey, state => { - expect.assertions(1) - expect(state.value).toEqual(ExpectValue) - }) - }) - test('form state', async () => { - const ExpectValue = CreatTestValue() - // set state value - actions.setFormState(state => { - expect.assertions(1) - state.values[ExpectKey] = ExpectValue - }) - // get state value - actions.getFormState(state => { - expect.assertions(1) - expect(state.values[ExpectKey]).toEqual(ExpectValue) - }) - }) - test('form graph', async () => { - // set new graph - const ExpectValue = CreatTestValue() - actions.getFieldState(ExpectKey,state =>{ - expect.assertions(1) - actions.setFormGraph({ - [ExpectValue]: state - }) - expect(actions.getFormGraph()[ExpectValue]).toEqual(state) - }) +describe('test all apis',()=>{ + //todo + test('basic',()=>{ + //todo }) - // test('subscribe', async () => { - // // set new graph - // const ExpectType = CreatTestValue() - // const ExpectPayload = CreatTestValue() - // const ExpectID = actions.subscribe(({ - // type, - // payload - // })=>{ - // expect.assertions(1) - // expect(type).toBe(ExpectType) - // expect(payload).toBe(ExpectPayload) - // }) - // actions.notify(ExpectType,ExpectPayload) - // actions.unsubscribe(ExpectID) - // actions.notify(ExpectType,ExpectPayload) - // }) - - - // test('submit', async () => { - // const actions = createFormActions() - // const onSubmitHandler = jest.fn() - // const onValidateFailedHandler = jest.fn() - // render( - //
    { - // onFieldValueChange$('aaa').subscribe(fieldState => { - // $.setFieldState('aaa', state => { - // state.value = 'hello world' - // }) - // }) - // }} - // > - // - //
    - // ) - // try { - // await actions.submit() - // } catch (e) { - // expect(e).toEqual([{ path: 'aaa', messages: ['This field is required'] }]) - // } - // actions.setFieldState('aaa', state => { - // state.value = '123' - // }) - // await actions.submit() - // expect(onSubmitHandler).toBeCalledWith({ aaa: 'hello world' }) - - // }) }) describe('major scenes',()=>{ @@ -164,4 +18,4 @@ describe('bugfix',()=>{ test('basic',()=>{ //todo }) -}) +}) \ No newline at end of file From 64c527c4074efeca2f520ee005f6a3ab5eb96836 Mon Sep 17 00:00:00 2001 From: Janry Date: Fri, 8 Nov 2019 13:52:51 +0800 Subject: [PATCH 30/99] Feat support watch (#382) --- docs/Examples/next/Sample.md | 14 ++-- packages/core/src/index.ts | 92 ++++++++++++++++++++++++--- packages/core/src/shared/lifecycle.ts | 20 +++++- packages/core/src/shared/model.ts | 15 +++-- packages/core/src/types.ts | 22 +++++-- packages/react/src/index.ts | 4 +- packages/react/src/shared.ts | 23 +------ packages/react/src/types.ts | 10 +++ 8 files changed, 151 insertions(+), 49 deletions(-) diff --git a/docs/Examples/next/Sample.md b/docs/Examples/next/Sample.md index 6cbcb12cc1e..bf3ae807cdf 100644 --- a/docs/Examples/next/Sample.md +++ b/docs/Examples/next/Sample.md @@ -23,6 +23,11 @@ import Printer from '@uform/printer' const actions = createFormActions() +const sleep = duration => + new Promise(resolve => { + setTimeout(resolve, duration) + }) + ReactDOM.render( { + effects={($, { setFieldState, watch }) => { $('onFormMount').subscribe(() => { setFieldState('radio', state => { state.required = true }) }) - $('onFormChange') - .pipe(filterChanged('values.hello')) - .subscribe(state => { + $('onFormChange').subscribe(async state => { + watch(state, 'values.hello', async () => { + await sleep(1000) setFieldState('radio', state => { state.value = '4' }) }) + }) }} > ( }) } - function setFormState(callback?: (state: IFormState) => any, silent?: boolean) { + function setFormState( + callback?: (state: IFormState) => any, + silent?: boolean + ) { leadingUpdate(() => { state.setState(callback, silent) }) @@ -830,9 +836,13 @@ export function createForm( } function setFieldValue(path: FormPathPattern, value?: any, silent?: boolean) { - setFieldState(path, state => { - state.value = value - }, silent) + setFieldState( + path, + state => { + state.value = value + }, + silent + ) } function getFieldValue(path?: FormPathPattern) { @@ -841,10 +851,18 @@ export function createForm( }) } - function setFieldInitialValue(path?: FormPathPattern, value?: any, silent?: boolean) { - setFieldState(path, state => { - state.initialValue = value - }, silent) + function setFieldInitialValue( + path?: FormPathPattern, + value?: any, + silent?: boolean + ) { + setFieldState( + path, + state => { + state.initialValue = value + }, + silent + ) } function getFieldInitialValue(path?: FormPathPattern) { @@ -930,6 +948,49 @@ export function createForm( ) } + //在subscribe中必须同步使用,否则会监听不到变化 + function watch( + target: any, + path: FormPathPattern | (() => T), + callback?: () => T + ): Promise { + if (isFn(path)) { + callback = path as any + path = '' + } + + if (!env.publishing) { + throw new Error( + 'The watch function must be used synchronously in the subscribe callback.' + ) + } + + const resolve = () => { + return new Promise(resolve => { + return setTimeout(() => { + if (isFn(callback)) { + resolve(Promise.resolve(callback())) + } + }) + }) + } + + if (isFormState(target)) { + if (state.hasChanged(path)) { + return resolve() + } + } else if (isFieldState(target) || isVirtualFieldState(target)) { + const node = graph.get(target.path) + if (node && node.hasChanged(path)) { + return resolve() + } + } else { + throw new Error( + 'Illegal parameter,You must pass the correct state object(FormState/FieldState/VirtualFieldState).' + ) + } + } + const state = new FormState(options) const validator = new FormValidator(options) const graph = new FormGraph({ @@ -938,6 +999,7 @@ export function createForm( const formApi = { submit, reset, + watch, clearErrors, validate, setFormState, @@ -964,12 +1026,22 @@ export function createForm( heart.publish(type, payload) } } - const heart = new FormHeart({ ...options, context: formApi }) + const heart = new FormHeart({ + ...options, + context: formApi, + beforeNotify: () => { + env.publishing = true + }, + afterNotify: () => { + env.publishing = false + } + }) const env = { validateTimer: null, graphChangeTimer: null, shadowStage: false, leadingStage: false, + publishing: false, taskQueue: [], taskIndexes: {}, removeNodes: {}, diff --git a/packages/core/src/shared/lifecycle.ts b/packages/core/src/shared/lifecycle.ts index 136fe6eb350..4bb8c16a864 100644 --- a/packages/core/src/shared/lifecycle.ts +++ b/packages/core/src/shared/lifecycle.ts @@ -47,16 +47,26 @@ export class FormHeart extends Subscribable { private context: Context + private beforeNotify?: (...args: any[]) => void + + private afterNotify?: (...args: any[]) => void + constructor({ lifecycles, - context + context, + beforeNotify, + afterNotify }: { lifecycles?: FormLifeCycle[] context?: Context - }) { + beforeNotify?: (...args: any[]) => void + afterNotify?: (...args: any[]) => void + } = {}) { super() this.lifecycles = this.buildLifeCycles(lifecycles || []) this.context = context + this.beforeNotify = beforeNotify + this.afterNotify = afterNotify } buildLifeCycles(lifecycles: FormLifeCycle[]) { @@ -77,6 +87,9 @@ export class FormHeart extends Subscribable { publish = (type: any, payload: P, context?: C) => { if (isStr(type)) { + if (isFn(this.beforeNotify)) { + this.beforeNotify(type, payload, context) + } this.lifecycles.forEach(lifecycle => { lifecycle.notify(type, payload, context || this.context) }) @@ -84,6 +97,9 @@ export class FormHeart extends Subscribable { type, payload }) + if (isFn(this.afterNotify)) { + this.afterNotify(type, payload, context) + } } } } diff --git a/packages/core/src/shared/model.ts b/packages/core/src/shared/model.ts index c2a7b6ff6f4..874a3e240aa 100644 --- a/packages/core/src/shared/model.ts +++ b/packages/core/src/shared/model.ts @@ -4,7 +4,9 @@ import { isFn, each, globalThisPolyfill, - Subscribable + Subscribable, + FormPath, + FormPathPattern } from '@uform/shared' import produce, { Draft } from 'immer' import { IStateModelFactory, StateDirtyMap, IModel, StateModel } from '../types' @@ -180,8 +182,6 @@ export const createStateModel = ( this.notify(this.getState()) this.dirtys = {} this.dirtyNum = 0 - //1. onFieldChange内的setFormValuesIn中不希望重置当前字段的dirtymap,如果不重置就会死循环 - //2. 自己监听自己,自己修改自己的状态,希望触发onFieldChange } } @@ -203,9 +203,12 @@ export const createStateModel = ( * *在一组操作过程中的变化情况 */ - hasChanged = (key?: string) => { - return key - ? !isEqual(this.prevState[key], this.state[key]) + hasChanged = (path?: FormPathPattern) => { + return path + ? !isEqual( + FormPath.getIn(this.prevState, path), + FormPath.getIn(this.state, path) + ) : !isEqual(this.prevState, this.state) } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index ad63a68e671..c05a082dd74 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -28,7 +28,7 @@ export enum LifeCycleTypes { ON_FORM_SUBMIT_START = 'onFormSubmitStart', ON_FORM_SUBMIT_END = 'onFormSubmitEnd', ON_FORM_VALUES_CHANGE = 'onFormValuesChange', - ON_FORM_INITIAL_VALUES_CHANGE = 'onFormInitialValueChange', + ON_FORM_INITIAL_VALUES_CHANGE = 'onFormInitialValuesChange', ON_FORM_VALIDATE_START = 'onFormValidateStart', ON_FORM_VALIDATE_END = 'onFormValidateEnd', ON_FORM_INPUT_CHANGE = 'onFormInputChange', @@ -157,13 +157,22 @@ export interface IFieldStateProps { } export const isField = (target: any): target is IField => - target && target.displayName === 'FieldState' + target && + target.displayName === 'FieldState' && + isFn(target.getState) && + isFn(target.setState) export const isFieldState = (target: any): target is IFieldState => - target && target.displayName === 'FieldState' + target && target.displayName === 'FieldState' && target.name && target.path + +export const isFormState = (target: any): target is IFormState => + target && target.displayName === 'FormState' && target.name && target.path export const isVirtualField = (target: any): target is IVirtualField => - target && target.displayName === 'VirtualFieldState' + target && + target.displayName === 'VirtualFieldState' && + isFn(target.getState) && + isFn(target.setState) export const isVirtualFieldState = ( target: any @@ -296,6 +305,11 @@ export interface IForm { onSubmit?: (values: IFormState['values']) => any | Promise ): Promise clearErrors: (pattern?: FormPathPattern) => void + watch( + target: any, + path: FormPathPattern | (() => T), + callback?: () => T + ): Promise reset(options?: IFormResetOptions): Promise validate(path?: FormPathPattern, options?: {}): Promise setFormState(callback?: (state: IFormState) => any, silent?: boolean): void diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 1afb239166a..7427cdcb193 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -2,8 +2,7 @@ import { FormEffectHooks, createEffectHook, createFormActions, - createAsyncFormActions, - filterChanged + createAsyncFormActions } from './shared' export * from '@uform/core' export * from './components/Form' @@ -18,7 +17,6 @@ export * from './hooks/useVirtualField' export * from './types' export { - filterChanged, FormEffectHooks, createEffectHook, createFormActions, diff --git a/packages/react/src/shared.ts b/packages/react/src/shared.ts index 04f6f3be80a..8d793dcbf72 100644 --- a/packages/react/src/shared.ts +++ b/packages/react/src/shared.ts @@ -1,10 +1,4 @@ -import { - isFn, - isEqual, - FormPath, - globalThisPolyfill, - Subscribable -} from '@uform/shared' +import { isFn, FormPath, globalThisPolyfill, Subscribable } from '@uform/shared' import { IFormEffect, IFormActions, IFormAsyncActions } from './types' import { Observable } from 'rxjs/internal/Observable' import { filter } from 'rxjs/internal/operators/filter' @@ -24,6 +18,7 @@ export const createFormActions = (): IFormActions => { return createActions( 'submit', 'reset', + 'watch', 'validate', 'setFormState', 'getFormState', @@ -42,23 +37,11 @@ export const createFormActions = (): IFormActions => { ) as IFormActions } -export const filterChanged = (key?: string) => { - const caches = {} - return filter(x => { - if (!x) return true - const old = caches[x.name] || {} - const result = key - ? isEqual(FormPath.getIn(x, key), FormPath.getIn(old, key)) - : isEqual(x, old) - caches[x.name] = x - return !result - }) -} - export const createAsyncFormActions = (): IFormAsyncActions => createAsyncActions( 'submit', 'reset', + 'watch', 'validate', 'setFormState', 'getFormState', diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 2bb219312ba..3238391941d 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -128,6 +128,11 @@ export interface IFormActions { onSubmit?: (values: IFormState['values']) => void | Promise ): Promise reset(options?: IFormResetOptions): void + watch( + target: any, + path: FormPathPattern | (() => T), + callback?: () => T + ): Promise validate(path?: FormPathPattern, options?: {}): Promise setFormState(callback?: (state: IFormState) => any): void getFormState(callback?: (state: IFormState) => any): any @@ -157,6 +162,11 @@ export interface IFormAsyncActions { onSubmit?: (values: IFormState['values']) => void | Promise ): Promise reset(options?: IFormResetOptions): Promise + watch( + target: any, + path: FormPathPattern | (() => T), + callback?: () => T + ): Promise clearErrors: (pattern?: FormPathPattern) => Promise validate(path?: FormPathPattern, options?: {}): Promise setFormState(callback?: (state: IFormState) => any): Promise From fe4ce56b228c87cfbf5948cdf7a4b26215d30ef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=B8=E6=B3=BD=E6=B8=85?= <835702580@qq.com> Date: Sat, 9 Nov 2019 20:53:01 +0800 Subject: [PATCH 31/99] test(@uform/react-schema-renderer): add old test case (#381) --- .../src/__tests__/dynamic.spec.tsx | 1 + .../src/__tests__/effects.spec.tsx | 49 +- .../src/__tests__/validate.spec.tsx | 433 +++++++++++++++++- .../src/__tests__/value.spec.tsx | 385 ++++++++++++++++ 4 files changed, 839 insertions(+), 29 deletions(-) create mode 100644 packages/react-schema-renderer/src/__tests__/value.spec.tsx diff --git a/packages/react-schema-renderer/src/__tests__/dynamic.spec.tsx b/packages/react-schema-renderer/src/__tests__/dynamic.spec.tsx index 7bd0012d894..92ad902e9e5 100644 --- a/packages/react-schema-renderer/src/__tests__/dynamic.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/dynamic.spec.tsx @@ -518,6 +518,7 @@ test('dynamic change functions onChange/onReset/onSubmit/onValidateFailed', asyn ) } const { queryByTestId, queryByText } = render() + await sleep(100) fireEvent.click(queryByTestId('radio-a2')) await wait() diff --git a/packages/react-schema-renderer/src/__tests__/effects.spec.tsx b/packages/react-schema-renderer/src/__tests__/effects.spec.tsx index 13d7f05f61b..494b3078cf9 100644 --- a/packages/react-schema-renderer/src/__tests__/effects.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/effects.spec.tsx @@ -6,8 +6,10 @@ import { SchemaMarkupForm as SchemaForm, SchemaMarkupField as Field, createFormActions, + // createVirtualBox, registerFieldMiddleware } from '../index' +// import { toArr } from '@uform/shared' import { render, wait, act, fireEvent } from '@testing-library/react' import { filter } from 'rxjs/operators' @@ -28,13 +30,9 @@ registerFieldMiddleware(Field => { }) registerFormField( 'string', - connect()(props => - props.disabled ? ( - Disabled - ) : ( - - ) - ) + connect()(props => props.disabled + ? Disabled + : ) ) test('onFormInit setFieldState', async () => { @@ -49,7 +47,6 @@ test('onFormInit setFieldState', async () => { state.rules = [ { required: true, - message: 'field is required' } ] }) @@ -67,20 +64,20 @@ test('onFormInit setFieldState', async () => { const { getByText, getByTestId, queryByText } = render() - await wait() + await wait(); expect(queryByText('text')).toBeVisible() - await wait() + await wait(); fireEvent.click(getByTestId('btn')) - await wait() + await wait(); expect(getByText('This field is required')).toBeVisible() - await wait() + await wait(); actions.setFieldState('aaa', state => { state.rules = [] }) - await wait() + await wait(); fireEvent.click(getByTestId('btn')) - await wait() - expect(queryByText('field is required')).toBeNull() + await wait(); + expect(queryByText('This field is required')).toBeNull() }) test('init triggers', async () => { @@ -103,7 +100,7 @@ test('init triggers', async () => { } render() - await wait() + await wait(); expect(callback).toHaveBeenCalledTimes(1) }) @@ -138,7 +135,7 @@ test('onFieldChange will trigger with initialValues', async () => { } render() - await wait() + await wait(); expect(callback).toHaveBeenCalledTimes(2) expect(callback.mock.calls[0][0].value).toBe(undefined) expect(callback.mock.calls[1][0].value).toBe(123) @@ -167,7 +164,7 @@ test('setFieldState x-props with onFormInit', async () => { } const { queryByText } = render() - await wait() + await wait(); expect(queryByText('Disabled')).toBeVisible() }) @@ -196,9 +193,9 @@ test('getFieldState with onFieldChange', async () => { ) } const { queryByTestId } = render() - await wait() + await wait(); fireEvent.change(queryByTestId('this is aa'), { target: { value: '333' } }) - await wait() + await wait(); expect(aaValue).toBe('333') }) @@ -227,9 +224,9 @@ test('set errors in effects', async () => { } const { queryByTestId } = render() - await wait() + await wait(); fireEvent.click(queryByTestId('btn')) - await wait() + await wait(); expect(callback).toHaveBeenCalledTimes(0) }) @@ -241,7 +238,7 @@ test('setFieldState from buffer', async () => { effects={($, { setFieldState }) => { $('onFormInit').subscribe(() => { setFieldState(FormPath.match('*') as any, state => { - ;(state as any).title = '1123' + (state as any).title = '1123' }) }) $('onFieldChange', 'kkk').subscribe(() => { @@ -268,7 +265,7 @@ test('setFieldState from buffer', async () => { } const { queryByTestId } = render() - await wait() + await wait(); expect(queryByTestId('test')).toBeVisible() }) @@ -291,7 +288,7 @@ test('filter first onFieldChange', async () => { } render() - await wait() + await wait(); expect(sub1).toHaveBeenCalledTimes(0) expect(sub2).toHaveBeenCalledTimes(1) -}) +}) \ No newline at end of file diff --git a/packages/react-schema-renderer/src/__tests__/validate.spec.tsx b/packages/react-schema-renderer/src/__tests__/validate.spec.tsx index bcabfebfc20..f1eb7e22053 100644 --- a/packages/react-schema-renderer/src/__tests__/validate.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/validate.spec.tsx @@ -1,13 +1,14 @@ -import React, { Fragment } from 'react' +import React, { Fragment, useState, useEffect } from 'react' import { registerFormField, connect, SchemaMarkupForm as SchemaForm, SchemaMarkupField as Field, createFormActions, - registerFieldMiddleware + registerFieldMiddleware, + FormPath } from '../index' -import { render, fireEvent, wait } from '@testing-library/react' +import { render, fireEvent, wait, act, waitForElement, waitForDomChange } from '@testing-library/react' registerFieldMiddleware(Field => { return props => { @@ -74,3 +75,429 @@ test('setFieldState will trigger validate', async () => { 'This field is required' ) }) + +test('basic validate', async () => { + const handleSubmit = jest.fn() + const handleValidateFailed = jest.fn() + const TestComponent = () => ( + + + + + + + ) + + const { getByTestId, getByText } = render() + + fireEvent.click(getByTestId('btn')) + await wait(); + fireEvent.click(getByTestId('btn')) + await wait() + expect(handleSubmit).toHaveBeenCalledTimes(0) + expect(handleValidateFailed).toHaveBeenCalledTimes(2) + expect(getByText('text is required')).toBeVisible() +}) + +test('validate in init', async () => { + const handleSubmit = jest.fn() + const handleValidateFailed = jest.fn() + const TestComponent = () => { + const [state, setState] = useState() + useEffect(() => { + act(() => { + setState({ + text: '' + }) + }) + }, []) + return ( + + + + + + + ) + } + let result + act(() => { + result = render() + }) + await wait() + const { queryByText } = result + expect(queryByText('text is required')).toBeNull() + fireEvent.click(queryByText('Submit')) + await wait() + expect(handleSubmit).toHaveBeenCalledTimes(0) + expect(handleValidateFailed).toHaveBeenCalledTimes(1) + expect(queryByText('text is required')).toBeVisible() +}) + +test('validate in editable false', async () => { + const handleSubmit = jest.fn() + const handleValidateFailed = jest.fn() + const actions = createFormActions() + const TestComponent = () => { + const [state, setState] = useState() + useEffect(() => { + act(() => { + setState({ + editable: '' + }) + }) + }, []) + return ( + + + + + + + ) + } + let result + act(() => { + result = render() + }) + await wait() + const { queryByText } = result + expect(queryByText('editable is required')).toBeNull() + fireEvent.click(queryByText('Submit')) + await wait() + expect(handleSubmit).toHaveBeenCalledTimes(0) + expect(handleValidateFailed).toHaveBeenCalledTimes(1) + expect(queryByText('editable is required')).toBeVisible() + actions.setFieldState('editable', state => { + state.value = '123' + }) + await wait() + fireEvent.click(queryByText('Submit')) + await wait() + expect(handleSubmit).toHaveBeenCalledTimes(1) + expect(handleValidateFailed).toHaveBeenCalledTimes(1) + expect(queryByText('editable is required')).toBeNull() +}) + +test('modify required rules by setFieldState', async () => { + const actions = createFormActions() + const handleSubmit = jest.fn() + const handleValidateFailed = jest.fn() + const TestComponent = () => { + return ( + + + + + + + ) + } + const { queryByText } = render() + await wait() + fireEvent.click(queryByText('Submit')) + await wait() + expect(handleSubmit).toBeCalledTimes(1) + expect(handleValidateFailed).toBeCalledTimes(0) + actions.setFieldState('kk', state => { + state.props.required = true + }) + await wait() + fireEvent.click(queryByText('Submit')) + await wait() + expect(queryByText('kk is required')).toBeVisible() + expect(handleSubmit).toBeCalledTimes(1) + expect(handleValidateFailed).toBeCalledTimes(1) + actions.setFieldState('kk', state => { + state.required = false + }) + await wait() + fireEvent.click(queryByText('Submit')) + await wait() + expect(queryByText('kk is required')).toBeNull() + expect(handleSubmit).toBeCalledTimes(2) + expect(handleValidateFailed).toBeCalledTimes(1) + actions.setFieldState('kk', state => { + state.required = true + }) + await wait() + fireEvent.click(queryByText('Submit')) + await wait() + expect(queryByText('kk is required')).toBeVisible() + expect(handleSubmit).toBeCalledTimes(2) + expect(handleValidateFailed).toBeCalledTimes(2) +}) + +test('modify validate rules by setFieldState', async () => { + const actions = createFormActions() + const TestComponent = () => { + return ( + + + + + + + ) + } + const { queryByText, queryByTestId } = render() + await wait() + fireEvent.click(queryByText('Submit')) + await wait() + expect(queryByText('required')).toBeVisible() + actions.setFieldState('bb', state => { + state.rules = [ + { required: true }, + { + pattern: /^\d{6}$/, + message: 'must have 6 numbers' + } + ] + }) + await wait() + fireEvent.change(queryByTestId('test-input'), { + target: { value: '123' } + }) + await wait() + expect(queryByText('must have 6 numbers')).toBeVisible() +}) + +test('dynamic update values', async () => { + const TestComponent = () => { + return ( + + + + + + + ) + } + const { queryByTestId, queryByText } = render() + await wait() + fireEvent.change(queryByTestId('test-input'), { + target: { value: '12332123' } + }) + await wait() + fireEvent.change(queryByTestId('test-input'), { + target: { value: '12332123a' } + }) + await wait() + fireEvent.change(queryByTestId('test-input'), { target: { value: '123321' } }) + await wait() + fireEvent.change(queryByTestId('test-input'), { target: { value: '12332' } }) + await wait() + expect(queryByText('must be number')).toBeNull() + fireEvent.change(queryByTestId('test-input'), { target: { value: '12332a' } }) + await wait() + expect(queryByText('must be number')).toBeVisible() +}) + +test('test idcard rules', async () => { + const TestComponent = () => { + return ( + + + + ) + } + const { queryByTestId, queryByText } = render() + // 14位数字 + const value1 = '12345678912345' + // 15位数字 + const value2 = '123456789123456' + // 17位数字 + const value3 = '12345678912345678' + // 17位数字+x + const value4 = '12345678912345678x' + // 17位数字+X + const value5 = '12345678912345678X' + // 18位数字 + const value6 = '123456789123456789' + + const element = await waitForElement(() => queryByTestId('test-input')) + waitForDomChange({ container: element }).then(mutationsList => { + const mutation = mutationsList[0] + const { value } = mutation.target + const errorTipsElement = queryByText('idCard is not an idcard format') + if (value === value1 || value === value3) { + expect(errorTipsElement).toBeVisible() + } else { + expect(errorTipsElement).toBeNull() + } + }) + fireEvent.change(element, { target: { value: value1 } }) + fireEvent.change(element, { target: { value: value2 } }) + fireEvent.change(element, { target: { value: value3 } }) + fireEvent.change(element, { target: { value: value4 } }) + fireEvent.change(element, { target: { value: value5 } }) + fireEvent.change(element, { target: { value: value6 } }) +}) + +test('dynamic switch visible', async () => { + const TestComponent = () => { + return ( + { + $('onFieldChange', 'aa').subscribe(({ value }) => { + setFieldState('bb', state => { + state.visible = value == 'aa' + }) + }) + }} + > + + + + + + ) + } + const { queryAllByTestId, queryByText } = render() + await wait() + fireEvent.change(queryAllByTestId('test-input')[0], { + target: { value: 'aa' } + }) + await wait() + fireEvent.change(queryAllByTestId('test-input')[0], { + target: { value: 'bb' } + }) + await wait() + fireEvent.change(queryAllByTestId('test-input')[0], { + target: { value: 'aa' } + }) + await wait() + expect(queryByText('bb is required')).toBeNull() +}) + +test('async validate prevent submit', async () => { + const onSubmitHandler = jest.fn() + const TestComponent = () => { + return ( + + + { + return new Promise(resolve => { + setTimeout(() => { + if (val === '123') { + resolve('can not input 123') + } else { + resolve() + } + }, 100) + }) + }} + /> + + + + ) + } + const { queryAllByTestId, queryByText } = render() + await wait() + fireEvent.change(queryAllByTestId('test-input')[0], { + target: { value: '444' } + }) + await wait() + fireEvent.click(queryByText('Submit')) + fireEvent.click(queryByText('Submit')) + fireEvent.click(queryByText('Submit')) + await wait() + expect(queryByText('can not input 123')).toBeNull() + expect(onSubmitHandler).toBeCalledTimes(1) + fireEvent.change(queryAllByTestId('test-input')[0], { + target: { value: '123' } + }) + await wait() + fireEvent.click(queryByText('Submit')) + await wait() + expect(queryByText('can not input 123')).toBeVisible() + expect(onSubmitHandler).toBeCalledTimes(1) +}) + +test('async validate side effect', async () => { + const actions = createFormActions() + const TestComponent = () => { + return ( + + + + + + + + ) + } + const { queryAllByTestId, queryByText } = render() + await wait() + fireEvent.change(queryAllByTestId('test-input')[0], { + target: { value: 'aaaaa' } + }) + fireEvent.change(queryAllByTestId('test-input')[1], { + target: { value: 'bbbbb' } + }) + await wait() + fireEvent.click(queryByText('Cancel')) + await wait() + fireEvent.change(queryAllByTestId('test-input')[0], { + target: { value: 'aaaaa' } + }) + await wait() + expect(queryByText('aa is required')).toBeNull() + expect(queryByText('bb is required')).toBeNull() + await wait() + fireEvent.change(queryAllByTestId('test-input')[0], { + target: { value: '' } + }) + await wait() + expect(queryByText('aa is required')).toBeVisible() + expect(queryByText('bb is required')).toBeNull() +}) \ No newline at end of file diff --git a/packages/react-schema-renderer/src/__tests__/value.spec.tsx b/packages/react-schema-renderer/src/__tests__/value.spec.tsx new file mode 100644 index 00000000000..47a8ea113a1 --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/value.spec.tsx @@ -0,0 +1,385 @@ +import React, { Fragment, useState } from 'react' +import { + registerFormField, + connect, + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, + createFormActions, + // FormSlot +} from '../index' +import { render, fireEvent, act, wait } from '@testing-library/react' + +registerFormField( + 'test-string', + connect()(props => ( + +
    {props.value}
    +
    {typeof props.value}
    +
    + )) +) + +registerFormField( + 'string', + connect()(props => ( + + )) +) + +registerFormField( + 'name-string', + connect()(props => ( + + )) +) + +test('default value', async () => { + const Component = () => ( + + + + ) + + const { getByTestId } = render() + await wait() + expect(getByTestId('type-value').textContent).toEqual('string') +}) + +test('initialValues', async () => { + const Component = () => ( + + + + ) + + const { getByTestId } = render() + await wait() + expect(getByTestId('type-value').textContent).toEqual('string') +}) + +test('controlled initialValues', async () => { + const actions = createFormActions() + let outerSetState + const Component = () => { + const [state, setState] = useState({ + foo: '123' + }) + outerSetState = values => + act(() => { + setState(values) + }) + return ( + + + + ) + } + + const { getByTestId } = render() + await wait() + expect(getByTestId('type-value').textContent).toEqual('string') + await actions.setFieldState('foo', state => { + state.value = '321' + }) + await actions.reset() + await wait() + expect(getByTestId('value').textContent).toEqual('123') + await actions.setFieldState('foo', state => { + state.value = '321' + }) + await wait() + act(() => { + outerSetState({ foo: '123' }) + }) + await wait() + expect(getByTestId('value').textContent).toEqual('123') + await wait() +}) + + +// test('controlled with hooks by initalValues', async () => { +// const onChangeHandler = jest.fn() +// const actions = createFormActions() +// const Component = () => { +// const [total, setTotal] = useState(0) +// return ( +//
    +// { +// $('onFieldChange', 'a3').subscribe(onChangeHandler) +// $('onFieldChange', 'a3').subscribe(state => { +// act(() => { +// setTotal(state.value) +// }) +// }) +// }} +// > +// +// +// +//
    Total is:{total}
    +//
    +//
    +//
    +//
    Total is:{total}
    +//
    +// ) +// } + +// const { queryByTestId } = render() +// expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') +// expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') +// fireEvent.change(queryByTestId('test-input'), { target: { value: '333' } }) +// await wait() +// expect(queryByTestId('outer-result').textContent).toEqual('Total is:333') +// expect(queryByTestId('inner-result').textContent).toEqual('Total is:333') +// expect(onChangeHandler).toHaveBeenCalledTimes(2) +// await actions.setFieldState('a3', state => { +// state.value = '456' +// }) +// await wait() +// expect(queryByTestId('test-input').getAttribute('value')).toEqual('456') +// expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') +// expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') +// expect(onChangeHandler).toHaveBeenCalledTimes(3) +// await actions.reset() +// await wait() +// expect(queryByTestId('test-input').getAttribute('value')).toEqual('123') +// expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') +// expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') +// expect(onChangeHandler).toHaveBeenCalledTimes(3) +// }) + +// test('controlled with hooks by static value', async () => { +// const onChangeHandler = jest.fn() +// const actions = createFormActions() +// const Component = () => { +// const [total, setTotal] = useState(0) + +// return ( +//
    +// { +// $('onFieldChange', 'a3').subscribe(onChangeHandler) +// $('onFieldChange', 'a3').subscribe(state => { +// act(() => { +// setTotal(state.value) +// }) +// }) +// }} +// > +// +// +// +//
    Total is:{total}
    +//
    +//
    +//
    +//
    Total is:{total}
    +//
    +// ) +// } + +// const { queryByTestId } = render() +// expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') +// expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') +// fireEvent.change(queryByTestId('test-input'), { target: { value: '333' } }) +// await wait() +// expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') +// expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') +// expect(onChangeHandler).toHaveBeenCalledTimes(3) +// actions.reset() +// await wait() +// expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') +// expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') +// expect(onChangeHandler).toHaveBeenCalledTimes(3) +// await actions.setFieldState('a3', state => { +// state.value = '456' +// }) +// await wait() +// expect(queryByTestId('test-input').getAttribute('value')).toEqual('123') +// expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') +// expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') +// expect(onChangeHandler).toHaveBeenCalledTimes(5) +// await actions.reset() +// await wait() +// expect(queryByTestId('test-input').getAttribute('value')).toEqual('') +// expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') +// expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') +// expect(onChangeHandler).toHaveBeenCalledTimes(5) +// }) + +// test('controlled with hooks by dynamic value', async () => { +// const onChangeHandler = jest.fn() +// const actions = createFormActions() +// const Component = () => { +// const [total, setTotal] = useState('123') + +// return ( +//
    +// { +// $('onFieldChange', 'a3').subscribe(onChangeHandler) +// $('onFieldChange', 'a3').subscribe(state => { +// act(() => { +// setTotal(state.value) +// }) +// }) +// }} +// > +// +// +// +//
    Total is:{total}
    +//
    +//
    +//
    +//
    Total is:{total}
    +//
    +// ) +// } + +// const { queryByTestId } = render() +// expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') +// expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') +// fireEvent.change(queryByTestId('test-input'), { target: { value: '333' } }) +// await wait() +// expect(queryByTestId('outer-result').textContent).toEqual('Total is:333') +// expect(queryByTestId('inner-result').textContent).toEqual('Total is:333') +// expect(onChangeHandler).toHaveBeenCalledTimes(2) +// actions.reset() +// await wait() +// expect(queryByTestId('test-input').getAttribute('value')).toEqual('') +// expect(queryByTestId('outer-result').textContent).toEqual('Total is:333') +// expect(queryByTestId('inner-result').textContent).toEqual('Total is:333') +// expect(onChangeHandler).toHaveBeenCalledTimes(2) +// await actions.setFieldState('a3', state => { +// state.value = '456' +// }) +// await wait() +// expect(queryByTestId('test-input').getAttribute('value')).toEqual('456') +// expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') +// expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') +// expect(onChangeHandler).toHaveBeenCalledTimes(3) +// await actions.reset() +// await wait() +// expect(queryByTestId('test-input').getAttribute('value')).toEqual('') +// expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') +// expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') +// expect(onChangeHandler).toHaveBeenCalledTimes(3) +// }) + +test('invariant initialValues will not be changed when form rerender', async () => { + const Component = () => { + const [, setDisabled] = useState(false) + + return ( +
    + { + act(() => setDisabled(false)) + }} + > + + + + + + + +
    + ) + } + + const { queryByTestId, queryByText } = render() + expect(queryByTestId('test-input-a1').getAttribute('value')).toEqual('a1') + expect(queryByTestId('test-input-a2').getAttribute('value')).toEqual('a2') + expect(queryByTestId('test-input-a3').getAttribute('value')).toEqual('') + + fireEvent.click(queryByText('Click')) + await wait() + + expect(queryByTestId('test-input-a1').getAttribute('value')).toEqual('a1') + expect(queryByTestId('test-input-a2').getAttribute('value')).toEqual('a2') + expect(queryByTestId('test-input-a3').getAttribute('value')).toEqual('') + + // 重新设置 SchemaForm Rerender + fireEvent.click(queryByText('Click')) + await wait() + + expect(queryByTestId('test-input-a1').getAttribute('value')).toEqual('a1') + expect(queryByTestId('test-input-a2').getAttribute('value')).toEqual('a2') + expect(queryByTestId('test-input-a3').getAttribute('value')).toEqual('') +}) + +test('submit with number name', async () => { + const onSubmitHandler = jest.fn() + const Component = () => { + return ( + + + + + + + + + ) + } + + const { queryByTestId, queryByText } = render() + fireEvent.change(queryByTestId('test-input'), { target: { value: '333' } }) + fireEvent.click(queryByText('Click')) + + await wait() + expect(onSubmitHandler).toHaveBeenCalledWith({ + aaa: { + bbb: '333' + } + }) +}) + +test('remove initial value by onFieldChange', async () => { + const Component = () => { + return ( +
    + { + $('onFieldChange', 'a2').subscribe(() => { + setFieldState('a1', state => { + state.value = undefined + }) + }) + }} + > + + + + + +
    + ) + } + const { queryAllByTestId } = render() + + await wait() + + expect(queryAllByTestId('test-input')[0].getAttribute('value')).toEqual('') +}) \ No newline at end of file From c69f71abe0d20adb2a1737283e910181467d08cb Mon Sep 17 00:00:00 2001 From: Janry Date: Sun, 10 Nov 2019 11:28:44 +0800 Subject: [PATCH 32/99] test(@uform/react-schema-renderer): improve v0 test cases (#384) --- docs/Examples/next/Relations.md | 15 +- docs/Examples/next/Sample.md | 6 +- packages/core/src/__tests__/index.spec.ts | 2 +- packages/core/src/index.ts | 47 +- packages/core/src/shared/model.ts | 7 + packages/core/src/state/field.ts | 57 ++- packages/core/src/types.ts | 16 +- .../src/__old_tests__/validate.spec.js | 439 ------------------ .../src/__old_tests__/value.spec.js | 371 --------------- .../src/__tests__/validate.spec.tsx | 65 ++- .../src/__tests__/value.spec.tsx | 354 +++++++------- .../src/components/SchemaField.tsx | 48 +- .../src/components/SchemaMarkup.tsx | 15 + packages/react/src/hooks/useField.ts | 4 +- packages/react/src/shared.ts | 4 +- packages/react/src/types.ts | 12 +- .../validator/src/__tests__/index.spec.ts | 4 +- packages/validator/src/rules.ts | 33 +- packages/validator/src/types.ts | 8 +- packages/validator/src/validator.ts | 5 +- scripts/global.ts | 1 - 21 files changed, 394 insertions(+), 1119 deletions(-) delete mode 100644 packages/react-schema-renderer/src/__old_tests__/validate.spec.js delete mode 100644 packages/react-schema-renderer/src/__old_tests__/value.spec.js diff --git a/docs/Examples/next/Relations.md b/docs/Examples/next/Relations.md index de06d314bc6..479a49c8804 100644 --- a/docs/Examples/next/Relations.md +++ b/docs/Examples/next/Relations.md @@ -39,8 +39,7 @@ import { FormCard, FormPath, FormBlock, - FormLayout, - filterChanged + FormLayout } from '@uform/next' import { filter, combineLatest, map, debounceTime } from 'rxjs/operators' import { Button } from '@alifd/next' @@ -64,20 +63,17 @@ const App = () => { } }) }) - $('onFieldChange', '*(aa,bb)') - .pipe(filterChanged('value')) + $('onFieldValueChange', '*(aa,bb)') .subscribe(fieldState => { console.log('aa或者bb发生变化了') }) - $('onFieldChange', 'aa') - .pipe(filterChanged('value')) + $('onFieldValueChange', 'aa') .subscribe(fieldState => { setFieldState('bb', state => { state.visible = !fieldState.value }) }) - $('onFieldChange', 'cc') - .pipe(filterChanged('value')) + $('onFieldValueChange', 'cc') .subscribe(fieldState => { setFieldState('dd', state => { state.visible = !fieldState.value @@ -96,9 +92,8 @@ const App = () => { } }) }) - $('onFieldChange', 'gg') + $('onFieldValueChange', 'gg') .pipe( - filterChanged('value'), combineLatest($('onChangeOption')), map(([fieldState, { payload: option }]) => { return { diff --git a/docs/Examples/next/Sample.md b/docs/Examples/next/Sample.md index bf3ae807cdf..22c7bb01801 100644 --- a/docs/Examples/next/Sample.md +++ b/docs/Examples/next/Sample.md @@ -35,7 +35,7 @@ ReactDOM.render( actions={actions} labelCol={7} wrapperCol={12} - effects={($, { setFieldState, watch }) => { + effects={($, { setFieldState, hasChanged }) => { $('onFormMount').subscribe(() => { setFieldState('radio', state => { state.required = true @@ -43,12 +43,12 @@ ReactDOM.render( }) $('onFormChange').subscribe(async state => { - watch(state, 'values.hello', async () => { + if (hasChanged(state, 'values.hello')) { await sleep(1000) setFieldState('radio', state => { state.value = '4' }) - }) + } }) }} > diff --git a/packages/core/src/__tests__/index.spec.ts b/packages/core/src/__tests__/index.spec.ts index cf7e6682a5d..379ac5fe3cd 100644 --- a/packages/core/src/__tests__/index.spec.ts +++ b/packages/core/src/__tests__/index.spec.ts @@ -738,7 +738,7 @@ describe('setFieldState', () => { ] form.setFieldState('a', state => (state.rules = customValidator)) const rules = form.getFieldState('a', state => state.rules) - expect(rules).toEqual([...customValidator, { required: true }]) + expect(rules).toEqual([...customValidator]) }) test('pristine', () => { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5dc9c1723af..38bd8afd1fc 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -286,7 +286,9 @@ export function createForm( function registerVirtualField({ name, path, - props + props, + computeState, + useDirty }: IVirtualFieldStateProps): IVirtualField { let nodePath = FormPath.parse(path || name) let dataPath = transformDataPath(nodePath) @@ -296,7 +298,8 @@ export function createForm( field = new VirtualFieldState({ nodePath, dataPath, - useDirty: options.useDirty + computeState, + useDirty: isValid(useDirty) ? useDirty : options.useDirty }) field.subscription = { notify: onVirtualFieldChange({ field, path: nodePath }) @@ -331,6 +334,8 @@ export function createForm( required, rules, editable, + computeState, + useDirty, props }: Exclude): IField { let field: IField @@ -341,7 +346,8 @@ export function createForm( field = new FieldState({ nodePath, dataPath, - useDirty: options.useDirty + computeState, + useDirty: isValid(useDirty) ? useDirty : options.useDirty }) field.subscription = { notify: onFieldChange({ field, path: nodePath }) @@ -644,12 +650,13 @@ export function createForm( } async function reset({ + selector = '*', forceClear = false, validate = true }: IFormResetOptions = {}): Promise { let result: Promise leadingUpdate(() => { - graph.eachChildren(field => { + graph.eachChildren('', selector, field => { field.setState((state: IFieldState) => { state.modified = false state.ruleErrors = [] @@ -949,41 +956,17 @@ export function createForm( } //在subscribe中必须同步使用,否则会监听不到变化 - function watch( - target: any, - path: FormPathPattern | (() => T), - callback?: () => T - ): Promise { - if (isFn(path)) { - callback = path as any - path = '' - } - + function hasChanged(target: any, path: FormPathPattern): boolean { if (!env.publishing) { throw new Error( 'The watch function must be used synchronously in the subscribe callback.' ) } - - const resolve = () => { - return new Promise(resolve => { - return setTimeout(() => { - if (isFn(callback)) { - resolve(Promise.resolve(callback())) - } - }) - }) - } - if (isFormState(target)) { - if (state.hasChanged(path)) { - return resolve() - } + return state.hasChanged(path) } else if (isFieldState(target) || isVirtualFieldState(target)) { const node = graph.get(target.path) - if (node && node.hasChanged(path)) { - return resolve() - } + return node && node.hasChanged(path) } else { throw new Error( 'Illegal parameter,You must pass the correct state object(FormState/FieldState/VirtualFieldState).' @@ -999,7 +982,7 @@ export function createForm( const formApi = { submit, reset, - watch, + hasChanged, clearErrors, validate, setFormState, diff --git a/packages/core/src/shared/model.ts b/packages/core/src/shared/model.ts index 874a3e240aa..8fc79fe882e 100644 --- a/packages/core/src/shared/model.ts +++ b/packages/core/src/shared/model.ts @@ -21,6 +21,7 @@ export const createStateModel = ( public props: Props & DefaultProps & { useDirty?: boolean + computeState?: (draft: State, prevState: State) => void } public displayName?: string public dirtyNum: number @@ -109,6 +110,9 @@ export const createStateModel = ( this.dirtyNum = 0 } callback(draft) + if (isFn(this.props.computeState)) { + this.props.computeState(draft, this.state) + } if (isFn(this.controller.computeState)) { this.controller.computeState(draft, this.state) } @@ -150,6 +154,9 @@ export const createStateModel = ( this.state, draft => { callback(draft) + if (isFn(this.props.computeState)) { + this.props.computeState(draft, this.state) + } if (isFn(this.controller.computeState)) { this.controller.computeState(draft, this.state) } diff --git a/packages/core/src/state/field.ts b/packages/core/src/state/field.ts index 405c7138c12..dd5b3af6991 100644 --- a/packages/core/src/state/field.ts +++ b/packages/core/src/state/field.ts @@ -74,21 +74,56 @@ export const FieldState = createStateModel( } } - readRules({ rules, required }: IFieldStateProps) { + readRequired(rules: any[]) { + for (let i = 0; i < rules.length; i++) { + if (rules[i].required !== undefined) { + return rules[i].required + } + } + } + + readRules({ rules, required }: IFieldStateProps, prevState: IFieldState) { let newRules = isValid(rules) ? clone(toArr(rules)) : this.state.rules - if (isValid(required)) { - if ( - required && - !newRules.some(rule => rule && rule.required !== undefined) - ) { - newRules.push({ required }) + let newRequired = required !== undefined ? required : false + const currentRulesRequired = this.readRequired(newRules) + const prevRulesRequired = this.readRequired(prevState.rules) + if (prevState.required !== newRequired) { + if (!newRules.some(rule => rule && rule.required !== undefined)) { + newRules.push({ required: newRequired }) + } else { + newRules = newRules.reduce((buf: any[], item: any) => { + const keys = Object.keys(item || {}) + if (item.required !== undefined) { + if (item.message !== undefined) { + if (keys.length > 2) { + return { + ...item, + required: newRequired + } + } else { + return buf + } + } else { + if (keys.length > 1) { + return { + ...item, + required: newRequired + } + } else { + return buf + } + } + } + return buf.concat(item) + }, []) } - } else { - required = newRules.some(rule => rule && rule.required === true) + } + if (currentRulesRequired !== prevRulesRequired) { + newRequired = currentRulesRequired } return { rules: newRules, - required + required: newRequired } } @@ -179,7 +214,7 @@ export const FieldState = createStateModel( draft.invalid = false draft.valid = true } - const { rules, required } = this.readRules(draft) + const { rules, required } = this.readRules(draft, prevState) draft.rules = rules draft.required = required } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index c05a082dd74..ceef3f9ce7b 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -153,6 +153,8 @@ export interface IFieldStateProps { rules?: ValidatePatternRules[] required?: boolean editable?: boolean + useDirty?: boolean + computeState?: (draft: IFieldState, prevState: IFieldState) => void onChange?: (fieldState: IField) => void } @@ -166,7 +168,7 @@ export const isFieldState = (target: any): target is IFieldState => target && target.displayName === 'FieldState' && target.name && target.path export const isFormState = (target: any): target is IFormState => - target && target.displayName === 'FormState' && target.name && target.path + target && target.displayName === 'FormState' export const isVirtualField = (target: any): target is IVirtualField => target && @@ -236,6 +238,11 @@ export interface IVirtualFieldStateProps { path?: FormPathPattern dataPath?: FormPathPattern nodePath?: FormPathPattern + useDirty?: boolean + computeState?: ( + draft: IVirtualFieldState, + prevState: IVirtualFieldState + ) => void name?: string props?: FieldProps onChange?: (fieldState: IVirtualField) => void @@ -251,6 +258,7 @@ export interface IFormSubmitResult { export interface IFormResetOptions { forceClear?: boolean validate?: boolean + selector?: FormPathPattern } export interface IFormGraph { @@ -305,11 +313,7 @@ export interface IForm { onSubmit?: (values: IFormState['values']) => any | Promise ): Promise clearErrors: (pattern?: FormPathPattern) => void - watch( - target: any, - path: FormPathPattern | (() => T), - callback?: () => T - ): Promise + hasChanged(target: any, path: FormPathPattern): boolean reset(options?: IFormResetOptions): Promise validate(path?: FormPathPattern, options?: {}): Promise setFormState(callback?: (state: IFormState) => any, silent?: boolean): void diff --git a/packages/react-schema-renderer/src/__old_tests__/validate.spec.js b/packages/react-schema-renderer/src/__old_tests__/validate.spec.js deleted file mode 100644 index b5b57119123..00000000000 --- a/packages/react-schema-renderer/src/__old_tests__/validate.spec.js +++ /dev/null @@ -1,439 +0,0 @@ -import React, { useState, useEffect } from 'react' -import SchemaForm, { - Field, - registerFormField, - connect, - registerFieldMiddleware, - FormPath, - createFormActions -} from '../index' -import { - render, - fireEvent, - act, - waitForElement, - waitForDomChange -} from '@testing-library/react' - -registerFieldMiddleware(Field => { - return props => { - return ( -
    - - {props.errors} -
    - ) - } -}) - -registerFormField( - 'string', - connect()(props => ( - - )) -) - -test('basic validate', async () => { - const handleSubmit = jest.fn() - const handleValidateFailed = jest.fn() - const TestComponent = () => ( - - - - - ) - - const { getByTestId, getByText } = render() - - fireEvent.click(getByTestId('btn')) - await sleep(33) - fireEvent.click(getByTestId('btn')) - await sleep(33) - expect(handleSubmit).toHaveBeenCalledTimes(0) - expect(handleValidateFailed).toHaveBeenCalledTimes(2) - expect(getByText('text is required')).toBeVisible() -}) - -test('validate in init', async () => { - const handleSubmit = jest.fn() - const handleValidateFailed = jest.fn() - const TestComponent = () => { - const [state, setState] = useState() - useEffect(() => { - act(() => { - setState({ - text: '' - }) - }) - }, []) - return ( - - - - - ) - } - let result - act(() => { - result = render() - }) - await sleep(33) - const { queryByText } = result - expect(queryByText('text is required')).toBeNull() - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(handleSubmit).toHaveBeenCalledTimes(0) - expect(handleValidateFailed).toHaveBeenCalledTimes(1) - expect(queryByText('text is required')).toBeVisible() -}) - -test('validate in editable false', async () => { - const handleSubmit = jest.fn() - const handleValidateFailed = jest.fn() - const actions = createFormActions() - const TestComponent = () => { - const [state, setState] = useState() - useEffect(() => { - act(() => { - setState({ - editable: '' - }) - }) - }, []) - return ( - - - - - ) - } - let result - act(() => { - result = render() - }) - await sleep(33) - const { queryByText } = result - expect(queryByText('editable is required')).toBeNull() - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(handleSubmit).toHaveBeenCalledTimes(0) - expect(handleValidateFailed).toHaveBeenCalledTimes(1) - expect(queryByText('editable is required')).toBeVisible() - actions.setFieldState('editable', state => { - state.value = '123' - }) - await sleep(33) - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(handleSubmit).toHaveBeenCalledTimes(1) - expect(handleValidateFailed).toHaveBeenCalledTimes(1) - expect(queryByText('editable is required')).toBeNull() -}) - -test('modify required rules by setFieldState', async () => { - const actions = createFormActions() - const handleSubmit = jest.fn() - const handleValidateFailed = jest.fn() - const TestComponent = () => { - return ( - - - - - ) - } - const { queryByText } = render() - await sleep(33) - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(handleSubmit).toBeCalledTimes(1) - expect(handleValidateFailed).toBeCalledTimes(0) - actions.setFieldState('kk', state => { - state.props.required = true - }) - await sleep(33) - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(queryByText('kk is required')).toBeVisible() - expect(handleSubmit).toBeCalledTimes(1) - expect(handleValidateFailed).toBeCalledTimes(1) - actions.setFieldState('kk', state => { - state.required = false - }) - await sleep(33) - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(queryByText('kk is required')).toBeNull() - expect(handleSubmit).toBeCalledTimes(2) - expect(handleValidateFailed).toBeCalledTimes(1) - actions.setFieldState('kk', state => { - state.required = true - }) - await sleep(33) - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(queryByText('kk is required')).toBeVisible() - expect(handleSubmit).toBeCalledTimes(2) - expect(handleValidateFailed).toBeCalledTimes(2) -}) - -test('modify validate rules by setFieldState', async () => { - const actions = createFormActions() - const TestComponent = () => { - return ( - - - - - ) - } - const { queryByText, queryByTestId } = render() - await sleep(33) - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(queryByText('required')).toBeVisible() - actions.setFieldState('bb', state => { - state.rules = [ - { required: true }, - { - pattern: /^\d{6}$/, - message: 'must have 6 numbers' - } - ] - }) - await sleep(33) - fireEvent.change(queryByTestId('test-input'), { - target: { value: '123' } - }) - await sleep(33) - expect(queryByText('must have 6 numbers')).toBeVisible() -}) - -test('dynamic update values', async () => { - const TestComponent = () => { - return ( - - - - - ) - } - const { queryByTestId, queryByText } = render() - await sleep(33) - fireEvent.change(queryByTestId('test-input'), { - target: { value: '12332123' } - }) - await sleep(33) - fireEvent.change(queryByTestId('test-input'), { - target: { value: '12332123a' } - }) - await sleep(33) - fireEvent.change(queryByTestId('test-input'), { target: { value: '123321' } }) - await sleep(33) - fireEvent.change(queryByTestId('test-input'), { target: { value: '12332' } }) - await sleep(33) - expect(queryByText('must be number')).toBeNull() - fireEvent.change(queryByTestId('test-input'), { target: { value: '12332a' } }) - await sleep(33) - expect(queryByText('must be number')).toBeVisible() -}) - -test('test idcard rules', async () => { - const TestComponent = () => { - return ( - - - - ) - } - const { queryByTestId, queryByText } = render() - // 14位数字 - const value1 = '12345678912345' - // 15位数字 - const value2 = '123456789123456' - // 17位数字 - const value3 = '12345678912345678' - // 17位数字+x - const value4 = '12345678912345678x' - // 17位数字+X - const value5 = '12345678912345678X' - // 18位数字 - const value6 = '123456789123456789' - - const element = await waitForElement(() => queryByTestId('test-input')) - waitForDomChange({ container: element }).then(mutationsList => { - const mutation = mutationsList[0] - const { value } = mutation.target - const errorTipsElement = queryByText('idCard is not an idcard format') - if (value === value1 || value === value3) { - expect(errorTipsElement).toBeVisible() - } else { - expect(errorTipsElement).toBeNull() - } - }) - fireEvent.change(element, { target: { value: value1 } }) - fireEvent.change(element, { target: { value: value2 } }) - fireEvent.change(element, { target: { value: value3 } }) - fireEvent.change(element, { target: { value: value4 } }) - fireEvent.change(element, { target: { value: value5 } }) - fireEvent.change(element, { target: { value: value6 } }) -}) - -test('dynamic switch visible', async () => { - const TestComponent = () => { - return ( - { - $('onFieldChange', 'aa').subscribe(({ value }) => { - setFieldState('bb', state => { - state.visible = value == 'aa' - }) - }) - }} - > - - - - ) - } - const { queryAllByTestId, queryByText } = render() - await sleep(33) - fireEvent.change(queryAllByTestId('test-input')[0], { - target: { value: 'aa' } - }) - await sleep(33) - fireEvent.change(queryAllByTestId('test-input')[0], { - target: { value: 'bb' } - }) - await sleep(33) - fireEvent.change(queryAllByTestId('test-input')[0], { - target: { value: 'aa' } - }) - await sleep(33) - expect(queryByText('bb is required')).toBeNull() -}) - -test('async validate prevent submit', async () => { - const onSubmitHandler = jest.fn() - const TestComponent = () => { - return ( - - { - return new Promise(resolve => { - setTimeout(() => { - if (val === '123') { - resolve('can not input 123') - } else { - resolve() - } - }, 100) - }) - }} - /> - - - ) - } - const { queryAllByTestId, queryByText } = render() - await sleep(33) - fireEvent.change(queryAllByTestId('test-input')[0], { - target: { value: '444' } - }) - await sleep(33) - fireEvent.click(queryByText('Submit')) - fireEvent.click(queryByText('Submit')) - fireEvent.click(queryByText('Submit')) - await sleep(300) - expect(queryByText('can not input 123')).toBeNull() - expect(onSubmitHandler).toBeCalledTimes(1) - fireEvent.change(queryAllByTestId('test-input')[0], { - target: { value: '123' } - }) - await sleep(33) - fireEvent.click(queryByText('Submit')) - await sleep(200) - expect(queryByText('can not input 123')).toBeVisible() - expect(onSubmitHandler).toBeCalledTimes(1) -}) - -test('async validate side effect', async () => { - const actions = createFormActions() - const TestComponent = () => { - return ( - - - - - - ) - } - const { queryAllByTestId, queryByText } = render() - await sleep(33) - fireEvent.change(queryAllByTestId('test-input')[0], { - target: { value: 'aaaaa' } - }) - fireEvent.change(queryAllByTestId('test-input')[1], { - target: { value: 'bbbbb' } - }) - await sleep(33) - fireEvent.click(queryByText('Cancel')) - await sleep(33) - fireEvent.change(queryAllByTestId('test-input')[0], { - target: { value: 'aaaaa' } - }) - await sleep(33) - expect(queryByText('aa is required')).toBeNull() - expect(queryByText('bb is required')).toBeNull() - await sleep(33) - fireEvent.change(queryAllByTestId('test-input')[0], { - target: { value: '' } - }) - await sleep(33) - expect(queryByText('aa is required')).toBeVisible() - expect(queryByText('bb is required')).toBeNull() -}) diff --git a/packages/react-schema-renderer/src/__old_tests__/value.spec.js b/packages/react-schema-renderer/src/__old_tests__/value.spec.js deleted file mode 100644 index d3ef1224b45..00000000000 --- a/packages/react-schema-renderer/src/__old_tests__/value.spec.js +++ /dev/null @@ -1,371 +0,0 @@ -import React, { useState } from 'react' -import { render, act, fireEvent } from '@testing-library/react' -import SchemaForm, { - Field, - registerFormField, - connect, - createFormActions, - FormSlot -} from '../index' - -registerFormField( - 'test-string', - connect()(props => ( - -
    {props.value}
    -
    {typeof props.value}
    -
    - )) -) - -registerFormField( - 'string', - connect()(props => ( - - )) -) - -registerFormField( - 'name-string', - connect()(props => ( - - )) -) - -test('default value', async () => { - const Component = () => ( - - - - ) - - const { getByTestId } = render() - await sleep(33) - expect(getByTestId('type-value').textContent).toEqual('string') -}) - -test('initialValues', async () => { - const Component = () => ( - - - - ) - - const { getByTestId } = render() - await sleep(33) - expect(getByTestId('type-value').textContent).toEqual('string') -}) - -test('controlled initialValues', async () => { - const actions = createFormActions() - let outerSetState - const Component = () => { - const [state, setState] = useState({ - foo: '123' - }) - outerSetState = values => - act(() => { - setState(values) - }) - return ( - - - - ) - } - - const { getByTestId } = render() - await sleep(33) - expect(getByTestId('type-value').textContent).toEqual('string') - await actions.setFieldState('foo', state => { - state.value = '321' - }) - await actions.reset() - await sleep(33) - expect(getByTestId('value').textContent).toEqual('123') - await actions.setFieldState('foo', state => { - state.value = '321' - }) - await sleep(33) - act(() => { - outerSetState({ foo: '123' }) - }) - await sleep(33) - expect(getByTestId('value').textContent).toEqual('123') - await sleep(33) -}) - -test('controlled with hooks by initalValues', async () => { - const onChangeHandler = jest.fn() - const actions = createFormActions() - const Component = () => { - const [total, setTotal] = useState(0) - return ( -
    - { - $('onFieldChange', 'a3').subscribe(onChangeHandler) - $('onFieldChange', 'a3').subscribe(state => { - act(() => { - setTotal(state.value) - }) - }) - }} - > - - -
    Total is:{total}
    -
    -
    -
    Total is:{total}
    -
    - ) - } - - const { queryByTestId } = render() - expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') - fireEvent.change(queryByTestId('test-input'), { target: { value: '333' } }) - await sleep(33) - expect(queryByTestId('outer-result').textContent).toEqual('Total is:333') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:333') - expect(onChangeHandler).toHaveBeenCalledTimes(2) - await actions.setFieldState('a3', state => { - state.value = '456' - }) - await sleep(33) - expect(queryByTestId('test-input').value).toEqual('456') - expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') - expect(onChangeHandler).toHaveBeenCalledTimes(3) - await actions.reset() - await sleep(33) - expect(queryByTestId('test-input').value).toEqual('123') - expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') - expect(onChangeHandler).toHaveBeenCalledTimes(3) -}) - -test('controlled with hooks by static value', async () => { - const onChangeHandler = jest.fn() - const actions = createFormActions() - const Component = () => { - const [total, setTotal] = useState(0) - - return ( -
    - { - $('onFieldChange', 'a3').subscribe(onChangeHandler) - $('onFieldChange', 'a3').subscribe(state => { - act(() => { - setTotal(state.value) - }) - }) - }} - > - - -
    Total is:{total}
    -
    -
    -
    Total is:{total}
    -
    - ) - } - - const { queryByTestId } = render() - expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') - fireEvent.change(queryByTestId('test-input'), { target: { value: '333' } }) - await sleep(33) - expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') - expect(onChangeHandler).toHaveBeenCalledTimes(3) - actions.reset() - await sleep(33) - expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') - expect(onChangeHandler).toHaveBeenCalledTimes(3) - await actions.setFieldState('a3', state => { - state.value = '456' - }) - await sleep(33) - expect(queryByTestId('test-input').value).toEqual('123') - expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') - expect(onChangeHandler).toHaveBeenCalledTimes(5) - await actions.reset() - await sleep(33) - expect(queryByTestId('test-input').value).toEqual('') - expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') - expect(onChangeHandler).toHaveBeenCalledTimes(5) -}) - -test('controlled with hooks by dynamic value', async () => { - const onChangeHandler = jest.fn() - const actions = createFormActions() - const Component = () => { - const [total, setTotal] = useState('123') - - return ( -
    - { - $('onFieldChange', 'a3').subscribe(onChangeHandler) - $('onFieldChange', 'a3').subscribe(state => { - act(() => { - setTotal(state.value) - }) - }) - }} - > - - -
    Total is:{total}
    -
    -
    -
    Total is:{total}
    -
    - ) - } - - const { queryByTestId } = render() - expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') - fireEvent.change(queryByTestId('test-input'), { target: { value: '333' } }) - await sleep(33) - expect(queryByTestId('outer-result').textContent).toEqual('Total is:333') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:333') - expect(onChangeHandler).toHaveBeenCalledTimes(2) - actions.reset() - await sleep(33) - expect(queryByTestId('test-input').value).toEqual('') - expect(queryByTestId('outer-result').textContent).toEqual('Total is:333') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:333') - expect(onChangeHandler).toHaveBeenCalledTimes(2) - await actions.setFieldState('a3', state => { - state.value = '456' - }) - await sleep(33) - expect(queryByTestId('test-input').value).toEqual('456') - expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') - expect(onChangeHandler).toHaveBeenCalledTimes(3) - await actions.reset() - await sleep(33) - expect(queryByTestId('test-input').value).toEqual('') - expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') - expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') - expect(onChangeHandler).toHaveBeenCalledTimes(3) -}) - -test('invariant initialValues will not be changed when form rerender', async () => { - const Component = () => { - const [, setDisabled] = useState(false) - - return ( -
    - { - act(setDisabled) - }} - > - - - - - -
    - ) - } - - const { queryByTestId, queryByText } = render() - expect(queryByTestId('test-input-a1').value).toEqual('a1') - expect(queryByTestId('test-input-a2').value).toEqual('a2') - expect(queryByTestId('test-input-a3').value).toEqual('') - - fireEvent.click(queryByText('Click')) - await sleep(33) - - expect(queryByTestId('test-input-a1').value).toEqual('a1') - expect(queryByTestId('test-input-a2').value).toEqual('a2') - expect(queryByTestId('test-input-a3').value).toEqual('') - - // 重新设置 SchemaForm Rerender - fireEvent.click(queryByText('Click')) - await sleep(33) - - expect(queryByTestId('test-input-a1').value).toEqual('a1') - expect(queryByTestId('test-input-a2').value).toEqual('a2') - expect(queryByTestId('test-input-a3').value).toEqual('') -}) - -test('submit with number name', async () => { - const onSubmitHandler = jest.fn() - const Component = () => { - return ( - - - - - - - ) - } - - const { queryByTestId, queryByText } = render() - fireEvent.change(queryByTestId('test-input'), { target: { value: '333' } }) - fireEvent.click(queryByText('Click')) - - await sleep(33) - expect(onSubmitHandler).toHaveBeenCalledWith({ - aaa: { - bbb: '333' - } - }) -}) - -test('remove initial value by onFieldChange', async () => { - const Component = () => { - return ( -
    - { - $('onFieldChange', 'a2').subscribe(() => { - setFieldState('a1', state => { - state.value = undefined - }) - }) - }} - > - - - -
    - ) - } - const { queryAllByTestId } = render() - - await sleep(33) - - expect(queryAllByTestId('test-input')[0].value).toEqual('') -}) diff --git a/packages/react-schema-renderer/src/__tests__/validate.spec.tsx b/packages/react-schema-renderer/src/__tests__/validate.spec.tsx index f1eb7e22053..f5c27c43026 100644 --- a/packages/react-schema-renderer/src/__tests__/validate.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/validate.spec.tsx @@ -8,7 +8,21 @@ import { registerFieldMiddleware, FormPath } from '../index' -import { render, fireEvent, wait, act, waitForElement, waitForDomChange } from '@testing-library/react' +import '@testing-library/jest-dom/extend-expect' +import { + render, + fireEvent, + wait, + act, + waitForElement, + waitForDomChange +} from '@testing-library/react' + +const sleep = timeout => { + return new Promise(resolve => { + setTimeout(resolve, timeout) + }) +} registerFieldMiddleware(Field => { return props => { @@ -26,7 +40,7 @@ registerFormField( connect()(props => ( )) @@ -64,12 +78,14 @@ test('setFieldState will trigger validate', async () => { const { getByTestId } = render() fireEvent.click(getByTestId('btn')) - await wait(); + await wait() expect(handleSubmit).toHaveBeenCalledTimes(0) expect(handleValidateFailed).toHaveBeenCalledTimes(1) - expect(getByTestId('test-errors-2')).toHaveTextContent('This field is required') + expect(getByTestId('test-errors-2')).toHaveTextContent( + 'This field is required' + ) fireEvent.change(getByTestId('test-input-1'), { target: { value: '123' } }) - await wait(); + await wait() expect(getByTestId('test-input-2')).toHaveAttribute('value', '123') expect(getByTestId('test-errors-2')).not.toHaveTextContent( 'This field is required' @@ -93,12 +109,12 @@ test('basic validate', async () => { const { getByTestId, getByText } = render() fireEvent.click(getByTestId('btn')) - await wait(); + await wait() fireEvent.click(getByTestId('btn')) await wait() expect(handleSubmit).toHaveBeenCalledTimes(0) expect(handleValidateFailed).toHaveBeenCalledTimes(2) - expect(getByText('text is required')).toBeVisible() + expect(getByText('This field is required')).toBeVisible() }) test('validate in init', async () => { @@ -139,7 +155,7 @@ test('validate in init', async () => { await wait() expect(handleSubmit).toHaveBeenCalledTimes(0) expect(handleValidateFailed).toHaveBeenCalledTimes(1) - expect(queryByText('text is required')).toBeVisible() + expect(queryByText('This field is required')).toBeVisible() }) test('validate in editable false', async () => { @@ -183,7 +199,7 @@ test('validate in editable false', async () => { await wait() expect(handleSubmit).toHaveBeenCalledTimes(0) expect(handleValidateFailed).toHaveBeenCalledTimes(1) - expect(queryByText('editable is required')).toBeVisible() + expect(queryByText('This field is required')).toBeVisible() actions.setFieldState('editable', state => { state.value = '123' }) @@ -227,7 +243,7 @@ test('modify required rules by setFieldState', async () => { await wait() fireEvent.click(queryByText('Submit')) await wait() - expect(queryByText('kk is required')).toBeVisible() + expect(queryByText('This field is required')).toBeVisible() expect(handleSubmit).toBeCalledTimes(1) expect(handleValidateFailed).toBeCalledTimes(1) actions.setFieldState('kk', state => { @@ -236,7 +252,7 @@ test('modify required rules by setFieldState', async () => { await wait() fireEvent.click(queryByText('Submit')) await wait() - expect(queryByText('kk is required')).toBeNull() + expect(queryByText('This field is required')).toBeNull() expect(handleSubmit).toBeCalledTimes(2) expect(handleValidateFailed).toBeCalledTimes(1) actions.setFieldState('kk', state => { @@ -245,7 +261,7 @@ test('modify required rules by setFieldState', async () => { await wait() fireEvent.click(queryByText('Submit')) await wait() - expect(queryByText('kk is required')).toBeVisible() + expect(queryByText('This field is required')).toBeVisible() expect(handleSubmit).toBeCalledTimes(2) expect(handleValidateFailed).toBeCalledTimes(2) }) @@ -384,7 +400,7 @@ test('dynamic switch visible', async () => { - +
    ) } @@ -407,9 +423,10 @@ test('dynamic switch visible', async () => { test('async validate prevent submit', async () => { const onSubmitHandler = jest.fn() + const actions = createFormActions() const TestComponent = () => { return ( - + { ) } - const { queryAllByTestId, queryByText } = render() + const { queryByTestId, queryByText } = render() await wait() - fireEvent.change(queryAllByTestId('test-input')[0], { + fireEvent.change(queryByTestId('test-input'), { target: { value: '444' } }) await wait() fireEvent.click(queryByText('Submit')) fireEvent.click(queryByText('Submit')) fireEvent.click(queryByText('Submit')) - await wait() + await sleep(110) expect(queryByText('can not input 123')).toBeNull() expect(onSubmitHandler).toBeCalledTimes(1) - fireEvent.change(queryAllByTestId('test-input')[0], { + fireEvent.change(queryByTestId('test-input'), { target: { value: '123' } }) await wait() fireEvent.click(queryByText('Submit')) - await wait() + await sleep(110) expect(queryByText('can not input 123')).toBeVisible() expect(onSubmitHandler).toBeCalledTimes(1) }) @@ -476,7 +493,7 @@ test('async validate side effect', async () => { ) } - const { queryAllByTestId, queryByText } = render() + const { queryAllByTestId,queryAllByText, queryByText } = render() await wait() fireEvent.change(queryAllByTestId('test-input')[0], { target: { value: 'aaaaa' } @@ -491,13 +508,11 @@ test('async validate side effect', async () => { target: { value: 'aaaaa' } }) await wait() - expect(queryByText('aa is required')).toBeNull() - expect(queryByText('bb is required')).toBeNull() + expect(queryAllByText('This field is required').length).toEqual(0) await wait() fireEvent.change(queryAllByTestId('test-input')[0], { target: { value: '' } }) await wait() - expect(queryByText('aa is required')).toBeVisible() - expect(queryByText('bb is required')).toBeNull() -}) \ No newline at end of file + expect(queryAllByText('This field is required').length).toEqual(1) +}) diff --git a/packages/react-schema-renderer/src/__tests__/value.spec.tsx b/packages/react-schema-renderer/src/__tests__/value.spec.tsx index 47a8ea113a1..179f1638695 100644 --- a/packages/react-schema-renderer/src/__tests__/value.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/value.spec.tsx @@ -5,7 +5,7 @@ import { SchemaMarkupForm as SchemaForm, SchemaMarkupField as Field, createFormActions, - // FormSlot + FormSlot } from '../index' import { render, fireEvent, act, wait } from '@testing-library/react' @@ -82,17 +82,18 @@ test('controlled initialValues', async () => { const { getByTestId } = render() await wait() expect(getByTestId('type-value').textContent).toEqual('string') - await actions.setFieldState('foo', state => { + actions.setFieldState('foo', state => { state.value = '321' }) - await actions.reset() + actions.reset() await wait() expect(getByTestId('value').textContent).toEqual('123') - await actions.setFieldState('foo', state => { + actions.setFieldState('foo', state => { state.value = '321' }) await wait() act(() => { + actions.reset() outerSetState({ foo: '123' }) }) await wait() @@ -100,184 +101,183 @@ test('controlled initialValues', async () => { await wait() }) +test('controlled with hooks by initalValues', async () => { + const onChangeHandler = jest.fn() + const actions = createFormActions() + const Component = () => { + const [total, setTotal] = useState(0) + return ( +
    + { + $('onFieldChange', 'a3').subscribe(onChangeHandler) + $('onFieldChange', 'a3').subscribe(state => { + act(() => { + setTotal(state.value) + }) + }) + }} + > + + + +
    Total is:{total}
    +
    +
    +
    +
    Total is:{total}
    +
    + ) + } -// test('controlled with hooks by initalValues', async () => { -// const onChangeHandler = jest.fn() -// const actions = createFormActions() -// const Component = () => { -// const [total, setTotal] = useState(0) -// return ( -//
    -// { -// $('onFieldChange', 'a3').subscribe(onChangeHandler) -// $('onFieldChange', 'a3').subscribe(state => { -// act(() => { -// setTotal(state.value) -// }) -// }) -// }} -// > -// -// -// -//
    Total is:{total}
    -//
    -//
    -//
    -//
    Total is:{total}
    -//
    -// ) -// } - -// const { queryByTestId } = render() -// expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') -// expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') -// fireEvent.change(queryByTestId('test-input'), { target: { value: '333' } }) -// await wait() -// expect(queryByTestId('outer-result').textContent).toEqual('Total is:333') -// expect(queryByTestId('inner-result').textContent).toEqual('Total is:333') -// expect(onChangeHandler).toHaveBeenCalledTimes(2) -// await actions.setFieldState('a3', state => { -// state.value = '456' -// }) -// await wait() -// expect(queryByTestId('test-input').getAttribute('value')).toEqual('456') -// expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') -// expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') -// expect(onChangeHandler).toHaveBeenCalledTimes(3) -// await actions.reset() -// await wait() -// expect(queryByTestId('test-input').getAttribute('value')).toEqual('123') -// expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') -// expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') -// expect(onChangeHandler).toHaveBeenCalledTimes(3) -// }) + const { queryByTestId } = render() + expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') + fireEvent.change(queryByTestId('test-input'), { target: { value: '333' } }) + await wait() + expect(queryByTestId('outer-result').textContent).toEqual('Total is:333') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:333') + expect(onChangeHandler).toHaveBeenCalledTimes(2) + actions.setFieldState('a3', state => { + state.value = '456' + }) + await wait() + expect(queryByTestId('test-input').getAttribute('value')).toEqual('456') + expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') + expect(onChangeHandler).toHaveBeenCalledTimes(3) + actions.reset() + await wait() + expect(queryByTestId('test-input').getAttribute('value')).toEqual('123') + expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') + expect(onChangeHandler).toHaveBeenCalledTimes(4) +}) -// test('controlled with hooks by static value', async () => { -// const onChangeHandler = jest.fn() -// const actions = createFormActions() -// const Component = () => { -// const [total, setTotal] = useState(0) +test('controlled with hooks by static value', async () => { + const onChangeHandler = jest.fn() + const actions = createFormActions() + const Component = () => { + const [total, setTotal] = useState(0) -// return ( -//
    -// { -// $('onFieldChange', 'a3').subscribe(onChangeHandler) -// $('onFieldChange', 'a3').subscribe(state => { -// act(() => { -// setTotal(state.value) -// }) -// }) -// }} -// > -// -// -// -//
    Total is:{total}
    -//
    -//
    -//
    -//
    Total is:{total}
    -//
    -// ) -// } + return ( +
    + { + $('onFieldChange', 'a3').subscribe(onChangeHandler) + $('onFieldChange', 'a3').subscribe(state => { + act(() => { + setTotal(state.value) + }) + }) + }} + > + + + +
    Total is:{total}
    +
    +
    +
    +
    Total is:{total}
    +
    + ) + } -// const { queryByTestId } = render() -// expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') -// expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') -// fireEvent.change(queryByTestId('test-input'), { target: { value: '333' } }) -// await wait() -// expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') -// expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') -// expect(onChangeHandler).toHaveBeenCalledTimes(3) -// actions.reset() -// await wait() -// expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') -// expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') -// expect(onChangeHandler).toHaveBeenCalledTimes(3) -// await actions.setFieldState('a3', state => { -// state.value = '456' -// }) -// await wait() -// expect(queryByTestId('test-input').getAttribute('value')).toEqual('123') -// expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') -// expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') -// expect(onChangeHandler).toHaveBeenCalledTimes(5) -// await actions.reset() -// await wait() -// expect(queryByTestId('test-input').getAttribute('value')).toEqual('') -// expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') -// expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') -// expect(onChangeHandler).toHaveBeenCalledTimes(5) -// }) + const { queryByTestId } = render() + expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') + fireEvent.change(queryByTestId('test-input'), { target: { value: '333' } }) + await wait() + expect(onChangeHandler).toHaveBeenCalledTimes(2) + expect(queryByTestId('outer-result').textContent).toEqual('Total is:333') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:333') + actions.reset() + await wait() + expect(queryByTestId('outer-result').textContent).toEqual('Total is:') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:') + expect(onChangeHandler).toHaveBeenCalledTimes(3) + actions.setFieldState('a3', state => { + state.value = '456' + }) + await wait() + expect(queryByTestId('test-input').getAttribute('value')).toEqual('456') + expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') + expect(onChangeHandler).toHaveBeenCalledTimes(4) + actions.reset() + await wait() + expect(queryByTestId('test-input').getAttribute('value')).toEqual('') + expect(queryByTestId('outer-result').textContent).toEqual('Total is:') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:') + expect(onChangeHandler).toHaveBeenCalledTimes(5) +}) -// test('controlled with hooks by dynamic value', async () => { -// const onChangeHandler = jest.fn() -// const actions = createFormActions() -// const Component = () => { -// const [total, setTotal] = useState('123') +test('controlled with hooks by dynamic value', async () => { + const onChangeHandler = jest.fn() + const actions = createFormActions() + const Component = () => { + const [total, setTotal] = useState('123') -// return ( -//
    -// { -// $('onFieldChange', 'a3').subscribe(onChangeHandler) -// $('onFieldChange', 'a3').subscribe(state => { -// act(() => { -// setTotal(state.value) -// }) -// }) -// }} -// > -// -// -// -//
    Total is:{total}
    -//
    -//
    -//
    -//
    Total is:{total}
    -//
    -// ) -// } + return ( +
    + { + $('onFieldChange', 'a3').subscribe(onChangeHandler) + $('onFieldChange', 'a3').subscribe(state => { + act(() => { + setTotal(state.value) + }) + }) + }} + > + + + +
    Total is:{total}
    +
    +
    +
    +
    Total is:{total}
    +
    + ) + } -// const { queryByTestId } = render() -// expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') -// expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') -// fireEvent.change(queryByTestId('test-input'), { target: { value: '333' } }) -// await wait() -// expect(queryByTestId('outer-result').textContent).toEqual('Total is:333') -// expect(queryByTestId('inner-result').textContent).toEqual('Total is:333') -// expect(onChangeHandler).toHaveBeenCalledTimes(2) -// actions.reset() -// await wait() -// expect(queryByTestId('test-input').getAttribute('value')).toEqual('') -// expect(queryByTestId('outer-result').textContent).toEqual('Total is:333') -// expect(queryByTestId('inner-result').textContent).toEqual('Total is:333') -// expect(onChangeHandler).toHaveBeenCalledTimes(2) -// await actions.setFieldState('a3', state => { -// state.value = '456' -// }) -// await wait() -// expect(queryByTestId('test-input').getAttribute('value')).toEqual('456') -// expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') -// expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') -// expect(onChangeHandler).toHaveBeenCalledTimes(3) -// await actions.reset() -// await wait() -// expect(queryByTestId('test-input').getAttribute('value')).toEqual('') -// expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') -// expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') -// expect(onChangeHandler).toHaveBeenCalledTimes(3) -// }) + const { queryByTestId } = render() + expect(queryByTestId('outer-result').textContent).toEqual('Total is:123') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:123') + fireEvent.change(queryByTestId('test-input'), { target: { value: '333' } }) + await wait() + expect(queryByTestId('outer-result').textContent).toEqual('Total is:333') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:333') + expect(onChangeHandler).toHaveBeenCalledTimes(2) + actions.reset() + await wait() + expect(queryByTestId('test-input').getAttribute('value')).toEqual('') + expect(queryByTestId('outer-result').textContent).toEqual('Total is:') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:') + expect(onChangeHandler).toHaveBeenCalledTimes(3) + await actions.setFieldState('a3', state => { + state.value = '456' + }) + await wait() + expect(queryByTestId('test-input').getAttribute('value')).toEqual('456') + expect(queryByTestId('outer-result').textContent).toEqual('Total is:456') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:456') + expect(onChangeHandler).toHaveBeenCalledTimes(4) + await actions.reset() + await wait() + expect(queryByTestId('test-input').getAttribute('value')).toEqual('') + expect(queryByTestId('outer-result').textContent).toEqual('Total is:') + expect(queryByTestId('inner-result').textContent).toEqual('Total is:') + expect(onChangeHandler).toHaveBeenCalledTimes(5) +}) test('invariant initialValues will not be changed when form rerender', async () => { const Component = () => { @@ -301,7 +301,7 @@ test('invariant initialValues will not be changed when form rerender', async () /> - +
    ) @@ -382,4 +382,4 @@ test('remove initial value by onFieldChange', async () => { await wait() expect(queryAllByTestId('test-input')[0].getAttribute('value')).toEqual('') -}) \ No newline at end of file +}) diff --git a/packages/react-schema-renderer/src/components/SchemaField.tsx b/packages/react-schema-renderer/src/components/SchemaField.tsx index 8354cf4edc1..1de023ab5d3 100644 --- a/packages/react-schema-renderer/src/components/SchemaField.tsx +++ b/packages/react-schema-renderer/src/components/SchemaField.tsx @@ -1,6 +1,6 @@ import React, { useContext, Fragment } from 'react' -import { Field, VirtualField } from '@uform/react' -import { FormPath, isFn, isStr } from '@uform/shared' +import { Field, VirtualField,IFieldState } from '@uform/react' +import { FormPath, isFn, isStr,isEqual } from '@uform/shared' import { ISchemaFieldProps, ISchemaFieldComponentProps, @@ -9,6 +9,26 @@ import { import { Schema } from '../shared/schema' import SchemaContext, { FormComponentsContext } from '../shared/context' +const computeSchemaState = (draft:IFieldState,prevState:IFieldState)=>{ + const schema = new Schema(draft.props) + const prevSchema = new Schema(prevState.props) + const currentRequired = schema.getExtendsRequired() + const prevRequired = prevSchema.getExtendsRequired() + const currentRules = schema.getExtendsRules() + const prevRules = prevSchema.getExtendsRules() + const currentEditable = schema.getExtendsEditable() + const prevEditable = prevSchema.getExtendsEditable() + if(!isEqual(currentRequired,prevRequired)){ + draft.required = currentRequired + } + if(!isEqual(currentRules,prevRules)){ + draft.rules = currentRules + } + if(!isEqual(currentEditable,prevEditable)){ + draft.selfEditable = currentEditable + } +} + export const SchemaField: React.FunctionComponent = ( props: ISchemaFieldProps ) => { @@ -44,6 +64,7 @@ export const SchemaField: React.FunctionComponent = ( editable={fieldSchema.getExtendsEditable()} required={fieldSchema.getExtendsRequired()} rules={fieldSchema.getExtendsRules()} + computeState={computeSchemaState} > {({ state, mutators, form }) => { const props: ISchemaFieldComponentProps = { @@ -69,11 +90,17 @@ export const SchemaField: React.FunctionComponent = ( return {properties} } return renderChildren(props => { - return React.createElement( - formRegistry.formItemComponent, - props, - properties - ) + const renderComponent = () => { + return React.createElement( + formRegistry.formItemComponent, + props, + properties + ) + } + if (isFn(schemaRenderer)) { + return schemaRenderer({ ...props, renderComponent }) + } + return renderComponent() }) } else { if (isFn(finalComponentName)) { @@ -113,12 +140,7 @@ export const SchemaField: React.FunctionComponent = ( children: fieldSchema.mapProperties( (schema: Schema, key: string) => { const childPath = path.concat(key) - return ( - - ) + return } ) } diff --git a/packages/react-schema-renderer/src/components/SchemaMarkup.tsx b/packages/react-schema-renderer/src/components/SchemaMarkup.tsx index a167d68db03..a57712d4c19 100644 --- a/packages/react-schema-renderer/src/components/SchemaMarkup.tsx +++ b/packages/react-schema-renderer/src/components/SchemaMarkup.tsx @@ -129,3 +129,18 @@ export function createControllerBox( } return VirtualBox } + +export const FormSlot: React.FC<{ + name?: string + children?: React.ReactElement +}> = ({ name, children }) => { + return ( + { + return {children} + }} + /> + ) +} diff --git a/packages/react/src/hooks/useField.ts b/packages/react/src/hooks/useField.ts index 200917f5671..fa5d4700ac8 100644 --- a/packages/react/src/hooks/useField.ts +++ b/packages/react/src/hooks/useField.ts @@ -52,12 +52,12 @@ export const useField = ( const mutators = useMemo(() => { let initialized = false ref.current.field = form.registerField(options) - ref.current.subscriberId = ref.current.field.subscribe(() => { + ref.current.subscriberId = ref.current.field.subscribe(fieldState => { /** * 同步Field状态只需要forceUpdate一下触发重新渲染,因为字段状态全部代理在uform core内部 */ if (initialized) { - if (options.triggerType === 'onChange') { + if (options.triggerType === 'onChange' && !fieldState.pristine) { if (ref.current.field.hasChanged('value')) { mutators.validate() } diff --git a/packages/react/src/shared.ts b/packages/react/src/shared.ts index 8d793dcbf72..b2c65bbf607 100644 --- a/packages/react/src/shared.ts +++ b/packages/react/src/shared.ts @@ -18,7 +18,7 @@ export const createFormActions = (): IFormActions => { return createActions( 'submit', 'reset', - 'watch', + 'hasChanged', 'validate', 'setFormState', 'getFormState', @@ -41,7 +41,7 @@ export const createAsyncFormActions = (): IFormAsyncActions => createAsyncActions( 'submit', 'reset', - 'watch', + 'hasChanged', 'validate', 'setFormState', 'getFormState', diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 3238391941d..39712cf1153 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -128,11 +128,7 @@ export interface IFormActions { onSubmit?: (values: IFormState['values']) => void | Promise ): Promise reset(options?: IFormResetOptions): void - watch( - target: any, - path: FormPathPattern | (() => T), - callback?: () => T - ): Promise + hasChanged(target: any, path: FormPathPattern): boolean validate(path?: FormPathPattern, options?: {}): Promise setFormState(callback?: (state: IFormState) => any): void getFormState(callback?: (state: IFormState) => any): any @@ -162,11 +158,7 @@ export interface IFormAsyncActions { onSubmit?: (values: IFormState['values']) => void | Promise ): Promise reset(options?: IFormResetOptions): Promise - watch( - target: any, - path: FormPathPattern | (() => T), - callback?: () => T - ): Promise + hasChanged(target: any, path: FormPathPattern): Promise clearErrors: (pattern?: FormPathPattern) => Promise validate(path?: FormPathPattern, options?: {}): Promise setFormState(callback?: (state: IFormState) => any): Promise diff --git a/packages/validator/src/__tests__/index.spec.ts b/packages/validator/src/__tests__/index.spec.ts index fce52e1fd8b..5895dc67baf 100644 --- a/packages/validator/src/__tests__/index.spec.ts +++ b/packages/validator/src/__tests__/index.spec.ts @@ -51,7 +51,7 @@ test('required', async () => { required: true, message: '该字段不能为空' }, - errors: ['This field is required'] + errors: ['该字段不能为空'] }, { value: [], @@ -59,7 +59,7 @@ test('required', async () => { required: true, message: '该字段不能为空' }, - errors: ['This field is required'] + errors: ['该字段不能为空'] }, { value: false, diff --git a/packages/validator/src/rules.ts b/packages/validator/src/rules.ts index e4f04740831..dc28bf6786a 100644 --- a/packages/validator/src/rules.ts +++ b/packages/validator/src/rules.ts @@ -24,44 +24,55 @@ const isValidateEmpty = (value: any) => { const getLength = (value: any) => isStr(value) ? stringLength(value) : value ? value.length : 0 +const getRuleMessage = (rule: any, type: string) => { + if (isFn(rule.validator) || Object.keys(rule).length > 2) { + return getMessage(type) + } else { + return rule.message || getMessage(type) + } +} + export default { required(value: any, rule: ValidateDescription) { - return isValidateEmpty(value) ? getMessage('required') : '' + if (rule.required === false) return '' + return isValidateEmpty(value) ? getRuleMessage(rule, 'required') : '' }, max(value: any, rule: ValidateDescription) { const length = getLength(value) const max = Number(rule.max) - return length > max ? getMessage('max') : '' + return length > max ? getRuleMessage(rule, 'max') : '' }, maximum(value: any, rule: ValidateDescription) { return Number(value) > Number(rule.maximum) ? getMessage('maximum') : '' }, exclusiveMaximum(value: any, rule: ValidateDescription) { return Number(value) >= Number(rule.maximum) - ? getMessage('exclusiveMaximum') + ? getRuleMessage(rule, 'exclusiveMaximum') : '' }, minimum(value: any, rule: ValidateDescription) { - return Number(value) < Number(rule.minimum) ? getMessage('minimum') : '' + return Number(value) < Number(rule.minimum) + ? getRuleMessage(rule, 'minimum') + : '' }, exclusiveMinimum(value: any, rule: ValidateDescription) { return Number(value) <= Number(rule.minimum) - ? getMessage('exclusiveMinimum') + ? getRuleMessage(rule, 'exclusiveMinimum') : '' }, len(value: any, rule: ValidateDescription) { const length = getLength(value) const len = Number(rule.len) - return length !== len ? getMessage('len') : '' + return length !== len ? getRuleMessage(rule, 'len') : '' }, min(value: any, rule: ValidateDescription) { const length = getLength(value) const min = Number(rule.len) - return length < min ? getMessage('min') : '' + return length < min ? getRuleMessage(rule, 'min') : '' }, pattern(value: any, rule: ValidateDescription) { return !new RegExp(rule.pattern).test(value) - ? rule.message || getMessage('pattern') + ? getRuleMessage(rule, 'pattern') : '' }, async validator(value: any, rule: ValidateDescription) { @@ -77,11 +88,13 @@ export default { }, whitespace(value: any, rule: ValidateDescription) { if (rule.whitespace) { - return /^\s+$/.test(value) || value === '' ? getMessage('whitespace') : '' + return /^\s+$/.test(value) || value === '' + ? getRuleMessage(rule, 'whitespace') + : '' } }, enum(value: any, rule: ValidateDescription) { const enums = toArr(rule.enum) - return enums.indexOf(value) === -1 ? getMessage('enum') : '' + return enums.indexOf(value) === -1 ? getRuleMessage(rule, 'enum') : '' } } diff --git a/packages/validator/src/types.ts b/packages/validator/src/types.ts index 788d03ac6d3..4d2b5673e1c 100644 --- a/packages/validator/src/types.ts +++ b/packages/validator/src/types.ts @@ -48,10 +48,10 @@ export type ValidatePatternRules = export type CustomValidator = ( value: any, - rescription: ValidateDescription + rescription?: ValidateDescription ) => ValidateResponse -export type ValidateResponse = +export type SyncValidateResponse = | null | string | boolean @@ -60,6 +60,10 @@ export type ValidateResponse = message: string } +export type ValidateResponse = SyncValidateResponse | AsyncValidateResponse + +export type AsyncValidateResponse = Promise + export type ValidateRulesMap = { [key in string]: ( value: any, diff --git a/packages/validator/src/validator.ts b/packages/validator/src/validator.ts index e32c6267f6a..6ce515eb100 100644 --- a/packages/validator/src/validator.ts +++ b/packages/validator/src/validator.ts @@ -10,7 +10,8 @@ import { ValidateFieldOptions, ValidateCalculator, ValidateNode, - ValidateNodeResult + ValidateNodeResult, + SyncValidateResponse } from './types' import { isFn, @@ -32,7 +33,7 @@ const ValidatorRules: ValidateRulesMap = {} const ValidatorFormators: ValidateFormatsMap = {} //模板引擎 -const template = (message: ValidateResponse, context: any): string => { +const template = (message: SyncValidateResponse, context: any): string => { if (isStr(message)) { if (isFn(FormValidator.template)) { return FormValidator.template(message, context) diff --git a/scripts/global.ts b/scripts/global.ts index 0b28d7f2bf5..016f47cbd1c 100644 --- a/scripts/global.ts +++ b/scripts/global.ts @@ -1,5 +1,4 @@ import prettyFormat from 'pretty-format' -import '@testing-library/jest-dom/extend-expect' global['prettyFormat'] = prettyFormat From 7a93b736847490e1ab89ce6ca129873cb919a909 Mon Sep 17 00:00:00 2001 From: Janry Date: Sun, 10 Nov 2019 14:09:03 +0800 Subject: [PATCH 33/99] feat: support formStep component (#385) * test(@uform/react-schema-renderer): improve v0 test cases * feat(@uform/next/antd): support formStep --- docs/Examples/antd/Layout.md | 78 +++++++++++++++++++ docs/Examples/next/Layout.md | 77 ++++++++++++++++++ packages/antd/src/components/FormStep.tsx | 25 ++++-- packages/antd/src/components/FormTextBox.tsx | 33 ++++++-- packages/next/README.md | 2 +- packages/next/src/components/FormStep.tsx | 25 ++++-- packages/next/src/components/FormTextBox.tsx | 34 ++++++-- .../src/shared/schema.ts | 14 ++-- 8 files changed, 250 insertions(+), 38 deletions(-) diff --git a/docs/Examples/antd/Layout.md b/docs/Examples/antd/Layout.md index a19533af5f6..f60367b3517 100644 --- a/docs/Examples/antd/Layout.md +++ b/docs/Examples/antd/Layout.md @@ -268,3 +268,81 @@ const App = () => ( ) ReactDOM.render(, document.getElementById('root')) ``` + + +## 分步表单 + + +```jsx +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + FormEffectHooks, + createFormActions, + FormGridRow, + FormItemGrid, + FormGridCol, + FormPath, + FormLayout, + FormBlock, + FormCard, + FormTextBox, + FormStep +} from '@uform/antd' +import { Button } from 'antd' +import 'antd/dist/antd.css' + +const { onFormInit$ } = FormEffectHooks + +const actions = createFormActions() + +export default () => ( + { + console.log('提交') + console.log(values) + }} + actions={actions} + labelCol={{ span: 8 }} + wrapperCol={{ span: 6 }} + validateFirst + effects={({ setFieldState, getFormGraph }) => { + onFormInit$().subscribe(() => { + setFieldState('col1', state => { + state.visible = false + }) + }) + FormStep.useEffects(['step-1', 'step-2', 'step-3']) + }} + > + + + + + + + + + + + + 提交 + + + + +) +``` diff --git a/docs/Examples/next/Layout.md b/docs/Examples/next/Layout.md index 630e0e6666c..17f3c605a23 100644 --- a/docs/Examples/next/Layout.md +++ b/docs/Examples/next/Layout.md @@ -255,3 +255,80 @@ const App = () => ( ) ReactDOM.render(, document.getElementById('root')) ``` + +## 分步表单 + + +```jsx +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + FormEffectHooks, + createFormActions, + FormGridRow, + FormItemGrid, + FormGridCol, + FormPath, + FormLayout, + FormBlock, + FormCard, + FormTextBox, + FormStep +} from '@uform/next' +import { Button } from '@alifd/next' +import '@alifd/next/dist/next.css' + +const { onFormInit$ } = FormEffectHooks + +const actions = createFormActions() + +export default () => ( + { + console.log('提交') + console.log(values) + }} + actions={actions} + labelCol={{ span: 8 }} + wrapperCol={{ span: 6 }} + validateFirst + effects={({ setFieldState, getFormGraph }) => { + onFormInit$().subscribe(() => { + setFieldState('col1', state => { + state.visible = false + }) + }) + FormStep.useEffects(['step-1', 'step-2', 'step-3']) + }} + > + + + + + + + + + + + + 提交 + + + + +) +``` diff --git a/packages/antd/src/components/FormStep.tsx b/packages/antd/src/components/FormStep.tsx index 3f5ee35121a..0d8faa117b9 100644 --- a/packages/antd/src/components/FormStep.tsx +++ b/packages/antd/src/components/FormStep.tsx @@ -27,19 +27,21 @@ const EffectHooks = { }>(StateMap.ON_FORM_STEP_CURRENT_CHANGE) } -const effects = (relations: FormPathPattern[]) => { +const useEffects = (relations: FormPathPattern[]) => { const actions = createFormActions() return EffectHooks.onStepCurrentChange$().subscribe(({ value }) => { relations.forEach((pattern, index) => { - actions.setFieldState(pattern, (state: any) => { - state.display = index === value + setTimeout(()=>{ + actions.setFieldState(pattern, (state: any) => { + state.display = index === value + }) }) }) }) } type StepComponentExtendsProps = StateMap & { - getEffects: ( + useEffects: ( relations: FormPathPattern[] ) => Observable<{ value: number @@ -67,9 +69,16 @@ export const FormStep: React.FC & form.subscribe(({ type, payload }) => { switch (type) { case StateMap.ON_FORM_STEP_NEXT: - update( - ref.current + 1 > items.length - 1 ? ref.current : ref.current + 1 - ) + form.validate().then(({ errors }) => { + if (errors.length === 0) { + update( + ref.current + 1 > items.length - 1 + ? ref.current + : ref.current + 1 + ) + } + }) + break case StateMap.ON_FORM_STEP_PREVIOUS: update(ref.current - 1 < 0 ? ref.current : ref.current - 1) @@ -94,5 +103,5 @@ export const FormStep: React.FC & ) as any Object.assign(FormStep, StateMap, EffectHooks, { - effects + useEffects }) diff --git a/packages/antd/src/components/FormTextBox.tsx b/packages/antd/src/components/FormTextBox.tsx index 45cf18233ee..07bc59a314a 100644 --- a/packages/antd/src/components/FormTextBox.tsx +++ b/packages/antd/src/components/FormTextBox.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useEffect } from 'react' +import React, { useRef, useLayoutEffect } from 'react' import { createControllerBox } from '@uform/react-schema-renderer' import { IFormTextBox } from '../types' import { toArr } from '@uform/shared' @@ -27,15 +27,34 @@ export const FormTextBox = createControllerBox( const arrChildren = toArr(children) const split = text.split('%s') let index = 0 - useEffect(() => { + useLayoutEffect(() => { if (ref.current) { - const eles = ref.current.querySelectorAll('.text-box-field') - eles.forEach((el: HTMLElement) => { - const ctrl = el.querySelector('.next-form-item-control:first-child') - if (ctrl) { - el.style.width = getComputedStyle(ctrl).width + const elements = ref.current.querySelectorAll('.text-box-field') + const syncLayouts = Array.prototype.map.call( + elements, + (el: HTMLElement) => { + return [ + el, + () => { + const ctrl = el.querySelector( + '.ant-form-item-control:first-child' + ) + if (ctrl) { + el.style.width = ctrl.getBoundingClientRect().width + 'px' + } + } + ] } + ) + syncLayouts.forEach(([el, handler]) => { + el.addEventListener('DOMSubtreeModified', handler) }) + + return () => { + syncLayouts.forEach(([el, handler]) => { + el.removeEventListener('DOMSubtreeModified', handler) + }) + } } }, []) const newChildren = split.reduce((buf, item, key) => { diff --git a/packages/next/README.md b/packages/next/README.md index f7b7d78e396..dabde78f31b 100644 --- a/packages/next/README.md +++ b/packages/next/README.md @@ -43,7 +43,7 @@ export default () => ( state.visible = false }) }) - FormStep.effects(['step-1', 'step-2', 'step-3']) + FormStep.useEffects(['step-1', 'step-2', 'step-3']) }} > (StateMap.ON_FORM_STEP_CURRENT_CHANGE) } -const effects = (relations: FormPathPattern[]) => { +const useEffects = (relations: FormPathPattern[]) => { const actions = createFormActions() return EffectHooks.onStepCurrentChange$().subscribe(({ value }) => { relations.forEach((pattern, index) => { - actions.setFieldState(pattern, (state: any) => { - state.display = index === value + setTimeout(()=>{ + actions.setFieldState(pattern, (state: any) => { + state.display = index === value + }) }) }) }) } type StepComponentExtendsProps = StateMap & { - getEffects: ( + useEffects: ( relations: FormPathPattern[] ) => Observable<{ value: number @@ -67,9 +69,16 @@ export const FormStep: React.FC & form.subscribe(({ type, payload }) => { switch (type) { case StateMap.ON_FORM_STEP_NEXT: - update( - ref.current + 1 > items.length - 1 ? ref.current : ref.current + 1 - ) + form.validate().then(({ errors }) => { + if (errors.length === 0) { + update( + ref.current + 1 > items.length - 1 + ? ref.current + : ref.current + 1 + ) + } + }) + break case StateMap.ON_FORM_STEP_PREVIOUS: update(ref.current - 1 < 0 ? ref.current : ref.current - 1) @@ -94,5 +103,5 @@ export const FormStep: React.FC & ) as any Object.assign(FormStep, StateMap, EffectHooks, { - effects + useEffects }) diff --git a/packages/next/src/components/FormTextBox.tsx b/packages/next/src/components/FormTextBox.tsx index 45cf18233ee..84ced3ff485 100644 --- a/packages/next/src/components/FormTextBox.tsx +++ b/packages/next/src/components/FormTextBox.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useEffect } from 'react' +import React, { useRef, useLayoutEffect } from 'react' import { createControllerBox } from '@uform/react-schema-renderer' import { IFormTextBox } from '../types' import { toArr } from '@uform/shared' @@ -27,15 +27,34 @@ export const FormTextBox = createControllerBox( const arrChildren = toArr(children) const split = text.split('%s') let index = 0 - useEffect(() => { + useLayoutEffect(() => { if (ref.current) { - const eles = ref.current.querySelectorAll('.text-box-field') - eles.forEach((el: HTMLElement) => { - const ctrl = el.querySelector('.next-form-item-control:first-child') - if (ctrl) { - el.style.width = getComputedStyle(ctrl).width + const elements = ref.current.querySelectorAll('.text-box-field') + const syncLayouts = Array.prototype.map.call( + elements, + (el: HTMLElement) => { + return [ + el, + () => { + const ctrl = el.querySelector( + '.next-form-item-control:first-child' + ) + if (ctrl) { + el.style.width = ctrl.getBoundingClientRect().width + 'px' + } + } + ] } + ) + syncLayouts.forEach(([el, handler]) => { + el.addEventListener('DOMSubtreeModified', handler) }) + + return () => { + syncLayouts.forEach(([el, handler]) => { + el.removeEventListener('DOMSubtreeModified', handler) + }) + } } }, []) const newChildren = split.reduce((buf, item, key) => { @@ -99,6 +118,7 @@ export const FormTextBox = createControllerBox( } .preview-text { text-align: center !important; + white-space:nowrap; } ` ) diff --git a/packages/react-schema-renderer/src/shared/schema.ts b/packages/react-schema-renderer/src/shared/schema.ts index 1055b85f3e4..0d75ba16972 100644 --- a/packages/react-schema-renderer/src/shared/schema.ts +++ b/packages/react-schema-renderer/src/shared/schema.ts @@ -95,18 +95,18 @@ export class Schema implements ISchema { return this } let res: Schema = this - let suc = 0 - path = FormPath.parse(path) - path.forEach(key => { + let index = 0 + let newPath = FormPath.parse(path) + newPath.forEach(key => { if (res && !isEmpty(res.properties)) { - res = res.properties[key] - suc++ + const lastKey = newPath.segments.slice(index).join('.') + res = res.properties[key] || res.properties[lastKey] } else if (res && !isEmpty(res.items) && numberRE.test(key as string)) { res = isArr(res.items) ? res.items[key] : res.items - suc++ } + index++ }) - return suc === path.length ? res : undefined + return res } merge(props: {}) { From e49d7bea386b5826dfa04281c707fc32522aec6b Mon Sep 17 00:00:00 2001 From: Janry Date: Mon, 11 Nov 2019 11:26:15 +0800 Subject: [PATCH 34/99] Fix next item style (#388) * test(@uform/react-schema-renderer): improve v0 test cases * feat(@uform/next/antd): support formStep * fix(@uform/next): add formItem default labelAlign --- packages/next/src/compat/FormItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/src/compat/FormItem.tsx b/packages/next/src/compat/FormItem.tsx index db71dc8d82a..1c05d4a6be7 100644 --- a/packages/next/src/compat/FormItem.tsx +++ b/packages/next/src/compat/FormItem.tsx @@ -93,7 +93,7 @@ export const CompatNextFormItem: React.FC = props => { label={label} labelTextAlign={labelTextAlign} labelCol={label ? normalizeCol(labelCol) : undefined} - labelAlign={labelAlign} + labelAlign={labelAlign || 'left'} required={props.required} wrapperCol={label ? normalizeCol(wrapperCol) : undefined} size={size} From 646203c16a29c7a77ac2472b900d97be96eb1675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=B8=E6=B3=BD=E6=B8=85?= <835702580@qq.com> Date: Tue, 12 Nov 2019 22:55:31 +0800 Subject: [PATCH 35/99] test(@uform/react-schema-renderer): add old test case (#391) --- .../src/__tests__/virtualbox.spec.tsx | 162 ++++++++++++++++ .../src/__tests__/visible.spec.tsx | 179 ++++++++++++++++++ .../src/__tests__/x-component.spec.tsx | 59 ++++++ 3 files changed, 400 insertions(+) create mode 100644 packages/react-schema-renderer/src/__tests__/virtualbox.spec.tsx create mode 100644 packages/react-schema-renderer/src/__tests__/visible.spec.tsx create mode 100644 packages/react-schema-renderer/src/__tests__/x-component.spec.tsx diff --git a/packages/react-schema-renderer/src/__tests__/virtualbox.spec.tsx b/packages/react-schema-renderer/src/__tests__/virtualbox.spec.tsx new file mode 100644 index 00000000000..312453e5ed4 --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/virtualbox.spec.tsx @@ -0,0 +1,162 @@ +import React, { Fragment, useState } from 'react' +import { + registerFormField, + connect, + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, + registerFieldMiddleware, + createVirtualBox +} from '../index' +import { render, fireEvent, act, wait } from '@testing-library/react' + +let FormCard + +registerFieldMiddleware(Field => { + return props => { + if (typeof props.editable === 'boolean' && props.name !== '') { + if (!props.editable) return
    empty
    + } + return ( +
    + {props.schema.title} + + {props.errors && props.errors.length ? ( +
    {props.errors}
    + ) : ( + '' + )} +
    + ) + } +}) +registerFormField( + 'string', + connect()(props => ) +) +registerFormField('text', connect()(props =>
    This is Text Component
    )) + +FormCard = createVirtualBox('card', ({ children }) => { + return
    card content{children}
    +}) + +test('createVirtualBox', async () => { + const TestComponent = () => ( + + + + + + + + + ) + + const { queryByText } = render() + + await wait(); + expect(queryByText('card content')).toBeVisible() +}) + +test('dynamic node', async () => { + const TestComponent = () => { + const [editable, setEditable] = useState(false) + return ( + + + {editable && ( + + + + )} + + + + + + + ) + } + + const { queryByText } = render() + + await wait(); + fireEvent.click(queryByText('Change Editable')) + await wait(); + expect(queryByText('This is Text Component')).toBeVisible() +}) + +test('dynamic schema', async () => { + const TestComponent = () => { + const [schema, setSchema] = useState({ + type: 'object', + properties: { + card: { + type: 'object', + 'x-component': 'card', + properties: { + aa: { + type: 'string', + 'x-props': { + 'data-testid': 'aa' + } + }, + bb: { + type: 'string', + 'x-props': { + 'data-testid': 'bb' + } + } + } + } + } + }) + + const deleteProperty = () => + setSchema({ + type: 'object', + properties: { + card: { + type: 'object', + 'x-component': 'card', + properties: { + aa: { + type: 'string', + 'x-props': { + 'data-testid': 'aa', + disabled: true + } + } + // bb deleted + } + } + } + }) + return ( +
    + + +
    + ) + } + + const { queryByText, queryByTestId, baseElement } = render() + + await wait(); + act(() => { + fireEvent.click(queryByText('Delete')) + }) + await wait(); + expect(queryByTestId('aa').hasAttribute('disabled')).toBe(true) + expect(queryByTestId('aa')).toBeVisible() + expect(queryByTestId('bb')).toBeNull() +}) diff --git a/packages/react-schema-renderer/src/__tests__/visible.spec.tsx b/packages/react-schema-renderer/src/__tests__/visible.spec.tsx new file mode 100644 index 00000000000..6a2ec1a470d --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/visible.spec.tsx @@ -0,0 +1,179 @@ +import React, { Fragment } from 'react' +import { + registerFormField, + connect, + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, + createFormActions +} from '../index' +import { render, fireEvent, wait } from '@testing-library/react' + +registerFormField('string', connect()(props =>
    {props.value}
    )) + +test('visible is false will remove react node', async () => { + const actions = createFormActions() + const TestComponent = () => { + return ( + { + $('onFormInit').subscribe(() => { + setFieldState('aa', state => { + state.visible = false + }) + }) + }} + > + + + ) + } + + const { queryByText } = render() + + await wait() + expect(queryByText('123321')).toBeNull() + actions.setFieldState('aa', state => { + state.visible = true + }) + expect(queryByText('123321')).toBeVisible() +}) + +test('visible is false will remove react children node', async () => { + const TestComponent = () => { + return ( + { + $('onFormInit').subscribe(() => { + setFieldState('obj', state => { + state.visible = false + }) + }) + }} + > + + + + + ) + } + + const { queryByText } = render() + + await wait() + expect(queryByText('123321')).toBeNull() +}) + +test('visible is false will remove value(include default value)', async () => { + const onSubmitHandler = jest.fn() + const actions = createFormActions() + const TestComponent = () => { + return ( + { + $('onFieldChange', 'bb').subscribe(({ value }) => { + if (value === '123') { + setFieldState('obj', state => { + state.visible = false + }) + } + }) + }} + > + + + + + + + + + ) + } + + const { queryByText } = render() + + await wait() + expect(queryByText('123321')).toBeNull() + fireEvent.click(queryByText('Submit')) + await wait() + expect(onSubmitHandler).toHaveBeenCalledWith({ + bb: '123' + }) + await actions.reset() + fireEvent.click(queryByText('Submit')) + await wait() + expect(onSubmitHandler).toHaveBeenCalledWith({ + bb: '123' + }) +}) + +test('visible is false will not validate(include children)', async () => { + const onSubmitHandler = jest.fn() + const onValidateFailedHandler = jest.fn() + const actions = createFormActions() + const TestComponent = () => { + return ( + { + $('onFieldChange', 'bb').subscribe(({ value }) => { + if (value === '123') { + setFieldState('obj', state => { + state.visible = false + }) + } + }) + }} + > + + + + + + + + + ) + } + + const { queryByText } = render() + + await wait() + expect(queryByText('123321')).toBeNull() + fireEvent.click(queryByText('Submit')) + await wait() + expect(onSubmitHandler).toHaveBeenCalledWith({ + bb: '123' + }) + expect(onValidateFailedHandler).toHaveBeenCalledTimes(0) + await actions.reset() + fireEvent.click(queryByText('Submit')) + await wait() + expect(onSubmitHandler).toHaveBeenCalledWith({ + bb: '123' + }) + await actions.setFieldState('obj', state => { + state.visible = true + }) + fireEvent.click(queryByText('Submit')) + await wait() + expect(onSubmitHandler).toHaveBeenCalledWith({ + obj: { aa: '123321' }, + bb: '123' + }) + expect(onValidateFailedHandler).toHaveBeenCalledTimes(0) + await actions.reset() + fireEvent.click(queryByText('Submit')) + await wait() + expect(onSubmitHandler).toHaveBeenCalledWith({ + obj: { aa: '123321' }, + bb: '123' + }) +}) diff --git a/packages/react-schema-renderer/src/__tests__/x-component.spec.tsx b/packages/react-schema-renderer/src/__tests__/x-component.spec.tsx new file mode 100644 index 00000000000..81b8fc7e727 --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/x-component.spec.tsx @@ -0,0 +1,59 @@ +import React, { Fragment } from 'react' +import { + registerFormField, + connect, + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, + createFormActions, + registerFieldMiddleware +} from '../index' +import { render, wait } from '@testing-library/react' + +registerFieldMiddleware(Field => { + return props => { + if (typeof props.editable === 'boolean') { + if (!props.editable) return
    empty
    + } + return ( +
    + {props.schema.title} + + {props.errors && props.errors.length ? ( +
    {props.errors}
    + ) : ( + '' + )} +
    + ) + } +}) +registerFormField( + 'string', + connect()(props => ) +) +registerFormField('text', connect()(props =>
    text component
    )) + +test('update x-component by setFieldState', async () => { + const actions = createFormActions() + const TestComponent = () => ( + + + + + + + ) + + const { queryByText } = render() + + await wait(); + expect(queryByText('text component')).toBeNull() + await wait(); + actions.setFieldState('aaa', state => { + state.props['x-component'] = 'text' + }) + await wait(); + expect(queryByText('text component')).toBeVisible() +}) From b98526540808e040600cde35d4dd1a5979499c9f Mon Sep 17 00:00:00 2001 From: Janry Date: Wed, 13 Nov 2019 12:44:01 +0800 Subject: [PATCH 36/99] test(@uform/react-schema-renderer): Compat old test (#392) --- packages/antd/src/index.tsx | 4 +- packages/next/src/index.tsx | 4 +- .../src/__old_tests__/virtualbox.spec.js | 157 ---------------- .../src/__old_tests__/visible.spec.js | 174 ------------------ .../src/__old_tests__/x-component.spec.js | 56 ------ .../src/__tests__/virtualbox.spec.tsx | 12 +- .../src/__tests__/visible.spec.tsx | 1 + .../src/components/SchemaField.tsx | 116 ++++++------ .../src/shared/schema.ts | 2 +- packages/react-schema-renderer/src/types.ts | 2 +- 10 files changed, 73 insertions(+), 455 deletions(-) delete mode 100644 packages/react-schema-renderer/src/__old_tests__/virtualbox.spec.js delete mode 100644 packages/react-schema-renderer/src/__old_tests__/visible.spec.js delete mode 100644 packages/react-schema-renderer/src/__old_tests__/x-component.spec.js diff --git a/packages/antd/src/index.tsx b/packages/antd/src/index.tsx index 85d9569b902..7056bbbed78 100644 --- a/packages/antd/src/index.tsx +++ b/packages/antd/src/index.tsx @@ -10,8 +10,6 @@ export * from '@uform/react-schema-renderer' export * from './components' export * from './types' export { mapStyledProps, mapTextComponent } from './shared' -export const SchemaForm: React.FC< - INextSchemaFormProps -> = SchemaMarkupForm as any +export const SchemaForm: React.FC = SchemaMarkupForm as any export const Field: React.FC = SchemaMarkupField export default SchemaForm diff --git a/packages/next/src/index.tsx b/packages/next/src/index.tsx index 85d9569b902..7056bbbed78 100644 --- a/packages/next/src/index.tsx +++ b/packages/next/src/index.tsx @@ -10,8 +10,6 @@ export * from '@uform/react-schema-renderer' export * from './components' export * from './types' export { mapStyledProps, mapTextComponent } from './shared' -export const SchemaForm: React.FC< - INextSchemaFormProps -> = SchemaMarkupForm as any +export const SchemaForm: React.FC = SchemaMarkupForm as any export const Field: React.FC = SchemaMarkupField export default SchemaForm diff --git a/packages/react-schema-renderer/src/__old_tests__/virtualbox.spec.js b/packages/react-schema-renderer/src/__old_tests__/virtualbox.spec.js deleted file mode 100644 index bcb2345b5fd..00000000000 --- a/packages/react-schema-renderer/src/__old_tests__/virtualbox.spec.js +++ /dev/null @@ -1,157 +0,0 @@ -import React, { useState } from 'react' -import SchemaForm, { - Field, - registerFormField, - createVirtualBox, - connect, - registerFieldMiddleware -} from '../index' -import { render, fireEvent, act } from '@testing-library/react' - -let FormCard - -registerFieldMiddleware(Field => { - return props => { - if (typeof props.editable === 'boolean' && props.name !== '') { - if (!props.editable) return
    empty
    - } - return ( -
    - {props.schema.title} - - {props.errors && props.errors.length ? ( -
    {props.errors}
    - ) : ( - '' - )} -
    - ) - } -}) -registerFormField( - 'string', - connect()(props => ) -) -registerFormField('text', connect()(props =>
    This is Text Component
    )) - -FormCard = createVirtualBox('card', ({ children }) => { - return
    card content{children}
    -}) - -test('createVirtualBox', async () => { - const TestComponent = () => ( - - - - - - - ) - - const { queryByText } = render() - - await sleep(33) - expect(queryByText('card content')).toBeVisible() -}) - -test('dynamic node', async () => { - const TestComponent = () => { - const [editable, setEditable] = useState(false) - return ( - - {editable && ( - - - - )} - - - - - - ) - } - - const { queryByText } = render() - - await sleep(33) - fireEvent.click(queryByText('Change Editable')) - await sleep(33) - expect(queryByText('This is Text Component')).toBeVisible() -}) - -test('dynamic schema', async () => { - const TestComponent = () => { - const [schema, setSchema] = useState({ - type: 'object', - properties: { - card: { - type: 'object', - 'x-component': 'card', - properties: { - aa: { - type: 'string', - 'x-props': { - 'data-testid': 'aa' - } - }, - bb: { - type: 'string', - 'x-props': { - 'data-testid': 'bb' - } - } - } - } - } - }) - - const deleteProperty = () => - setSchema({ - type: 'object', - properties: { - card: { - type: 'object', - 'x-component': 'card', - properties: { - aa: { - type: 'string', - 'x-props': { - 'data-testid': 'aa', - disabled: true - } - } - // bb deleted - } - } - } - }) - return ( -
    - - -
    - ) - } - - const { queryByText, queryByTestId, baseElement } = render() - - await sleep(33) - act(() => { - fireEvent.click(queryByText('Delete')) - }) - await sleep(33) - expect(queryByTestId('aa').hasAttribute('disabled')).toBe(true) - expect(queryByTestId('aa')).toBeVisible() - expect(queryByTestId('bb')).toBeNull() -}) diff --git a/packages/react-schema-renderer/src/__old_tests__/visible.spec.js b/packages/react-schema-renderer/src/__old_tests__/visible.spec.js deleted file mode 100644 index d02632bffff..00000000000 --- a/packages/react-schema-renderer/src/__old_tests__/visible.spec.js +++ /dev/null @@ -1,174 +0,0 @@ -import React from 'react' -import SchemaForm, { - Field, - registerFormField, - connect, - createFormActions -} from '../index' -import { render, fireEvent } from '@testing-library/react' - -registerFormField('string', connect()(props =>
    {props.value}
    )) - -test('visible is false will remove react node', async () => { - const actions = createFormActions() - const TestComponent = () => { - return ( - { - $('onFormInit').subscribe(() => { - setFieldState('aa', state => { - state.visible = false - }) - }) - }} - > - - - ) - } - - const { queryByText } = render() - - await sleep(33) - expect(queryByText('123321')).toBeNull() - await actions.setFieldState('aa', state => { - state.visible = true - }) - expect(queryByText('123321')).toBeVisible() -}) - -test('visible is false will remove react children node', async () => { - const TestComponent = () => { - return ( - { - $('onFormInit').subscribe(() => { - setFieldState('obj', state => { - state.visible = false - }) - }) - }} - > - - - - - ) - } - - const { queryByText } = render() - - await sleep(33) - expect(queryByText('123321')).toBeNull() -}) - -test('visible is false will remove value(include default value)', async () => { - const onSubmitHandler = jest.fn() - const actions = createFormActions() - const TestComponent = () => { - return ( - { - $('onFieldChange', 'bb').subscribe(({ value }) => { - if (value === '123') { - setFieldState('obj', state => { - state.visible = false - }) - } - }) - }} - > - - - - - - - ) - } - - const { queryByText } = render() - - await sleep(33) - expect(queryByText('123321')).toBeNull() - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(onSubmitHandler).toHaveBeenCalledWith({ - bb: '123' - }) - await actions.reset() - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(onSubmitHandler).toHaveBeenCalledWith({ - bb: '123' - }) -}) - -test('visible is false will not validate(include children)', async () => { - const onSubmitHandler = jest.fn() - const onValidateFailedHandler = jest.fn() - const actions = createFormActions() - const TestComponent = () => { - return ( - { - $('onFieldChange', 'bb').subscribe(({ value }) => { - if (value === '123') { - setFieldState('obj', state => { - state.visible = false - }) - } - }) - }} - > - - - - - - - ) - } - - const { queryByText } = render() - - await sleep(33) - expect(queryByText('123321')).toBeNull() - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(onSubmitHandler).toHaveBeenCalledWith({ - bb: '123' - }) - expect(onValidateFailedHandler).toHaveBeenCalledTimes(0) - await actions.reset() - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(onSubmitHandler).toHaveBeenCalledWith({ - bb: '123' - }) - await actions.setFieldState('obj', state => { - state.visible = true - }) - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(onSubmitHandler).toHaveBeenCalledWith({ - obj: { aa: '123321' }, - bb: '123' - }) - expect(onValidateFailedHandler).toHaveBeenCalledTimes(0) - await actions.reset() - fireEvent.click(queryByText('Submit')) - await sleep(33) - expect(onSubmitHandler).toHaveBeenCalledWith({ - obj: { aa: '123321' }, - bb: '123' - }) -}) diff --git a/packages/react-schema-renderer/src/__old_tests__/x-component.spec.js b/packages/react-schema-renderer/src/__old_tests__/x-component.spec.js deleted file mode 100644 index 4eec77f582a..00000000000 --- a/packages/react-schema-renderer/src/__old_tests__/x-component.spec.js +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react' -import SchemaForm, { - Field, - registerFormField, - connect, - registerFieldMiddleware, - createFormActions -} from '../index' -import { render } from '@testing-library/react' - -registerFieldMiddleware(Field => { - return props => { - if (typeof props.editable === 'boolean') { - if (!props.editable) return
    empty
    - } - return ( -
    - {props.schema.title} - - {props.errors && props.errors.length ? ( -
    {props.errors}
    - ) : ( - '' - )} -
    - ) - } -}) -registerFormField( - 'string', - connect()(props => ) -) -registerFormField('text', connect()(props =>
    text component
    )) - -test('update x-component by setFieldState', async () => { - const actions = createFormActions() - const TestComponent = () => ( - - - - - ) - - const { queryByText } = render() - - await sleep(33) - expect(queryByText('text component')).toBeNull() - await sleep(33) - actions.setFieldState('aaa', state => { - state.props['x-component'] = 'text' - }) - await sleep(33) - expect(queryByText('text component')).toBeVisible() -}) diff --git a/packages/react-schema-renderer/src/__tests__/virtualbox.spec.tsx b/packages/react-schema-renderer/src/__tests__/virtualbox.spec.tsx index 312453e5ed4..fdc2ffb54c3 100644 --- a/packages/react-schema-renderer/src/__tests__/virtualbox.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/virtualbox.spec.tsx @@ -55,7 +55,7 @@ test('createVirtualBox', async () => { const { queryByText } = render() - await wait(); + await wait() expect(queryByText('card content')).toBeVisible() }) @@ -89,9 +89,9 @@ test('dynamic node', async () => { const { queryByText } = render() - await wait(); + await wait() fireEvent.click(queryByText('Change Editable')) - await wait(); + await wait() expect(queryByText('This is Text Component')).toBeVisible() }) @@ -149,13 +149,13 @@ test('dynamic schema', async () => { ) } - const { queryByText, queryByTestId, baseElement } = render() + const { queryByText, queryByTestId } = render() - await wait(); + await wait() act(() => { fireEvent.click(queryByText('Delete')) }) - await wait(); + await wait() expect(queryByTestId('aa').hasAttribute('disabled')).toBe(true) expect(queryByTestId('aa')).toBeVisible() expect(queryByTestId('bb')).toBeNull() diff --git a/packages/react-schema-renderer/src/__tests__/visible.spec.tsx b/packages/react-schema-renderer/src/__tests__/visible.spec.tsx index 6a2ec1a470d..0eff487dccc 100644 --- a/packages/react-schema-renderer/src/__tests__/visible.spec.tsx +++ b/packages/react-schema-renderer/src/__tests__/visible.spec.tsx @@ -36,6 +36,7 @@ test('visible is false will remove react node', async () => { actions.setFieldState('aa', state => { state.visible = true }) + await wait() expect(queryByText('123321')).toBeVisible() }) diff --git a/packages/react-schema-renderer/src/components/SchemaField.tsx b/packages/react-schema-renderer/src/components/SchemaField.tsx index 1de023ab5d3..379ac3c4b0a 100644 --- a/packages/react-schema-renderer/src/components/SchemaField.tsx +++ b/packages/react-schema-renderer/src/components/SchemaField.tsx @@ -1,6 +1,6 @@ import React, { useContext, Fragment } from 'react' -import { Field, VirtualField,IFieldState } from '@uform/react' -import { FormPath, isFn, isStr,isEqual } from '@uform/shared' +import { Field, VirtualField, IFieldState } from '@uform/react' +import { FormPath, isFn, isStr, isEqual } from '@uform/shared' import { ISchemaFieldProps, ISchemaFieldComponentProps, @@ -9,7 +9,7 @@ import { import { Schema } from '../shared/schema' import SchemaContext, { FormComponentsContext } from '../shared/context' -const computeSchemaState = (draft:IFieldState,prevState:IFieldState)=>{ +const computeSchemaState = (draft: IFieldState, prevState: IFieldState) => { const schema = new Schema(draft.props) const prevSchema = new Schema(prevState.props) const currentRequired = schema.getExtendsRequired() @@ -18,13 +18,13 @@ const computeSchemaState = (draft:IFieldState,prevState:IFieldState)=>{ const prevRules = prevSchema.getExtendsRules() const currentEditable = schema.getExtendsEditable() const prevEditable = prevSchema.getExtendsEditable() - if(!isEqual(currentRequired,prevRequired)){ + if (!isEqual(currentRequired, prevRequired)) { draft.required = currentRequired } - if(!isEqual(currentRules,prevRules)){ + if (!isEqual(currentRules, prevRules)) { draft.rules = currentRules } - if(!isEqual(currentEditable,prevEditable)){ + if (!isEqual(currentEditable, prevEditable)) { draft.selfEditable = currentEditable } } @@ -45,14 +45,14 @@ export const SchemaField: React.FunctionComponent = ( const schemaType = fieldSchema.type const schemaComponent = fieldSchema.getExtendsComponent() const schemaRenderer = fieldSchema.getExtendsRenderer() - const finalComponentName = schemaComponent || schemaType + const initialComponent = schemaComponent || schemaType const renderField = ( addtionKey: string | number, reactKey?: string | number ) => { return } - const renderChildren = ( + const renderFieldDelegate = ( callback: (props: ISchemaFieldComponentProps) => React.ReactElement ) => { return ( @@ -79,6 +79,31 @@ export const SchemaField: React.FunctionComponent = ( ) } + + const renderVirtualFieldDelegate = ( + callback: (props: ISchemaVirtualFieldComponentProps) => React.ReactElement + ) => { + return ( + + {({ state, form }) => { + const props: ISchemaVirtualFieldComponentProps = { + ...state, + schema: fieldSchema.merge(state.props), + form, + renderField, + children: fieldSchema.mapProperties( + (schema: Schema, key: string) => { + return + } + ) + } + + return callback(props) + }} + + ) + } + if (fieldSchema.isObject() && !schemaComponent) { const properties = fieldSchema.mapProperties( (schema: Schema, key: string) => { @@ -89,7 +114,7 @@ export const SchemaField: React.FunctionComponent = ( if (path.length == 0) { return {properties} } - return renderChildren(props => { + return renderFieldDelegate(props => { const renderComponent = () => { return React.createElement( formRegistry.formItemComponent, @@ -103,64 +128,47 @@ export const SchemaField: React.FunctionComponent = ( return renderComponent() }) } else { - if (isFn(finalComponentName)) { - return renderChildren(props => { - return React.createElement( - formRegistry.formItemComponent, - props, - React.createElement(finalComponentName, props) - ) - }) - } else if (isStr(finalComponentName)) { - if (formRegistry.fields[finalComponentName]) { - return renderChildren(props => { + if (isStr(initialComponent)) { + if (formRegistry.fields[initialComponent]) { + return renderFieldDelegate(props => { + const stateComponent = + props.schema.getExtendsComponent() || props.schema.type const renderComponent = (): React.ReactElement => React.createElement( formRegistry.formItemComponent, props, - React.createElement( - formRegistry.fields[finalComponentName], - props - ) + isStr(stateComponent) + ? React.createElement( + formRegistry.fields[stateComponent], + props + ) + : React.createElement(stateComponent, props) ) if (isFn(schemaRenderer)) { return schemaRenderer({ ...props, renderComponent }) } return renderComponent() }) - } else if (formRegistry.virtualFields[finalComponentName]) { - return ( - - {({ state, form }) => { - const props: ISchemaVirtualFieldComponentProps = { - ...state, - schema: fieldSchema.merge(state.props), - form, - renderField, - children: fieldSchema.mapProperties( - (schema: Schema, key: string) => { - const childPath = path.concat(key) - return - } - ) - } - const renderComponent = () => - React.createElement( - formRegistry.virtualFields[finalComponentName], + } else if (formRegistry.virtualFields[initialComponent]) { + return renderVirtualFieldDelegate(props => { + const stateComponent = + props.schema.getExtendsComponent() || props.schema.type + const renderComponent = () => + isStr(stateComponent) + ? React.createElement( + formRegistry.virtualFields[stateComponent], props ) - if (isFn(schemaRenderer)) { - return schemaRenderer({ ...props, renderComponent }) - } - return renderComponent() - }} - - ) - } else { - throw new Error( - `Can not found any custom component in ${path.toString()}.` - ) + : React.createElement(stateComponent, props) + + if (isFn(schemaRenderer)) { + return schemaRenderer({ ...props, renderComponent }) + } + return renderComponent() + }) } + } else { + throw new Error(`Can not found any form components.`) } } } diff --git a/packages/react-schema-renderer/src/shared/schema.ts b/packages/react-schema-renderer/src/shared/schema.ts index 0d75ba16972..3716c7d996d 100644 --- a/packages/react-schema-renderer/src/shared/schema.ts +++ b/packages/react-schema-renderer/src/shared/schema.ts @@ -64,7 +64,7 @@ export class Schema implements ISchema { public ['x-props']?: { [name: string]: any } public ['x-index']?: number public ['x-rules']?: ValidatePatternRules - public ['x-component']?: string | React.JSXElementConstructor + public ['x-component']?: string public ['x-component-props']?: { [name: string]: any } public ['x-render']?: ( props: T & { diff --git a/packages/react-schema-renderer/src/types.ts b/packages/react-schema-renderer/src/types.ts index 6ffedd1265a..5d36dccae96 100644 --- a/packages/react-schema-renderer/src/types.ts +++ b/packages/react-schema-renderer/src/types.ts @@ -113,7 +113,7 @@ export interface ISchema { ['x-props']?: { [name: string]: any } ['x-index']?: number ['x-rules']?: ValidatePatternRules - ['x-component']?: string | React.JSXElementConstructor + ['x-component']?: string ['x-component-props']?: { [name: string]: any } ['x-render']?: ( props: T & { From 21ee40b157b53529ff231d13ecdd3b54ee6f8e88 Mon Sep 17 00:00:00 2001 From: anyuxuan Date: Thu, 14 Nov 2019 10:23:05 +0800 Subject: [PATCH 37/99] feat: add react/actions tests --- packages/react/src/__tests__/actions.spec.tsx | 400 +++++++++++++++++- scripts/jest.base.js | 5 +- 2 files changed, 395 insertions(+), 10 deletions(-) diff --git a/packages/react/src/__tests__/actions.spec.tsx b/packages/react/src/__tests__/actions.spec.tsx index cfbcecec706..86e253f63b0 100644 --- a/packages/react/src/__tests__/actions.spec.tsx +++ b/packages/react/src/__tests__/actions.spec.tsx @@ -1,21 +1,403 @@ -describe('test all apis',()=>{ - //todo - test('basic',()=>{ - //todo +import React from 'react' +import { render, fireEvent, RenderResult } from '@testing-library/react' +import { + Form, + Field, + createFormActions, + createAsyncFormActions, + IFieldProps +} from '../index' +import { IFormActions, IFormAsyncActions } from '../types' + +const Input: React.FC = props => ( + + {({ state, mutators }) => ( +
    + +
    {state.errors}
    +
    {state.warnings}
    +
    + )} +
    +) + +describe('test all apis', () => { + const ACTIONS = [ + 'submit', + 'reset', + 'validate', + 'setFormState', + 'getFormState', + 'setFieldState', + 'getFieldState', + 'getFormGraph', + 'setFormGraph', + 'subscribe', + 'unsubscribe', + 'notify', + 'dispatch', + 'setFieldValue', + 'getFieldValue', + 'setFieldInitialValue', + 'getFieldInitialValue' + ] + + let actions: IFormActions + let asyncActions: IFormAsyncActions + let onSubmitHandler: any + let onResetHandler: any + let onValidateFailedHandler: any + let onChangeHandler: any + + const renderForm = (isAsync = false): RenderResult => + render( +
    { + $('onChangeFieldValue').subscribe(({ name, value }) => { + setFieldState(name, state => { + state.value = value + }) + }) + }} + > + +
    + ) + + beforeAll(() => { + actions = createFormActions() + asyncActions = createAsyncFormActions() + onSubmitHandler = jest.fn() + onResetHandler = jest.fn() + onValidateFailedHandler = jest.fn() + onChangeHandler = jest.fn() + }) + + test('createFormActions', () => { + expect(Object.keys(actions)).toEqual(expect.arrayContaining(ACTIONS)) + }) + + test('createAsyncFormActions', () => { + expect(Object.keys(asyncActions)).toEqual(expect.arrayContaining(ACTIONS)) + }) + + test('submit', async () => { + const { queryByTestId } = renderForm() + const inputEle = queryByTestId('input') + const errorsEle = queryByTestId('field-errors') + try { + await actions.submit() + } catch (e) { + expect(e).toEqual([{ path: 'aaa', messages: ['This field is required'] }]) + expect(errorsEle).toBeTruthy() + } + fireEvent.change(inputEle, { target: { value: '123' } }) + await actions.submit() + expect(onSubmitHandler).toBeCalledWith({ aaa: '123' }) + }) + + test('async submit', async () => { + const { queryByTestId } = renderForm(true) + const inputEle = queryByTestId('input') + const errorsEle = queryByTestId('field-errors') + try { + await asyncActions.submit() + } catch (e) { + expect(e).toEqual([{ path: 'aaa', messages: ['This field is required'] }]) + expect(errorsEle).toBeTruthy() + } + fireEvent.change(inputEle, { target: { value: '123' } }) + expect(onSubmitHandler).toBeCalledWith({ aaa: '123' }) + }) + + test('reset', () => { + renderForm() + actions.reset() + expect(onResetHandler).toBeCalled() + }) + + test('async reset', async () => { + renderForm(true) + await asyncActions.reset() + expect(onResetHandler).toBeCalled() + }) + + test('validate', async () => { + const { queryByTestId } = renderForm() + const errorsEle = queryByTestId('field-errors') + await actions.validate() + expect(onValidateFailedHandler).toBeCalledWith({ + errors: [{ path: 'aaa', messages: ['This field is required'] }], + warnings: [] + }) + expect(errorsEle).toBeTruthy() + }) + + test('async valid', async () => { + const { queryByTestId } = renderForm(true) + const errorsEle = queryByTestId('field-errors') + await asyncActions.validate() + expect(onValidateFailedHandler).toBeCalledWith({ + errors: [{ path: 'aaa', messages: ['This field is required'] }], + warnings: [] + }) + expect(errorsEle).toBeTruthy() + }) + + test('setFormState', () => { + renderForm() + const fn = jest.fn().mockImplementation(formState => { + formState.values.aaa = '123' + }) + actions.setFormState(fn) + expect(fn).toBeCalled() + }) + + test('async setFormState', async () => { + renderForm(true) + const fn = jest.fn().mockImplementation(formState => { + formState.values.aaa = '123' + }) + asyncActions.setFormState(fn) + expect(fn).toBeCalled() + }) + + test('getFormState', () => { + const { queryByTestId } = renderForm() + const inputEle = queryByTestId('input') + fireEvent.change(inputEle, { target: { value: '123' } }) + const formState = actions.getFormState() + expect(formState.values.aaa).toEqual('123') + }) + + test('async getFormState', async () => { + const { queryByTestId } = renderForm(true) + const inputEle = queryByTestId('input') + fireEvent.change(inputEle, { target: { value: '123' } }) + const formState = await asyncActions.getFormState() + expect(formState.values.aaa).toEqual('123') + }) + + test('setFieldState', () => { + renderForm() + const fn = jest.fn().mockImplementation(fieldState => { + fieldState.value = '123' + }) + actions.setFieldState('aaa', fn) + expect(fn).toBeCalled() + const formState = actions.getFormState() + expect(formState.values.aaa).toEqual('123') + }) + + test('async setFieldState', async () => { + renderForm(true) + const fn = jest.fn().mockImplementation(fieldState => { + fieldState.value = '123' + }) + await asyncActions.setFieldState('aaa', fn) + expect(fn).toBeCalled() + const formState = await asyncActions.getFormState() + expect(formState.values.aaa).toEqual('123') + }) + + test('getFieldState', async () => { + const { queryByTestId } = renderForm() + const inputEle = queryByTestId('input') + fireEvent.change(inputEle, { target: { value: '123' } }) + const fieldState = actions.getFieldState('aaa') + expect(fieldState.value).toEqual('123') + }) + + test('async getFieldState', async () => { + const { queryByTestId } = renderForm(true) + const inputEle = queryByTestId('input') + fireEvent.change(inputEle, { target: { value: '123' } }) + const fieldState = await asyncActions.getFieldState('aaa') + expect(fieldState.value).toEqual('123') + }) + + test('subscribe', () => { + renderForm() + const callAction = () => + new Promise(resolve => { + const fn = jest.fn().mockImplementation(params => resolve(params)) + actions.subscribe(fn) + }) + expect(callAction()).resolves.toEqual( + expect.objectContaining({ + type: expect.any(String), + payload: expect.any(Object) + }) + ) + }) + + test('async subscribe', () => { + renderForm(true) + const callAsyncAction = () => + new Promise(async resolve => { + const fn = jest.fn().mockImplementation(params => resolve(params)) + await asyncActions.subscribe(fn) + }) + expect(callAsyncAction()).resolves.toEqual( + expect.objectContaining({ + type: expect.any(String), + payload: expect.any(Object) + }) + ) + }) + + test('unsubscribe', () => { + renderForm() + expect.assertions(0) + const fn = jest.fn().mockImplementation(params => { + expect(params).toEqual( + expect.objectContaining({ + type: expect.any(String), + payload: expect.any(Object) + }) + ) + }) + const subscribeId = actions.subscribe(fn) + actions.unsubscribe(subscribeId) + }) + + test('async unsubscribe', async () => { + renderForm(true) + expect.assertions(0) + const fn = jest.fn().mockImplementation(params => { + expect(params).toEqual( + expect.objectContaining({ + type: expect.any(String), + payload: expect.any(Object) + }) + ) + }) + const subscribeId = await asyncActions.subscribe(fn) + await asyncActions.unsubscribe(subscribeId) + }) + + test('notify', () => { + renderForm() + expect.assertions(1) + const fn = jest.fn().mockImplementation(params => { + const { type, payload } = params + if (type === 'onTest') { + expect(payload).toEqual({ aaa: '123' }) + } + }) + actions.subscribe(fn) + actions.notify('onTest', { aaa: '123' }) + }) + + test('async notify', async () => { + renderForm(true) + expect.assertions(1) + const fn = jest.fn().mockImplementation(params => { + const { type, payload } = params + if (type === 'onTest') { + expect(payload).toEqual({ aaa: '123' }) + } + }) + await asyncActions.subscribe(fn) + await asyncActions.notify('onTest', { aaa: '123' }) + }) + + test('dispatch', () => { + renderForm() + actions.dispatch('onChangeFieldValue', { name: 'aaa', value: '123' }) + const fieldState = actions.getFieldState('aaa') + expect(fieldState.value).toEqual('123') + }) + + test('async dispatch', async () => { + renderForm(true) + asyncActions.dispatch('onChangeFieldValue', { + name: 'aaa', + value: '123' + }) + const fieldState = await asyncActions.getFieldState('aaa') + expect(fieldState.value).toEqual('123') + }) + + test('setFieldValue', () => { + renderForm() + actions.setFieldValue('aaa', '123') + const fieldState = actions.getFieldState('aaa') + expect(fieldState.value).toEqual('123') + }) + + test('async setFieldValue', async () => { + renderForm(true) + await asyncActions.setFieldValue('aaa', '123') + const fieldState = await asyncActions.getFieldState('aaa') + expect(fieldState.value).toEqual('123') + }) + + test('getFieldValue', () => { + renderForm() + actions.setFieldValue('aaa', '123') + const fieldValue = actions.getFieldValue('aaa') + expect(fieldValue).toEqual('123') + }) + + test('async getFieldValue', async () => { + renderForm(true) + await asyncActions.setFieldValue('aaa', '123') + const fieldValue = await asyncActions.getFieldValue('aaa') + expect(fieldValue).toEqual('123') + }) + + test('setFieldInitialValue', () => { + renderForm() + actions.setFieldInitialValue('aaa', '456') + const fieldInitialValue = actions.getFieldState('aaa') + expect(fieldInitialValue.initialValue).toEqual('456') + }) + + test('async setFieldInitialValue', async () => { + renderForm(true) + await asyncActions.setFieldInitialValue('aaa', '456') + const fieldInitialValue = await asyncActions.getFieldState('aaa') + expect(fieldInitialValue.initialValue).toEqual('456') + }) + + test('getFieldInitialValue', () => { + renderForm() + actions.setFieldInitialValue('aaa', '456') + const fieldInitialValue = actions.getFieldInitialValue('aaa') + expect(fieldInitialValue).toEqual('456') + }) + + test('async getFieldInitialValue', async () => { + renderForm(true) + await asyncActions.setFieldInitialValue('aaa', '456') + const fieldInitialValue = await asyncActions.getFieldInitialValue('aaa') + expect(fieldInitialValue).toEqual('456') }) }) -describe('major scenes',()=>{ +describe('major scenes', () => { //todo - test('basic',()=>{ + test('basic', () => { //todo }) }) - -describe('bugfix',()=>{ +describe('bugfix', () => { //todo - test('basic',()=>{ + test('basic', () => { //todo }) }) \ No newline at end of file diff --git a/scripts/jest.base.js b/scripts/jest.base.js index 3487b2e06e1..a8ffec2e676 100644 --- a/scripts/jest.base.js +++ b/scripts/jest.base.js @@ -32,7 +32,10 @@ module.exports = { globals: { 'ts-jest': { babelConfig: true, - tsConfig: 'tsconfig.jest.json' + tsConfig: 'tsconfig.jest.json', + diagnostics: { + pathRegex: '\\.(spec|test)\\.(ts|tsx|js|jsx)$' + } } }, //watchPlugins: ['jest-watch-lerna-packages'], From fa7bae74ea201e6206a06b2aafcc82a79b603f2b Mon Sep 17 00:00:00 2001 From: quirkyvar Date: Thu, 14 Nov 2019 10:42:17 +0800 Subject: [PATCH 38/99] feature(@uform/react): add useForm unittest (#393) --- package.json | 3 + .../react/src/__tests__/useDirty.spec.tsx | 69 +++++-- .../src/__tests__/useForceUpdate.spec.tsx | 48 +++-- packages/react/src/__tests__/useForm.spec.tsx | 168 ++++++++++++++++-- packages/react/src/hooks/useDirty.ts | 2 +- packages/react/src/hooks/useForm.ts | 1 + 6 files changed, 246 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index 4490528b021..be354c04d7f 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "sort-api": "node ./scripts/sort-api-table.js", "test": "npm run lint && jest", "test:core": "jest --watch packages/core/src/__tests__/*.spec.ts", + "test:hook": "jest --watch packages/react/src/__tests__/*.spec.ts", "test:prod": "cross-env TEST_ENV=production npm run build && jest", "doc:core": "doc-scripts start -i packages/core", "doc:react": "doc-scripts start -i packages/react", @@ -53,6 +54,7 @@ "@babel/runtime-corejs3": "^7.2.0", "@testing-library/jest-dom": "^4.2.3", "@testing-library/react": "^8.0.0", + "@testing-library/react-hooks": "^3.2.1", "@types/jest": "^24.0.18", "@types/node": "^12.6.8", "@typescript-eslint/eslint-plugin": "^1.11.0", @@ -103,6 +105,7 @@ "react": "^16.8.3", "react-dom": "^16.8.3", "react-eva": "^1.1.7", + "react-test-renderer": "^16.11.0", "remark-parse": "^6.0.3", "remark-stringify": "^6.0.4", "scheduler": "^0.15.0", diff --git a/packages/react/src/__tests__/useDirty.spec.tsx b/packages/react/src/__tests__/useDirty.spec.tsx index cfbcecec706..3e6603b7f78 100644 --- a/packages/react/src/__tests__/useDirty.spec.tsx +++ b/packages/react/src/__tests__/useDirty.spec.tsx @@ -1,21 +1,62 @@ -describe('test all apis',()=>{ - //todo - test('basic',()=>{ - //todo +import { renderHook } from '@testing-library/react-hooks' +import { useDirty } from '../hooks/useDirty' + +describe('useDirty hook',()=>{ + test('empty input without fields', ()=>{ + const { result } = renderHook(() => useDirty()) + expect(result.current).toEqual({ + num: 0, + dirtys: {}, + data: {}, + }) + }) + + test('empty input with fields', ()=>{ + const { result } = renderHook(() => useDirty({}, ['username', 'age'])) + expect(result.current).toEqual({ + num: 0, + dirtys: { + username: false, + age: false, + }, + data: {}, + }) }) -}) -describe('major scenes',()=>{ - //todo - test('basic',()=>{ - //todo + test('initial input', ()=>{ + const { result } = renderHook(() => useDirty({ username: 'billy', age: 21 }, ['username', 'age'])) + expect(result.current).toEqual({ + num: 0, + dirtys: { + username: false, + age: false, + }, + data: { username: 'billy', age: 21 }, + }) }) -}) + test('fields change', ()=>{ + let dirtyProps = { username: 'billy', age: 21 } + const { result, rerender } = renderHook(() => useDirty(dirtyProps, ['username', 'age'])) + expect(result.current).toEqual({ + num: 0, + dirtys: { + username: false, + age: false, + }, + data: dirtyProps, + }) + + dirtyProps = { username: 'lily', age: 21 } as any + rerender() -describe('bugfix',()=>{ - //todo - test('basic',()=>{ - //todo + expect(result.current).toEqual({ + num: 1, + dirtys: { + username: true, + age: false + }, + data: dirtyProps, + }) }) }) \ No newline at end of file diff --git a/packages/react/src/__tests__/useForceUpdate.spec.tsx b/packages/react/src/__tests__/useForceUpdate.spec.tsx index cfbcecec706..868af5ef5f0 100644 --- a/packages/react/src/__tests__/useForceUpdate.spec.tsx +++ b/packages/react/src/__tests__/useForceUpdate.spec.tsx @@ -1,21 +1,37 @@ -describe('test all apis',()=>{ - //todo - test('basic',()=>{ - //todo - }) -}) +import { renderHook, act } from '@testing-library/react-hooks' +import { useForceUpdate } from '../hooks/useForceUpdate' +import { useEffect } from 'react' -describe('major scenes',()=>{ - //todo - test('basic',()=>{ - //todo - }) -}) +describe('useForceUpdate hook',()=>{ + test('instance depency', ()=>{ + const events = [] + const onEvent = (cb) => { + events.push(cb) + } + const emitEvents = () => { + events.forEach(cb => cb()) + } + + const instance = { count: 0 } + function useDemo() { + const forceUpdate = useForceUpdate() + useEffect(() => { + onEvent(() => { + forceUpdate() + }) + }, []) + return instance.count + } + + const { result } = renderHook(() => useDemo()) + expect(result.current).toEqual(0) + instance.count = 1 + expect(result.current).toEqual(0) -describe('bugfix',()=>{ - //todo - test('basic',()=>{ - //todo + act(() => { + emitEvents() + }) + expect(result.current).toEqual(1) }) }) \ No newline at end of file diff --git a/packages/react/src/__tests__/useForm.spec.tsx b/packages/react/src/__tests__/useForm.spec.tsx index cfbcecec706..4507b2bd103 100644 --- a/packages/react/src/__tests__/useForm.spec.tsx +++ b/packages/react/src/__tests__/useForm.spec.tsx @@ -1,21 +1,161 @@ -describe('test all apis',()=>{ - //todo - test('basic',()=>{ - //todo +import React from 'react' +import { renderHook } from '@testing-library/react-hooks' +import { Broadcast } from '../shared'; +import { BroadcastContext } from '../context'; +import { createForm } from '@uform/core' +import useForm from '../hooks/useForm' +import { createFormActions, createAsyncFormActions } from '..'; + + +describe('useForm hook',()=>{ + test('return createForm instance', ()=>{ + const broadCastWrapper = ({ children }) => { + return {children} + } + + const opts = {} + const form = createForm(opts) + const { result } = renderHook(() => useForm({ form }), { wrapper: broadCastWrapper }) + expect(result.current).toEqual(form) + }) + + test('createForm instance binding api call with broadcast', ()=>{ + const fn = jest.fn() + const typeArr = [] + const broadcastInstance = new Broadcast() + broadcastInstance.subscribe(fn) + broadcastInstance.subscribe((a: any) => { + typeArr.push(a.type) + }) + const broadCastWrapper = ({ children }) => { + return {children} + } + + // 第一次formChange: initialized, editable(从true -> undefined , 有问题) + // 第二次formChange: mounted + const { result } = renderHook(() => useForm({}), { wrapper: broadCastWrapper }) + expect(typeArr).toEqual([ + 'onFormWillInit', 'onFormChange', 'onFormInit', 'onFormChange', 'onFormMount', + ]) + expect(fn).toBeCalledTimes(5) + + const targetValues = { username: 'abcd' } + result.current.setFormState(state => state.values = targetValues) + + // 第三次formChange: values + expect(typeArr).toEqual([ + 'onFormWillInit', 'onFormChange', 'onFormInit', 'onFormChange', 'onFormMount', + 'onFormChange', 'onFormValuesChange' + ]) + expect(fn).toBeCalledTimes(7) + expect(result.current.getFormState(state => state.values)).toEqual(targetValues) + }) + + test('useForm with initialValues', ()=>{ + const broadCastWrapper = ({ children }) => { + return {children} + } + + const targetValues = { username: 'abcd' } + const values = { age: 20 } + const { result } = renderHook(() => useForm({ initialValues: targetValues, value: values }), { wrapper: broadCastWrapper }) + expect(result.current.getFormState(state => state.initialValues)).toEqual(targetValues) + expect(result.current.getFormState(state => state.values)).toEqual(values) + }) + + test('useForm with defaultValue', ()=>{ + const broadCastWrapper = ({ children }) => { + return {children} + } + + const targetValues = { username: 'abcd' } + const values = { age: 20 } + const { result } = renderHook(() => useForm({ defaultValue: targetValues, value: values }), { wrapper: broadCastWrapper }) + expect(result.current.getFormState(state => state.initialValues)).toEqual(targetValues) + expect(result.current.getFormState(state => state.values)).toEqual(values) }) -}) -describe('major scenes',()=>{ - //todo - test('basic',()=>{ - //todo + test('useForm with values', ()=>{ + const broadCastWrapper = ({ children }) => { + return {children} + } + + const targetValues = { username: 'abcd' } + const { result } = renderHook(() => useForm({ value: targetValues }), { wrapper: broadCastWrapper }) + expect(result.current.getFormState(state => state.initialValues)).toEqual({}) + expect(result.current.getFormState(state => state.values)).toEqual(targetValues) + }) + + test('useForm with editable', ()=>{ + const broadCastWrapper = ({ children }) => { + return {children} + } + + const { result } = renderHook(() => useForm({ editable: false }), { wrapper: broadCastWrapper }) + expect(result.current.getFormState(state => state.editable)).toEqual(false) + }) + + test('FormState change', ()=>{ + const broadCastWrapper = ({ children }) => { + return {children} + } + + const form = createForm({}) + expect(form.getFormState(state => state.mounted)).toEqual(false) + expect(form.getFormState(state => state.initialized)).toEqual(true) + expect(form.getFormState(state => state.editable)).toEqual(undefined) + const { result } = renderHook(() => useForm({ form }), { wrapper: broadCastWrapper }) + expect(result.current.getFormState(state => state.mounted)).toEqual(true) + expect(result.current.getFormState(state => state.initialized)).toEqual(true) + expect(result.current.getFormState(state => state.editable)).toEqual(undefined) + }) + + test('FormActions will implemented(sync)', ()=>{ + const broadCastWrapper = ({ children }) => { + return {children} + } + + const actions = createFormActions() + expect(actions.getFormState()).toEqual(undefined) + const targetValues = { username: 'abcd' } + actions.setFormState(state => state.values = targetValues) + expect(actions.getFormState(state => state.values)).toEqual(undefined) + const { result } = renderHook(() => useForm({ actions }), { wrapper: broadCastWrapper }) + expect(actions.getFormState()).not.toEqual(undefined) + expect(result.current.getFormState()).toEqual(actions.getFormState()) + expect(actions.getFormState(state => state.values)).toEqual({}) + }) + + test('FormActions will implemented(async)', async ()=>{ + const broadCastWrapper = ({ children }) => { + return {children} + } + + const actions = createAsyncFormActions() + const targetValues = { username: 'abcd' } + const { result } = renderHook(() => useForm({ actions }), { wrapper: broadCastWrapper }) + const initialValues = await actions.getFormState(state => state.values) + expect(initialValues).toEqual({}) + await actions.setFormState(state => state.values = targetValues) + + const resultValues = await actions.getFormState(state => state.values) + expect(resultValues).toEqual(targetValues) + const actionResultValues = await result.current.getFormState(state => state.values) + expect(actionResultValues).toEqual(resultValues) }) -}) + test('broadaast context', ()=>{ + const broadcastInstance = new Broadcast() + const broadCastWrapper = ({ children }) => { + return {children} + } -describe('bugfix',()=>{ - //todo - test('basic',()=>{ - //todo + expect(broadcastInstance.context).toEqual(undefined) + const { result } = renderHook(() => useForm({}), { wrapper: broadCastWrapper }) + const symbolList = Object.getOwnPropertySymbols(result.current) + expect({ ...broadcastInstance.context, [symbolList[0]]: true, }).toEqual({ + ...result.current, + dispatch: result.current.notify, + }) }) }) \ No newline at end of file diff --git a/packages/react/src/hooks/useDirty.ts b/packages/react/src/hooks/useDirty.ts index f49b3af82fb..19393003b4a 100644 --- a/packages/react/src/hooks/useDirty.ts +++ b/packages/react/src/hooks/useDirty.ts @@ -2,7 +2,7 @@ import React from 'react' import { isEqual } from '@uform/shared' export const useDirty = (input: any = {}, keys: string[] = []) => { - const ref = React.useRef({ data: {...input}, dirtys: {}, num: 0 }) + const ref = React.useRef({ data: {...input}, dirtys: {}, num: 0 }) ref.current.num = 0 keys.forEach(key => { if (!isEqual(input[key], ref.current.data[key])) { diff --git a/packages/react/src/hooks/useForm.ts b/packages/react/src/hooks/useForm.ts index 19a75132182..64d4fd78188 100644 --- a/packages/react/src/hooks/useForm.ts +++ b/packages/react/src/hooks/useForm.ts @@ -104,6 +104,7 @@ export const useForm = < ...props, initialValues: props.initialValues || props.defaultValue }) + Object.assign(optionsRef.current, props) optionsRef.current.values = props.value optionsRef.current.lifecycles = lifecycles From ecff8efff7fa7c1459cd5d3fd3104feeebc1355a Mon Sep 17 00:00:00 2001 From: Janry Date: Thu, 14 Nov 2019 15:37:44 +0800 Subject: [PATCH 39/99] feat: add docs and some test cases (#395) --- packages/core/README.md | 1108 ++++++++++++++++- packages/core/README.zh-cn.md | 929 ++++++++++++++ .../__snapshots__/index.spec.ts.snap | 2 +- packages/core/src/__tests__/index.spec.ts | 164 ++- packages/core/src/index.ts | 7 +- packages/core/src/shared/model.ts | 14 +- packages/core/src/state/form.ts | 10 +- packages/core/src/types.ts | 30 +- packages/validator/src/rules.ts | 3 + packages/validator/src/types.ts | 2 +- packages/validator/src/validator.ts | 7 +- 11 files changed, 2218 insertions(+), 58 deletions(-) create mode 100644 packages/core/README.zh-cn.md diff --git a/packages/core/README.md b/packages/core/README.md index 7d8703f2c4a..521a0295080 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,33 +1,1099 @@ -# @uform/core -### 设计理念 +# @ Uform/core + +English | [简体中文](./README.zh-cn.md) + +> The form state core management package does not rely on any third-party UI frameworks. In this package, it mainly includes: +> +> - Manage Form status +> - Management Field status +> - Manage the Validator status +> - Manage dependencies between Form, Field, and Vaidator + +### Backdrop + +In the middle and back-end field, the core is two scenarios, **One is data entry, the other is Data Query + data presentation** , Whether it is data entry or data query, it is implemented by using forms. Only the forms of data presentation are diversified, but they are the easiest to reuse and abstract. Only forms, it involves a lot of Interactive Logic. Therefore, as long as we fundamentally solve the form problem, most of the middle and back-end scenarios are basically solved. UForm is born for this. + + + +### Design Concept + + **Anything comes from Observable Grpah.** -### 核心亮点 -- 时间旅行,借助首创Observable Graph,可以记录任意时刻的全量状态,也可以将状态回滚至任意时刻,这样的能力在,重事务型应用与本地调试上可以发挥出最大价值 -- 高效更新,精确渲染,无需整树渲染 -- 内置immer.js,智能降级,无需关心浏览器兼容性 -- 更加完备的生命周期钩子 -- 更加完备的校验引擎 - - validateFirst 校验 - - warning 校验(不阻塞提交校验) - - 校验消息模板引擎(不影响国际化文案存储的复杂校验文案消息解决方案) - - 校验规则可扩展,正则校验库可扩展 -- 更加灵活的路径解析,匹配,求值,取值引擎 - - 批量匹配数据路径能力 - - 解构求值,解构取值能力 -- 提供了基础表单状态模型之外的状态管理能力 -### 架构图 +### Core highlights + + + +- Time travel, with the help of the first Observable Graph, can record the full state at any time, can also roll back the state to any time, such ability in, retransaction applications and local debugging can maximize their value. +- Efficient update, accurate rendering, no full tree rendering required +- Built-in immer. js, intelligent degradation, no need to care about browser compatibility +- More complete life cycle hook +- More complete verification engine + +- - ValidateFirst verification + - Warning Verification (no blocking submission verification) + - Verification message template engine (a complex verification message solution that does not affect international copy storage) + - The verification rule can be extended, and the regular verification library can be extended. + +- More flexible path parsing, matching, evaluation, value engine + +- - Batch matching data path capability + - Deconstruct evaluation, deconstruct value ability + +- Provides state management capabilities beyond the basic form state model. + + + +### Architecture diagram + + + +![img](https://img.alicdn.com/tfs/TB18LXHlVP7gK0jSZFjXXc5aXXa-1428-926.png) + + + +### Terminology explanation + + + +**FormPath/FormPathPattern** Is an abstract data path form, FormPath is a path class, and FormPathPattern is a path form that can be parsed by FormPath. [Cool-path](https://github.com/janrywang/cool-path) Path parsing matching, ability to evaluate values + + + +**The virtual field** Is a special Field data structure. The difference between the Field and the Field is that it does not manage values. That is to say, it has no relevance to the value of the Form. Usually we use it, more importantly, it acts as a proxy for the status of a UI container. For example, the layout component FormBlock in UForm exists as an independent node in the whole Form Grpah, however, this node type is a VirtualField, but when the final data is submitted, the FormBlock does not pollute the data structure of the submitted data. + + + +**Observable Graph** Form is a unique Observer Tree. With the help of the observer tree, many forms-related internal linkage logic can be implemented. + + + +**Data Path** Is the name attribute of Field/VirtualField, which exists as the data path. + + + +**Node Path** Is the path attribute of Field/VirtualFIeld, which exists as the node path. + + + +For the data path and node path, we can look at the following figure: + + + +![img](https://img.alicdn.com/tfs/TB1.rAamG61gK0jSZFlXXXDKFXa-1496-898.png) + + + +If there exists such a tree, then: + + + +- The name attribute of field c is a.c, and the path attribute is a.b.c. +- The name attribute of field B is a.b, and the path attribute is a.b. +- The name attribute of field d is a.d, and the path attribute is a.d. +- The name attribute of field e is a.d.e, and the path attribute is a.d.e. + + + +After this explanation, we roughly understand that as long as VirtualField exists in a node path, its data path will skip VirtualField. However, for VirtualField itself, its name attribute contains its own node identification, which is why the name attribute of field B is a.b. + + + +### API + +#### `createForm` + +> Create a Form instance + + + +**Signature** + +``` +createForm(options?: IFormCreatorOptions): IForm +``` + + + +**Usage** + +``` + import { createForm } from '@uform/core' + + const form = createForm({ + values:{}, + initialValues:{}, + onChange:(values)=>{ + console.log(values) + } + }) + +const aa = form.registerField({ + path:"aa" +}) + +aa.setState(state=>{ + state.value = 123 +}) +console.log(form.getFormState(state=>state.values)) //{aa:123} +``` + + + +#### `registerValidationFormats` + + + +> Register a regular verification rule set + + + +**Signature** + +``` +registerValidationFormats(formats:{ + [formatName in string]: RegExp; +}) : void +``` + + + +**Usage** + +``` + import { createForm,registerValidationFormats } from '@uform/core' + + registerValidationFormats({ + number: /^[+-]?\d+(\.\d+)?$/ + }) + + const form = createForm({ + values:{}, + initialValues:{}, + onChange:(values)=>{ + console.log(values) + } +}) + +const aa = form.registerField({ + path:"aa", + rules:[{ + format:"number", + message:'This field is not a number.' + }] +}) + +aa.setState(state=>{ + state.value = 'hello world' +}) +form.validate() + +console.log(form.getFormState(state=>state.errors)) +/** +[{ + path: 'aa', + messages: [ 'This field is not a number.' ] +}] +**/ +``` + + + +#### `registerValidationRules` + + + +> The difference between registering a verification rule set and registering formats is that it can register complex verification rules, but the formats are just regular expressions. + + + +**Signature** + +``` +registerValidationRules( + rules:{ + [ruleName:string]:(value:any,rule:ValidateDescription)=>boolean + } +) : void +``` + + + +**Usage** + +``` + import { createForm,registerValidationRules } from '@uform/core' + + registerValidationRules({ + custom: value => { + return value === '123' ? 'This field can not be 123' : '' + } + }) + + const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + console.log(values) + } +}) + +const aa = form.registerField({ + path: 'aa', + rules: [ + { + custom: true + } + ] +}) + +aa.setState(state => { + state.value = '123' +}) +form.validate() + +console.log(form.getFormState(state =>state.errors)) +/** +[{ + path: 'aa', + messages: ['This field can not be 123'] +}] +**/ +``` + + + +#### `registerValidationMTEngine` + + + +> Register a verification message template engine + + + +**Signature** + +``` +registerValidationMTEngine(callback:(message,context)=>any) : void +``` + + + +**Usage** + +``` + import { createForm,registerValidationMTEngine } from '@uform/core' + + registerValidationMTEngine((message,context)=>{ + return message.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, $0) => { + return FormPath.getIn(context, $0) + }) + }) + + const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + console.log(values) + } +}) + +const aa = form.registerField({ + path: 'aa', + rules: [ + { + validator(value){ + return value === 123 : 'This field can not be 123 {{scope.outerVariable}}' + }, + scope:{ + outerVariable:'addonAfter' + } + } + ] +}) + +aa.setState(state => { + state.value = '123' +}) +form.validate() + +console.log(form.getFormState(state =>state.errors)) +/** +[{ + path: 'aa', + messages: ['This field can not be 123 addonAfter'] +}] +**/ +``` + + + +#### `setValidationLanguage` + + + +> Set the international language type + + + +**Signature** + +``` +setValidationLanguage(lang: string): void +``` + + + +**Usage** + +``` +import { setValidationLanguage } from '@uform/core' + +setValidationLanguage('en-US') +``` + + + +#### `setValidationLocale` + + + +> Set a language pack + + + +**Signature** + +``` +interface ILocaleMessages { + [key: string]: string | ILocaleMessages; +} +interface ILocales { + [lang: string]: ILocaleMessages; +} +setValidationLocale(locale: ILocales) => void +``` + + + +**Usage** + +``` +import { setValidationLocale } from '@uform/core' + +setValidationLocale({ + 'en-US':{ + required:"This field is required." + } +}) +``` + + + +### Classes + + + +#### `new FormPath()` + + + +> The form path engine is responsible for path analysis, matching, evaluation, value, deconstruction evaluation, and deconstruction value. + + + +For more information, see: [ https://github.com/janrywang/cool-path ](https://github.com/janrywang/cool-path) + + + +#### `new FormLifeCyle()` + + + +> Create a lifecycle listener + + + +**Signature** + +``` +type FormLifeCycleHandler = (payload: T, context: any) => void + +new FormLifeCyle(handler: FormLifeCycleHandler) +new FormLifeCyle(...type: LifeCycleTypes, handler: FormLifeCycleHandler...) +new FormLifeCyle(handlerMap: { [key: LifeCycleTypes]: FormLifeCycleHandler }) +``` + + + +**Usage** + +```typescript + import { createForm,FormLifeCycle,LifeCycleTypes } from '@uform/core' + + const form = createForm({ + lifecycles:[ + new FormLifeCycle(({type:LifeCycleTypes,payload:IForm | IField | IVirtualField })=>{ + // God mode, full monitoring + }), + new FormLifeCycle( + LifeCycleTypes.ON_FORM_MOUNT, + (payload:IForm | IField | IVirtualField)=>{ + // Accurate monitoring + }), + new FormLifeCycle({ + [LifeCycleTypes.ON_FORM_MOUNT]:(payload:IForm | IField | IVirtualField)=>{ + // Object form accurate listener + } + }), + ] +}) +``` + + + +### Enums + +#### Lifecycletypes + + + +```typescript +enum LifeCycleTypes { +  /** +   * Form LifeCycle +   **/ +  // Form pre-initialization trigger +  ON_FORM_WILL_INIT = 'onFormWillInit', + + // Form initialization trigger +  ON_FORM_INIT = 'onFormInit', + + // Triggered when the form changes +  ON_FORM_CHANGE = 'onFormChange', + + // Triggered when the form is mounted +  ON_FORM_MOUNT = 'onFormMount', + + // Triggered when the form is unloaded +  ON_FORM_UNMOUNT = 'onFormUnmount', + + // Triggered when the form is submitted +  ON_FORM_SUBMIT = 'onFormSubmit', + + // Triggered when the form is reset +  ON_FORM_RESET = 'onFormReset', + + // Triggered when the form submission starts +  ON_FORM_SUBMIT_START = 'onFormSubmitStart', + + // Triggered when the form submission ends +  ON_FORM_SUBMIT_END = 'onFormSubmitEnd', + + // Triggered when the form value changes +  ON_FORM_VALUES_CHANGE = 'onFormValuesChange', + + // Trigger when the form initial value changes +  ON_FORM_INITIAL_VALUES_CHANGE = 'onFormInitialValuesChange', + + // Triggered when form validation begins +  ON_FORM_VALIDATE_START = 'onFormValidateStart', + + // Triggered when the form validation ends +  ON_FORM_VALIDATE_END = 'onFormValidateEnd', + + // Triggered when the form event is triggered, used to monitor only manual operations +  ON_FORM_INPUT_CHANGE = 'onFormInputChange', + +  /** +   * FormGraph LifeCycle +   **/ + // Triggered when the form observer tree changes +  ON_FORM_GRAPH_CHANGE = 'onFormGraphChange', + +  /** +   * Field LifeCycle +   **/ + // Triggered when pre-initialized +  ON_FIELD_WILL_INIT = 'onFieldWillInit', + + // Triggered when the field is initialized +  ON_FIELD_INIT = 'onFieldInit', + + // Triggered when the field changes +  ON_FIELD_CHANGE = 'onFieldChange', + + // Triggered when the field event is triggered, used to monitor only manual operations +  ON_FIELD_INPUT_CHANGE = 'onFieldInputChange', + + // Triggered when the field value changes +  ON_FIELD_VALUE_CHANGE = 'onFieldValueChange', + + // Trigger when the initial value of the field changes +  ON_FIELD_INITIAL_VALUE_CHANGE = 'onFieldInitialValueChange', + + // Triggered when the field is mounted +  ON_FIELD_MOUNT = 'onFieldMount', + + // Trigger when the field is unloaded +  ON_FIELD_UNMOUNT = 'onFieldUnmount' +} +``` + + + +### Interfaces + +#### Iformcreatoroptions + + + +> CreateForm parameter object protocol + + + +```typescript +interface IFormCreatorOptions { +   // Form initial value +   initialValues?: {} + +   // Form value +   values?: {} + +   // Lifecycle listener, here mainly introduced to the instantiated object of FormLifeCycle +   lifecycles?: FormLifeCycle[] + +   // Is it editable, overall control in the Form dimension +   editable?: boolean | ((name: string) => boolean) + +   // Whether to use the dirty check, the default will go immer accurate update +   useDirty?: boolean + +   // Whether to go pessimistic check, stop the subsequent check when the first check fails +   validateFirst?: boolean + +   // Form change event callback +   onChange?: (values: IFormState['values']) => void + +   // Form submission event callback +   onSubmit?: (values: IFormState['values']) => any | Promise + +   // Form reset event callback +   onReset?: () => void + +   // Form verification failure event callback +   onValidateFailed?: (validated: IFormValidateResult) => void +} + +``` + + + +#### Iform + + + +> Form instance object API created by using createForm + + + +```typescript +interface IForm { +  /* +   * Form submission, if the callback parameter returns Promise, +   * Then the entire submission process will hold and load is true. +   * Wait for Promise resolve to trigger the form onFormSubmitEnd event while loading is false +   */ +   submit( +      onSubmit?: (values: IFormState['values']) => any | Promise +    ): Promise<{ +       Validated: IFormValidateResult +       Payload: any //onSubmit callback function return value +   }> +    +   /* +    * Clear the error message, you can pass the FormPathPattern to batch or precise control of the field to be cleared. +    * For example, clearErrors("*(aa,bb,cc)") +    */ +   clearErrors: (pattern?: FormPathPattern) => void +    +   /* +    * Get status changes, mainly used to determine which states in the current life cycle have changed in the form lifecycle hook. +    * For example, hasChanged(state,'value.aa') +    */ +   hasChanged(target: IFormState | IFieldState | IVirtualFieldState, path: FormPathPattern): boolean +    +   /* +    * Reset form +    */ +   reset(options?: { +     // Forced to empty +     forceClear?: boolean +     // Forced check +     validate?: boolean +     // Reset range for batch or precise control of the field to be reset +     selector?: FormPathPattern +   }): Promise +    +   /* +    * Validation form +    */ +   validate(path?: FormPathPattern, options?: { +     // Is it pessimistic check, if the current field encounters the first verification error, stop the subsequent verification process +     first?:boolean +   }): Promise +    +   /* +    * Set the form status +    */ +   setFormState( +     // Operation callback +     callback?: (state: IFormState) => any, +     // No trigger the event +     silent?: boolean +   ): void +    +   /* +    * Get form status +    */ +   getFormState( +     //transformer +     callback?: (state: IFormState) => any +   ): any +    +   /* +    * Set the field status +    */ +   setFieldState( +     // Field path +     path: FormPathPattern, +     // Operation callback +     callback?: (state: IFieldState) => void, +     // No trigger the event +     silent?: boolean +   ): void +    +   /* +    * Get the field status +    */ +   getFieldState( +     // Field path +     path: FormPathPattern, +     // Transformer +     callback?: (state: IFieldState) => any +   ): any +    +   /* +    * Registration field +    */ +   registerField(props: { +    // Node path +    path?: FormPathPattern +    // Data path +    name?: string +    // Field value +    value?: any +    // Field multi-value +    values?: any[] +    // Field initial value +    initialValue?: any +    // Field extension properties +    props?: any +    // Field check rule +    rules?: ValidatePatternRules[] +    // Field is required +    required?: boolean +    // Is the field editable? +    editable?: boolean +    // Whether the field is dirty check +    useDirty?: boolean +    // Field state calculation container, mainly used to extend the core linkage rules +    computeState?: (draft: IFieldState, prevState: IFieldState) => void +  }): IField +   +  /* +   * Register virtual fields +   */ +  registerVirtualField(props: { +    // Node path +    path?: FormPathPattern +    // Data path +    name?: string +    // Field extension properties +    props?: any +    // Whether the field is dirty check +    useDirty?: boolean +    // Field state calculation container, mainly used to extend the core linkage rules +    computeState?: (draft: IFieldState, prevState: IFieldState) => void +  }): IVirtualField +   +  /* +   * Create a field data operator, which will explain the returned API in detail later. +   */ +  createMutators(field: IField): IMutators +   +  /* +   * Get the form observer tree +   */ +  getFormGraph(): IFormGraph +   +  /* +   * Set the form observer tree +   */ +  setFormGraph(graph: IFormGraph): void +   +  /* +   * Listen to the form life cycle +   */ +  subscribe(callback?: ({ +    type, +    payload +  }: { +    type: string +    payload: any +  }) => void): number +   +  /* +   * Cancel the listening form life cycle +   */ +  unsubscribe(id: number): void +   +  /* +   * Trigger form custom life cycle +   */ +  notify: (type: string, payload?: T) => void +   +  /* +   * Set the field value +   */ +  setFieldValue(path?: FormPathPattern, value?: any): void +   +  /* +   * Get the field value +   */ +  getFieldValue(path?: FormPathPattern): any +   +  /* +   * Set the initial value of the field +   */ +  setFieldInitialValue(path?: FormPathPattern, value?: any): void +   +  /* +   * Get the initial value of the field +   */ +  getFieldInitialValue(path?: FormPathPattern): any +} +``` + + + +#### Imutators + + + +> The instance API created by crewikiutators is mainly used to operate field data. + + + +```typescript +interface IMutators { + // Changing the field value and multi parameter condition will store all parameters in values + change(...values: any[]): any + // Get focus, trigger active state change + focus(): void + // Lose focus, trigger active / visited status change + blur (): void + // Trigger current field verifier + validate(): Promise + // Whether the value of the current field exists in the values property of form + exist (index?: number | string): Boolean + + /**Array operation method**/ + + // Append data + push(value?: any): any[] + // Pop up tail data + pop (): any[] + // Insert data + insert(index: number, value: any): any[] + // Delete data + remove(index: number | string): any + // Head insertion + unshift(value: any): any[] + // Head ejection + shift(): any[] + // Move element + move($from: number, $to: number): any[] + // Move down + moveDown(index: number): any[] + // Move up + moveUp(index: number): any[] +} +``` + + + +#### The Validator + + + +> Here we mainly list the intermediate type signatures related to verification. + + + +```typescript + type CustomValidator = ( + value: any, + rescription?: ValidateDescription + ) => ValidateResponse + type SyncValidateResponse = null | string | boolean | { + type?: 'error' | 'warning' + message: string + }; + type AsyncValidateResponse = Promise + type ValidateResponse = SyncValidateResponse | AsyncValidateResponse; + + interface IFormValidateResult { + errors: Array<{ + path: string; + messages: string[] + }> + warnings: Array<{ + path: string + messages: string[] + }> +} + +interface ValidateDescription { +     // Regular rule type +     format?: string +     // Custom validator +     validator?: CustomValidator +     // Is it required? +     required?: boolean +     // Customize with regularity +     pattern?: RegExp | string +     // Maximum length rule +     max?: number; +     // Maximum numerical rule +     maximum?: number +     // Exclusive maximum numerical rule +     exclusiveMaximum?: number +     // Exclusive minimum numerical rules +     exclusiveMinimum?: number +     // Minimum value rule +     minimum?: number +     // Minimum length rule +     min?: number +     // Length rule +     len?: number +     // Whether to check the white space +     whitespace?: boolean +     // Enumeration check rules +     enum?: any[] +     // Custom error copy +     message?: string +     // Custom validation rules +     [key: string]: any +} +``` + + + +#### IFormState + + + +> Form the core state + + + +```typescript +interface IFormState { +    +   /**Read-only attribute**/ +    +  // Is it in the original state, pristine is true only when values === initialValues +  pristine: boolean +  // Is it legal, as long as the error length is greater than 0, the valid is false +  valid: boolean +  // Is it illegal, as long as the error length is greater than 0, the valid is true +  invalid: boolean +  // Is it in the check state, it will only be set when calling the validate API +  validating: boolean +  // Is it in the commit state, it will only be set when the submit API is called +  submitting: boolean +  //Error message list +  errors: string[] +  //Alarm message list +  warnings: string[] +   +  /** writable property**/ +   +  // Is it in the loaded state, writable state, as long as validating is true, the state will also be true, the same as false +  loading: boolean +  // Is it in the initial state? +  initialized: boolean +  // Is it editable? +  editable: boolean | ((name: string) => boolean) +  // form value +  values: {} +  // form initial value +  initialValues: {} +  // form mount, the life cycle hook mentioned earlier, must be triggered by setting the state, the default will not trigger +  mounted: boolean +  // Form unmount, the life cycle hook mentioned earlier, must be triggered by setting the state, the default will not trigger +  unmounted: boolean +  // Form extension properties +  props: FormProps +} +``` + + + +#### IFieldState + + + +> CORE Field status + + + +```typescript +interface IFieldState { +    +  /**Read-only attribute**/ +    +  // State name, FieldState +  displayName?: string +  // Data path +  name: string +  // Node path +  path: string +  // Has been initialized +  initialized: boolean +  // Is it in the original state, the state is true only when value===intialValues +  pristine: boolean +  // Is it in a legal state, as long as the error length is greater than 0, the valid is false +  valid: boolean +  // Is it illegal, as long as the error length is greater than 0, the valid is true +  invalid: boolean +  // Is it in check state? +  validating: boolean +  // Is it modified, if the value changes, the property is true, and will be true throughout the life of the field +  modified: boolean +  // Is it touched? +  touched: boolean +  // Is it activated, when the field triggers the onFocus event, it will be triggered to true, when onBlur is triggered, it is false +  active: boolean +  // Have you ever visited, when the field triggers the onBlur event, it will be triggered to true +  visited: boolean +   +  /** writable property**/ +   +  // Is it visible, note: if the state is false, then the value of the field will not be submitted, and the UI will not display +  visible: boolean +  // Whether to show, note: if the state is false, then the value of the field will be submitted, the UI will not display, similar to the form hidden field +  display: boolean +  // Is it editable? +  editable: boolean +  // Is it in the loading state, note: if the field is in asynchronous verification, loading is true +  loading: boolean +  // Field multi-parameter value, such as when the field onChange trigger, the event callback passed multi-parameter data, then the value of all parameters will be stored here +  values: any[] +  // Field error message +  errors: string[] +  // Field alert message +  warnings: string[] +  // Field value, is equal to values[0] +  value: any +  // Initial value +  initialValue: any +  // Check the rules, the specific type description refers to the following documents +  rules: ValidatePatternRules[] +  // Is it required? +  required: boolean +  // Whether to mount +  mounted: boolean +  // Whether to uninstall +  unmounted: boolean +  // field extension properties +  props: FieldProps +} +``` + + + +#### IVirtualFieldState + + + +> Virtual Field core status + + + +```typescript +interface IVirtualFieldState { +    +   /**Read-only status**/ +    +   // State name, VirtualFieldState +   displayName: string +   // Field data path +   name: string +   // Field node path +   path: string +   // Has been initialized +   initialized: boolean +   +   /** writable status**/ +   +   // Is it visible, note: if the state is false, the UI will not be displayed, the data will not be submitted (because it is a VirtualField) +   visible: boolean +   // Whether to show, note: if the state is false, the UI will not display, the data will not be submitted (because it is VirtualField) +   display: boolean +   // Is it mounted? +   mounted: boolean +   // Has been uninstalled +   unmounted: boolean +   // field extension properties +   props: FieldProps +} +``` + + -![](https://img.alicdn.com/tfs/TB18LXHlVP7gK0jSZFjXXc5aXXa-1428-926.png) +#### IField/IVirtualField -### API -### Interfaces -### Examples +> The instance API created by using registerField/registerVirtualField +```typescript +interface IField/IVirtualField { +   // Batch update container +   batch: (callback?: () => void) => void +   // Get the status +   getState: (callback?: (state: IFieldState) => any) => any +   // Set the status +   setState: ( +      callback?: (state: IFieldState | Draft) => void, +      silent?: boolean +   ) => void +   // Get the source status +   unsafe_getSourceState: (callback?: (state: IFieldState) => any) => any +   // Set the source state +   unsafe_setSourceState: (callback?: (state: IFieldState) => void) => void +   // Get status changes +   hasChanged: (key?: string) => boolean +   // Get the state dirty +   isDirty: (key?: string) => boolean +   // Get state dirty information +   getDirtyInfo: () => StateDirtyMap +} +``` \ No newline at end of file diff --git a/packages/core/README.zh-cn.md b/packages/core/README.zh-cn.md new file mode 100644 index 00000000000..6a1020cf23e --- /dev/null +++ b/packages/core/README.zh-cn.md @@ -0,0 +1,929 @@ +# @uform/core +> 表单状态核心管理包(不依赖任何第三方UI框架),在该包中,它主要做了: +> +> - 管理Form状态 +> - 管理Field状态 +> - 管理Validator状态 +> - 管理Form/Field/Vaidator之间的依赖关系 + +### 背景 + +中后台领域,核心就是两种场景,**一个是数据录入,一个是数据查询+数据展现**,不管是数据录入还是数据查询,都是借助表单来实现,只有数据展现的形式是比较多样化的,但是却是最容易复用和抽象的,只有表单,会涉及大量的交互逻辑,所以,只要我们根本上解决了表单问题,对于中后台场景,基本上解决了大部分中后台场景问题,UForm,就是为此而诞生的。 + +### 设计理念 + +**Anything comes from Observable Grpah.** + +### 核心亮点 + +- 时间旅行,借助首创Observable Graph,可以记录任意时刻的全量状态,也可以将状态回滚至任意时刻,这样的能力在,重事务型应用与本地调试上可以发挥出最大价值 +- 高效更新,精确渲染,无需整树渲染 +- 内置immer.js,智能降级,无需关心浏览器兼容性 +- 更加完备的生命周期钩子 +- 更加完备的校验引擎 + - validateFirst 校验 + - warning 校验(不阻塞提交校验) + - 校验消息模板引擎(不影响国际化文案存储的复杂校验文案消息解决方案) + - 校验规则可扩展,正则校验库可扩展 +- 更加灵活的路径解析,匹配,求值,取值引擎 + - 批量匹配数据路径能力 + - 解构求值,解构取值能力 +- 提供了基础表单状态模型之外的状态管理能力 + +### 架构图 + +![](https://img.alicdn.com/tfs/TB18LXHlVP7gK0jSZFjXXc5aXXa-1428-926.png) + + + +### 术语解释 + +**FormPath/FormPathPattern** 是一个抽象数据路径形式,FormPath是路径类,FormPathPattern是可以被FormPath解析的路径形式,在这里主要使用了 [cool-path](https://github.com/janrywang/cool-path) 路径解析匹配,求值取值能力 + +**VirtualField** 是一个特殊的Field数据结构,它与Field的差异就是,它不管理value,也就是说,它与Form的value是没有关联性的,通常我们使用它,更多的是作为代理一个UI容器的状态,比如:UForm中的布局组件FormBlock,它会在整个Form Grpah中作为一个独立节点而存在,但是这个节点类型就是一个VirtualField,但是最终数据提交的时候,FormBlock并不会污染提交数据的数据结构。 + +**Observable Graph** 是Form独有的观察者树,借助观察者树,可以实现很多表单相关的内部联动逻辑 + +**Data Path** 是Field/VirtualField的name属性,它是作为数据路径而存在 + +**Node Path** 是Field/VirtualFIeld的path属性,它是作为节点路径而存在 + +对于数据路径和节点路径,我们可以看下面这张图: + +![](https://img.alicdn.com/tfs/TB1.rAamG61gK0jSZFlXXXDKFXa-1496-898.png) + +如果存在这样一棵树的话,那么: + +- c字段的name属性则是a.c,path属性是a.b.c +- b字段的name属性是a.b,path属性是a.b +- d字段的name属性是a.d,path属性是a.d +- e字段的name属性是a.d.e,path属性是a.d.e + +这一来解释之后,我们就大概明白了,只要在某个节点路径中,存在VirtualField,那么它的数据路径就会略过VirtualField,但是,对于VirtualField自身这个节点,它的name属性,是包含它自身的节点标识的,这就是为什么b字段的name属性是a.b的原因 + +### API + +--- + +#### `createForm` + +> 创建一个Form实例 + +**签名** + +```typescript +createForm(options?: IFormCreatorOptions): IForm +``` + +**用法** + +```typescript +import { createForm } from '@uform/core' + +const form = createForm({ + values:{}, + initialValues:{}, + onChange:(values)=>{ + console.log(values) + } +}) + +const aa = form.registerField({ + path:"aa" +}) + +aa.setState(state=>{ + state.value = 123 +}) +console.log(form.getFormState(state=>state.values)) //{aa:123} +``` + + + +#### `registerValidationFormats` + +> 注册正则校验规则集 + +**签名** + +```typescript +registerValidationFormats(formats:{ + [formatName in string]: RegExp; +}) : void +``` + + + +**用法** + +```typescript +import { createForm,registerValidationFormats } from '@uform/core' + +registerValidationFormats({ + number: /^[+-]?\d+(\.\d+)?$/ +}) + +const form = createForm({ + values:{}, + initialValues:{}, + onChange:(values)=>{ + console.log(values) + } +}) + +const aa = form.registerField({ + path:"aa", + rules:[{ + format:"number", + message:'This field is not a number.' + }] +}) + +aa.setState(state=>{ + state.value = 'hello world' +}) +form.validate() + +console.log(form.getFormState(state=>state.errors)) +/** +[{ + path: 'aa', + messages: [ 'This field is not a number.' ] +}] +**/ +``` + + + +#### `registerValidationRules` + +> 注册校验规则集,与注册formats的差别是,它可以注册复杂校验规则,但是formats只是正则表达式 + +**签名** + +```typescript +registerValidationRules( + rules:{ + [ruleName:string]:(value:any,rule:ValidateDescription)=>boolean + } +) : void +``` + +**用法** + +```typescript +import { createForm,registerValidationRules } from '@uform/core' + +registerValidationRules({ + custom: value => { + return value === '123' ? 'This field can not be 123' : '' + } +}) + +const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + console.log(values) + } +}) + +const aa = form.registerField({ + path: 'aa', + rules: [ + { + custom: true + } + ] +}) + +aa.setState(state => { + state.value = '123' +}) +form.validate() + +console.log(form.getFormState(state =>state.errors)) +/** +[{ + path: 'aa', + messages: ['This field can not be 123'] +}] +**/ +``` + +#### `registerValidationMTEngine` + +> 注册校验消息模板引擎 + +**签名** + +```typescript +registerValidationMTEngine(callback:(message,context)=>any) : void +``` + +**用法** + +```javascript +import { createForm,registerValidationMTEngine } from '@uform/core' + +registerValidationMTEngine((message,context)=>{ + return message.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, $0) => { + return FormPath.getIn(context, $0) + }) +}) + +const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + console.log(values) + } +}) + +const aa = form.registerField({ + path: 'aa', + rules: [ + { + validator(value){ + return value === 123 : 'This field can not be 123 {{scope.outerVariable}}' + }, + scope:{ + outerVariable:'addonAfter' + } + } + ] +}) + +aa.setState(state => { + state.value = '123' +}) +form.validate() + +console.log(form.getFormState(state =>state.errors)) +/** +[{ + path: 'aa', + messages: ['This field can not be 123 addonAfter'] +}] +**/ +``` + + + +#### `setValidationLanguage` + +> 设置国际化语言类型 + +**签名** + +```typescript +setValidationLanguage(lang: string): void +``` + +**用法** + +```javascript +import { setValidationLanguage } from '@uform/core' + +setValidationLanguage('en-US') +``` + + + +#### `setValidationLocale` + +> 设置语言包 + +**签名** + +```typescript +interface ILocaleMessages { + [key: string]: string | ILocaleMessages; +} +interface ILocales { + [lang: string]: ILocaleMessages; +} +setValidationLocale(locale: ILocales) => void +``` + +**用法** + +```javascript +import { setValidationLocale } from '@uform/core' + +setValidationLocale({ + 'en-US':{ + required:"This field is required." + } +}) +``` + + + +### Classes + +#### `new FormPath()` + +> 表单路径引擎,核心负责路径解析,匹配,求值,取值,解构求值,解构取值 + +具体文档参考:https://github.com/janrywang/cool-path + +#### `new FormLifeCyle()` + +> 创建一个生命周期监听器 + +**签名** + +```typescript +type FormLifeCycleHandler = (payload: T, context: any) => void + +new FormLifeCyle(handler: FormLifeCycleHandler) +new FormLifeCyle(...type: LifeCycleTypes, handler: FormLifeCycleHandler...) +new FormLifeCyle(handlerMap: { [key: LifeCycleTypes]: FormLifeCycleHandler }) +``` + +**用法** + +```typescript +import { createForm,FormLifeCycle,LifeCycleTypes } from '@uform/core' + +const form = createForm({ + lifecycles:[ + new FormLifeCycle(({type:LifeCycleTypes,payload:IForm | IField | IVirtualField })=>{ + //上帝模式,全量监听 + }), + new FormLifeCycle( + LifeCycleTypes.ON_FORM_MOUNT, + (payload:IForm | IField | IVirtualField)=>{ + //精确监听 + }), + new FormLifeCycle({ + [LifeCycleTypes.ON_FORM_MOUNT]:(payload:IForm | IField | IVirtualField)=>{ + //对象形式精确监听 + } + }), + ] +}) +``` + + + +### Enums + +--- + +#### LifeCycleTypes + +```typescript +enum LifeCycleTypes { + /** + * Form LifeCycle + **/ + + ON_FORM_WILL_INIT = 'onFormWillInit', //表单预初始化触发 + ON_FORM_INIT = 'onFormInit', //表单初始化触发 + ON_FORM_CHANGE = 'onFormChange', //表单变化时触发 + ON_FORM_MOUNT = 'onFormMount', //表单挂载时触发 + ON_FORM_UNMOUNT = 'onFormUnmount', //表单卸载时触发 + ON_FORM_SUBMIT = 'onFormSubmit', //表单提交时触发 + ON_FORM_RESET = 'onFormReset', //表单重置时触发 + ON_FORM_SUBMIT_START = 'onFormSubmitStart', //表单提交开始时触发 + ON_FORM_SUBMIT_END = 'onFormSubmitEnd', //表单提交结束时触发 + ON_FORM_VALUES_CHANGE = 'onFormValuesChange', //表单值变化时触发 + ON_FORM_INITIAL_VALUES_CHANGE = 'onFormInitialValuesChange',//表单初始值变化时触发 + ON_FORM_VALIDATE_START = 'onFormValidateStart', //表单校验开始时触发 + ON_FORM_VALIDATE_END = 'onFormValidateEnd', //表单校验结束时触发 + ON_FORM_INPUT_CHANGE = 'onFormInputChange', //表单事件触发时触发,用于只监控人工操作 + /** + * FormGraph LifeCycle + **/ + ON_FORM_GRAPH_CHANGE = 'onFormGraphChange', //表单观察者树变化时触发 + + /** + * Field LifeCycle + **/ + + ON_FIELD_WILL_INIT = 'onFieldWillInit', //字段预初始化时触发 + ON_FIELD_INIT = 'onFieldInit', //字段初始化时触发 + ON_FIELD_CHANGE = 'onFieldChange', //字段变化时触发 + ON_FIELD_INPUT_CHANGE = 'onFieldInputChange', //字段事件触发时触发,用于只监控人工操作 + ON_FIELD_VALUE_CHANGE = 'onFieldValueChange', //字段值变化时触发 + ON_FIELD_INITIAL_VALUE_CHANGE = 'onFieldInitialValueChange',//字段初始值变化时触发 + ON_FIELD_MOUNT = 'onFieldMount', //字段挂载时触发 + ON_FIELD_UNMOUNT = 'onFieldUnmount' //字段卸载时触发 +} +``` + + + +### Interfaces + +--- + +#### IFormCreatorOptions + +> createForm参数对象协议 + +```typescript +interface IFormCreatorOptions { + //初始值 + initialValues?: {} + //值 + values?: {} + //生命周期监听器,在这里主要传入FormLifeCycle的实例化对象 + lifecycles?: FormLifeCycle[] + //是否可编辑,在Form维度整体控制 + editable?: boolean | ((name: string) => boolean) + //是否使用脏检查,默认会走immer精确更新 + useDirty?: boolean + //是否走悲观校验,遇到第一个校验失败就停止后续校验 + validateFirst?: boolean + //表单变化事件回调 + onChange?: (values: IFormState['values']) => void + //表单提交事件回调 + onSubmit?: (values: IFormState['values']) => any | Promise + //表单重置事件回调 + onReset?: () => void + //表单校验失败事件回调 + onValidateFailed?: (validated: IFormValidateResult) => void +} +``` + + + +#### IForm + +> 通过createForm创建出来的Form实例对象API + +```typescript +interface IForm { + /* + * 表单提交,如果回调参数返回Promise, + * 那么整个提交流程会hold住,同时loading为true, + * 等待Promise resolve才触发表单onFormSubmitEnd事件,同时loading为false + */ + submit( + onSubmit?: (values: IFormState['values']) => any | Promise + ): Promise<{ + validated: IFormValidateResult + payload: any //onSubmit回调函数返回值 + }> + + /* + * 清空错误消息,可以通过传FormPathPattern来批量或精确控制要清空的字段, + * 比如clearErrors("*(aa,bb,cc)") + */ + clearErrors: (pattern?: FormPathPattern) => void + + /* + * 获取状态变化情况,主要用于在表单生命周期钩子内判断当前生命周期中有哪些状态发生了变化, + * 比如hasChanged(state,'value.aa') + */ + hasChanged(target: IFormState | IFieldState | IVirtualFieldState, path: FormPathPattern): boolean + + /* + * 重置表单 + */ + reset(options?: { + //强制清空 + forceClear?: boolean + //强制校验 + validate?: boolean + //重置范围,用于批量或者精确控制要重置的字段 + selector?: FormPathPattern + }): Promise + + /* + * 校验表单 + */ + validate(path?: FormPathPattern, options?: { + //是否悲观校验,如果当前字段遇到第一个校验错误则停止后续校验流程 + first?:boolean + }): Promise + + /* + * 设置表单状态 + */ + setFormState( + //操作回调 + callback?: (state: IFormState) => any, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取表单状态 + */ + getFormState( + //transformer + callback?: (state: IFormState) => any + ): any + + /* + * 设置字段状态 + */ + setFieldState( + //字段路径 + path: FormPathPattern, + //操作回调 + callback?: (state: IFieldState) => void, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取字段状态 + */ + getFieldState( + //字段路径 + path: FormPathPattern, + //transformer + callback?: (state: IFieldState) => any + ): any + + /* + * 注册字段 + */ + registerField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段值 + value?: any + //字段多参值 + values?: any[] + //字段初始值 + initialValue?: any + //字段扩展属性 + props?: any + //字段校验规则 + rules?: ValidatePatternRules[] + //字段是否必填 + required?: boolean + //字段是否可编辑 + editable?: boolean + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IField + + /* + * 注册虚拟字段 + */ + registerVirtualField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段扩展属性 + props?: any + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IVirtualField + + /* + * 创建字段数据操作器,后面会详细解释返回的API + */ + createMutators(field: IField): IMutators + + /* + * 获取表单观察者树 + */ + getFormGraph(): IFormGraph + + /* + * 设置表单观察者树 + */ + setFormGraph(graph: IFormGraph): void + + /* + * 监听表单生命周期 + */ + subscribe(callback?: ({ + type, + payload + }: { + type: string + payload: any + }) => void): number + + /* + * 取消监听表单生命周期 + */ + unsubscribe(id: number): void + + /* + * 触发表单自定义生命周期 + */ + notify: (type: string, payload?: T) => void + + /* + * 设置字段值 + */ + setFieldValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段值 + */ + getFieldValue(path?: FormPathPattern): any + + /* + * 设置字段初始值 + */ + setFieldInitialValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段初始值 + */ + getFieldInitialValue(path?: FormPathPattern): any +} +``` + + + +#### Imutators + +> 通过createMutators创建出来的实例API,主要用于操作字段数据 + +```typescript +interface IMutators { + //改变字段值,多参情况,会将所有参数存在values中 + change(...values: any[]): any + //获取焦点,触发active状态改变 + focus(): void + //失去焦点,触发active/visited状态改变 + blur(): void + //触发当前字段校验器 + validate(): Promise + //当前字段的值是否在Form的values属性中存在 + exist(index?: number | string): boolean + + /**数组操作方法**/ + + //追加数据 + push(value?: any): any[] + //弹出尾部数据 + pop(): any[] + //插入数据 + insert(index: number, value: any): any[] + //删除数据 + remove(index: number | string): any + //头部插入 + unshift(value: any): any[] + //头部弹出 + shift(): any[] + //移动元素 + move($from: number, $to: number): any[] + //下移 + moveDown(index: number): any[] + //上移 + moveUp(index: number): any[] + +} +``` + + + +#### Validator + +> 这里主要列举校验相关的中间类型签名 + +```typescript +type CustomValidator = (value: any, rescription?: ValidateDescription) => ValidateResponse +type SyncValidateResponse = null | string | boolean | { + type?: 'error' | 'warning'; + message: string; +}; +type AsyncValidateResponse = Promise +type ValidateResponse = SyncValidateResponse | AsyncValidateResponse; + +interface IFormValidateResult { + errors: Array<{ + path: string; + messages: string[]; + }>; + warnings: Array<{ + path: string; + messages: string[]; + }>; +} + +interface ValidateDescription { + //正则规则类型 + format?: string; + //自定义校验规则 + validator?: CustomValidator; + //是否必填 + required?: boolean; + //自定以正则 + pattern?: RegExp | string; + //最大长度规则 + max?: number; + //最大数值规则 + maximum?: number; + //封顶数值规则 + exclusiveMaximum?: number; + //封底数值规则 + exclusiveMinimum?: number; + //最小数值规则 + minimum?: number; + //最小长度规则 + min?: number; + //长度规则 + len?: number; + //是否校验空白符 + whitespace?: boolean; + //枚举校验规则 + enum?: any[]; + //自定义错误文案 + message?: string; + //自定义校验规则 + [key: string]: any; +} +``` + + + + + +#### IFormState + +> Form核心状态 + +```typescript +interface IFormState { + + /**只读属性**/ + + //是否处于原始态,只有values===initialValues时,pristine为true + pristine: boolean + //是否合法,只要errors长度大于0的时候valid为false + valid: boolean + //是否非法,只要errors长度大于0的时候valid为true + invalid: boolean + //是否处于校验态,只有在调用validate API的时候才会被设置 + validating: boolean + //是否处于提交态,只有在调用submit API的时候才会被设置 + submitting: boolean + //错误消息了列表 + errors: string[] + //告警消息列表 + warnings: string[] + + /**可写属性**/ + + //是否处于加载态,可写状态,只要validating为true时,该状态也会为true,为false时同理 + loading: boolean + //是否处于初始态 + initialized: boolean + //是否可编辑 + editable: boolean | ((name: string) => boolean) + //表单值 + values: {} + //表单初始值 + initialValues: {} + //表单挂载,前面讲到的生命周期钩子,是必须通过设置该状态来触发的,默认不会触发 + mounted: boolean + //表单卸载,前面讲到的生命周期钩子,是必须通过设置该状态来触发的,默认不会触发 + unmounted: boolean + //表单扩展属性 + props: FormProps +} +``` + + + +#### IFieldState + +> 核心Field状态 + +```typescript +interface IFieldState { + + /**只读属性**/ + + //状态名称,FieldState + displayName?: string + //数据路径 + name: string + //节点路径 + path: string + //是否已经初始化 + initialized: boolean + //是否处于原始态,只有value===intialValues时的时候该状态为true + pristine: boolean + //是否处于合法态,只要errors长度大于0的时候valid为false + valid: boolean + //是否处于非法态,只要errors长度大于0的时候valid为true + invalid: boolean + //是否处于校验态 + validating: boolean + //是否被修改,如果值发生变化,该属性为true,同时在整个字段的生命周期内都会为true + modified: boolean + //是否被触碰 + touched: boolean + //是否被激活,字段触发onFocus事件的时候,它会被触发为true,触发onBlur时,为false + active: boolean + //是否访问过,字段触发onBlur事件的时候,它会被触发为true + visited: boolean + + /**可写属性**/ + + //是否可见,注意:该状态如果为false,那么字段的值不会被提交,同时UI不会显示 + visible: boolean + //是否展示,注意:该状态如果为false,那么字段的值会提交,UI不会展示,类似于表单隐藏域 + display: boolean + //是否可编辑 + editable: boolean + //是否处于loading状态,注意:如果字段处于异步校验时,loading为true + loading: boolean + //字段多参值,比如字段onChange触发时,给事件回调传了多参数据,那么这里会存储所有参数的值 + values: any[] + //字段错误消息 + errors: string[] + //字段告警消息 + warnings: string[] + //字段值,与values[0]是恒定相等 + value: any + //初始值 + initialValue: any + //校验规则,具体类型描述参考后面文档 + rules: ValidatePatternRules[] + //是否必填 + required: boolean + //是否挂载 + mounted: boolean + //是否卸载 + unmounted: boolean + //字段扩展属性 + props: FieldProps +} +``` + + + +#### IVirtualFieldState + +> 虚拟Field核心状态 + +```typescript +interface IVirtualFieldState { + + /**只读状态**/ + + //状态名称,VirtualFieldState + displayName: string + //字段数据路径 + name: string + //字段节点路径 + path: string + //是否已经初始化 + initialized: boolean + + /**可写状态**/ + + //是否可见,注意:该状态如果为false,UI不会显示,数据也不会提交(因为它是VirtualField) + visible: boolean + //是否展示,注意:该状态如果为false,UI不会显示,数据也不会提交(因为它是VirtualField) + display: boolean + //是否已挂载 + mounted: boolean + //是否已卸载 + unmounted: boolean + //字段扩展属性 + props: FieldProps +} +``` + + + +#### IField/IVirtualField + +> 通过registerField/registerVirtualField创建出来的实例API + +```typescript +interface IField/IVirtualField { + //批量更新容器 + batch: (callback?: () => void) => void + //获取状态 + getState: (callback?: (state: IFieldState) => any) => any + //设置状态 + setState: (callback?: (state: IFieldState | Draft) => void, silent?: boolean) => void + //获取源状态 + unsafe_getSourceState: (callback?: (state: IFieldState) => any) => any + //设置源状态 + unsafe_setSourceState: (callback?: (state: IFieldState) => void) => void + //获取状态变化情况 + hasChanged: (key?: string) => boolean + //获取状态脏 + isDirty: (key?: string) => boolean + //获取状态脏信息 + getDirtyInfo: () => StateDirtyMap +} +``` + + diff --git a/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap b/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap index 09f00018cff..fa3c5b7f29b 100644 --- a/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap +++ b/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap @@ -617,7 +617,7 @@ Object { undefined, ], "visible": true, - "visited": true, + "visited": false, "warnings": Array [], }, } diff --git a/packages/core/src/__tests__/index.spec.ts b/packages/core/src/__tests__/index.spec.ts index 379ac5fe3cd..37b83d86b7c 100644 --- a/packages/core/src/__tests__/index.spec.ts +++ b/packages/core/src/__tests__/index.spec.ts @@ -1,5 +1,13 @@ import { isEqual } from '@uform/shared' -import { createForm, LifeCycleTypes, FormLifeCycle, FormPath } from '../index' +import { + createForm, + LifeCycleTypes, + FormLifeCycle, + FormPath, + registerValidationFormats, + registerValidationRules, + registerValidationMTEngine +} from '../index' import { ValidateDescription, ValidatePatternRules } from '@uform/validator' // mock datasource @@ -595,14 +603,14 @@ describe('setFormState', () => { form.setFormState(state => { state.values = { a: '1234' } }) - expect(form.getFormState((state) => state.values)).toEqual({ a: '1234' }) + expect(form.getFormState(state => state.values)).toEqual({ a: '1234' }) expect(fieldChange).toBeCalledTimes(2) expect(formChange).toBeCalledTimes(2) - form.setFormState((state) => state.values = { a: '5678' }, true) - expect(form.getFormState((state) => state.values)).toEqual({ a: '5678' }) + form.setFormState(state => (state.values = { a: '5678' }), true) + expect(form.getFormState(state => state.values)).toEqual({ a: '5678' }) expect(formChange).toBeCalledTimes(2) - expect(fieldChange).toBeCalledTimes(2) + expect(fieldChange).toBeCalledTimes(2) }) }) @@ -650,18 +658,18 @@ describe('setFieldState', () => { const fieldChange = jest.fn() const form = createForm({ lifecycles: [ - new FormLifeCycle(LifeCycleTypes.ON_FIELD_CHANGE, fieldChange), + new FormLifeCycle(LifeCycleTypes.ON_FIELD_CHANGE, fieldChange) ] }) form.registerField({ path: 'a' }) - form.getFieldState('a') + form.getFieldState('a') form.setFieldState('a', state => (state.value = '1234')) expect(form.getFieldState('a', state => state.value)).toEqual('1234') expect(fieldChange).toBeCalledTimes(2) form.setFieldState('a', state => (state.value = '5678'), true) expect(form.getFieldState('a', state => state.value)).toEqual('5678') - expect(fieldChange).toBeCalledTimes(2) + expect(fieldChange).toBeCalledTimes(2) }) test('validating and loading', () => { @@ -1089,6 +1097,16 @@ describe('createMutators', () => { })) ).toEqual({ active: true, + visited: false + }) + mutators.blur() + expect( + form.getFieldState('a', state => ({ + active: state.active, + visited: state.visited + })) + ).toEqual({ + active: false, visited: true }) }) @@ -1300,7 +1318,10 @@ describe('major sences', () => { test('dynamic remove with intialValues', async () => { const form = createForm({ initialValues: { - aa: [{ aa: 123, bb: 321 }, { aa: 345, bb: 678 }] + aa: [ + { aa: 123, bb: 321 }, + { aa: 345, bb: 678 } + ] } }) const aa = form.registerField({ path: 'aa' }) @@ -1398,3 +1419,128 @@ describe('major sences', () => { expect(form.getFormGraph()).toMatchSnapshot() }) }) + +describe('validator', () => { + test('registerValidationFormats', async () => { + registerValidationFormats({ + number: /^[+-]?\d+(\.\d+)?$/ + }) + + const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + // console.log(values) + } + }) + + const aa = form.registerField({ + path: 'aa', + rules: [ + { + format: 'number', + message: 'This field is not a number.' + } + ] + }) + + aa.setState(state => { + state.value = 'hello world' + }) + await form.validate() + + form.getFormState(state => + expect(state.errors).toEqual([ + { + path: 'aa', + messages: ['This field is not a number.'] + } + ]) + ) + }) + + test('registerValidationRules', async () => { + registerValidationRules({ + custom: value => { + return value === '123' ? 'This field can not be 123' : '' + } + }) + + const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + // console.log(values) + } + }) + + const aa = form.registerField({ + path: 'aa', + rules: [ + { + custom: true + } + ] + }) + + aa.setState(state => { + state.value = '123' + }) + await form.validate() + + form.getFormState(state => + expect(state.errors).toEqual([ + { + path: 'aa', + messages: ['This field can not be 123'] + } + ]) + ) + }) + + test('registerValidationMTEngine', async () => { + registerValidationMTEngine((message, context) => { + return message.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, $0) => { + return FormPath.getIn(context, $0) + }) + }) + + const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + // console.log(values) + } + }) + + const aa = form.registerField({ + path: 'aa', + rules: [ + { + validator(value) { + return value === 123 + ? 'This field can not be 123 {{scope.outerVariable}}' + : '' + }, + scope: { + outerVariable: 'addonAfter' + } + } + ] + }) + + aa.setState(state => { + state.value = 123 + }) + await form.validate() + + form.getFormState(state => + expect(state.errors).toEqual([ + { + path: 'aa', + messages: ['This field can not be 123 addonAfter'] + } + ]) + ) + }) +}) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 38bd8afd1fc..40a70d362b7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -14,7 +14,8 @@ import { import { FormValidator, setValidationLanguage, - setValidationLocale + setValidationLocale, + ValidateFieldOptions } from '@uform/validator' import { FormHeart } from './shared/lifecycle' import { FormGraph } from './shared/graph' @@ -545,12 +546,12 @@ export function createForm( focus() { field.setState((state: IFieldState) => { state.active = true - state.visited = true }) }, blur() { field.setState((state: IFieldState) => { state.active = false + state.visited = true }) }, push(value?: any) { @@ -752,7 +753,7 @@ export function createForm( async function validate( path?: FormPathPattern, - opts?: {} + opts?: ValidateFieldOptions ): Promise { if (!state.getState(state => state.validating)) { state.unsafe_setSourceState(state => { diff --git a/packages/core/src/shared/model.ts b/packages/core/src/shared/model.ts index 8fc79fe882e..0095cb7c4ba 100644 --- a/packages/core/src/shared/model.ts +++ b/packages/core/src/shared/model.ts @@ -9,13 +9,19 @@ import { FormPathPattern } from '@uform/shared' import produce, { Draft } from 'immer' -import { IStateModelFactory, StateDirtyMap, IModel, StateModel } from '../types' +import { + IStateModelProvider, + IStateModelFactory, + StateDirtyMap, + IModel, + StateModel +} from '../types' const hasProxy = !!globalThisPolyfill.Proxy export const createStateModel = ( Factory: IStateModelFactory -) => { - return class Model extends Subscribable +): IStateModelProvider => { + return class Model extends Subscribable implements IModel { public state: State & { displayName?: string } public props: Props & @@ -218,5 +224,5 @@ export const createStateModel = ( ) : !isEqual(this.prevState, this.state) } - } + } as any } diff --git a/packages/core/src/state/form.ts b/packages/core/src/state/form.ts index 9a2010a0d7a..6920e1c7e80 100644 --- a/packages/core/src/state/form.ts +++ b/packages/core/src/state/form.ts @@ -55,10 +55,12 @@ export const FormState = createStateModel( } else { draft.pristine = false } - if (draft.validating === true) { - draft.loading = true - } else if (draft.validating === false) { - draft.loading = false + if (draft.validating !== prevState.validating) { + if (draft.validating === true) { + draft.loading = true + } else if (draft.validating === false) { + draft.loading = false + } } if (draft.mounted === true && draft.mounted !== prevState.mounted) { draft.unmounted = false diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index ceef3f9ce7b..223e8df8a58 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,5 +1,9 @@ import { FormPath, FormPathPattern, isFn, Subscribable } from '@uform/shared' -import { ValidatePatternRules, ValidateNodeResult } from '@uform/validator' +import { + ValidatePatternRules, + ValidateNodeResult, + ValidateFieldOptions +} from '@uform/validator' import { FormLifeCycle } from './shared/lifecycle' import { Draft } from 'immer' @@ -105,6 +109,10 @@ export interface IStateModelFactory { displayName?: string } +export interface IStateModelProvider { + new (props: P): IModel +} + export interface IFieldState { displayName?: string name: string @@ -155,7 +163,6 @@ export interface IFieldStateProps { editable?: boolean useDirty?: boolean computeState?: (draft: IFieldState, prevState: IFieldState) => void - onChange?: (fieldState: IField) => void } export const isField = (target: any): target is IField => @@ -208,13 +215,12 @@ export interface IFormStateProps { initialValues?: {} values?: {} lifecycles?: FormLifeCycle[] + useDirty?: boolean editable?: boolean | ((name: string) => boolean) + validateFirst?: boolean } export interface IFormCreatorOptions extends IFormStateProps { - useDirty?: boolean - validateFirst?: boolean - editable?: boolean onChange?: (values: IFormState['values']) => void onSubmit?: (values: IFormState['values']) => any | Promise onReset?: () => void @@ -245,7 +251,6 @@ export interface IVirtualFieldStateProps { ) => void name?: string props?: FieldProps - onChange?: (fieldState: IVirtualField) => void } export type IFormValidateResult = ValidateNodeResult @@ -275,9 +280,9 @@ export interface IMutators { remove(index: number | string): any unshift(value: any): any[] shift(): any[] - move($from: number, $to: number): any - moveDown(index: number): any - moveUp(index: number): any + move($from: number, $to: number): any[] + moveDown(index: number): any[] + moveUp(index: number): any[] validate(): Promise exist(index?: number | string): boolean } @@ -297,7 +302,7 @@ export interface IModel extends Subscribable { setState: (callback?: (state: S | Draft) => void, silent?: boolean) => void unsafe_getSourceState: (callback?: (state: S) => any) => any unsafe_setSourceState: (callback?: (state: S) => void) => void - hasChanged: (key?: string) => boolean + hasChanged: (path?: FormPathPattern) => boolean isDirty: (key?: string) => boolean getDirtyInfo: () => StateDirtyMap } @@ -315,7 +320,10 @@ export interface IForm { clearErrors: (pattern?: FormPathPattern) => void hasChanged(target: any, path: FormPathPattern): boolean reset(options?: IFormResetOptions): Promise - validate(path?: FormPathPattern, options?: {}): Promise + validate( + path?: FormPathPattern, + options?: ValidateFieldOptions + ): Promise setFormState(callback?: (state: IFormState) => any, silent?: boolean): void getFormState(callback?: (state: IFormState) => any): any setFieldState( diff --git a/packages/validator/src/rules.ts b/packages/validator/src/rules.ts index dc28bf6786a..e28342d1465 100644 --- a/packages/validator/src/rules.ts +++ b/packages/validator/src/rules.ts @@ -26,6 +26,9 @@ const getLength = (value: any) => const getRuleMessage = (rule: any, type: string) => { if (isFn(rule.validator) || Object.keys(rule).length > 2) { + if (rule.format) { + return rule.message || getMessage(type) + } return getMessage(type) } else { return rule.message || getMessage(type) diff --git a/packages/validator/src/types.ts b/packages/validator/src/types.ts index 4d2b5673e1c..98319e8b9fb 100644 --- a/packages/validator/src/types.ts +++ b/packages/validator/src/types.ts @@ -32,6 +32,7 @@ export interface ValidateDescription { whitespace?: boolean enum?: any[] message?: string + [key: string]: any } export type ValidateRules = ValidateDescription[] @@ -73,7 +74,6 @@ export type ValidateRulesMap = { export interface ValidateFieldOptions { first?: boolean - key?: string } export type ValidateCalculator = ( diff --git a/packages/validator/src/validator.ts b/packages/validator/src/validator.ts index 6ce515eb100..233f2177dac 100644 --- a/packages/validator/src/validator.ts +++ b/packages/validator/src/validator.ts @@ -38,7 +38,7 @@ const template = (message: SyncValidateResponse, context: any): string => { if (isFn(FormValidator.template)) { return FormValidator.template(message, context) } - return message.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, $0) => { + return message.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, $0) => { return FormPath.getIn(context, $0) }) } else if (isObj(message)) { @@ -75,7 +75,7 @@ class FormValidator { } ] } else if (isArr(rules)) { - return rules.reduce((buf, rule) => { + return rules.reduce((buf: any, rule) => { return buf.concat(this.transformRules(rule)) }, []) } else if (isObj(rules)) { @@ -117,8 +117,7 @@ class FormValidator { const payload = await rule(value, ruleObj) const message = template(payload, { ...ruleObj, - value, - key: options.key + value }) if (isStr(payload)) { if (first) { From 57ecf3e4fc0267725ab1cefdd6ea83765933c66c Mon Sep 17 00:00:00 2001 From: Janry Date: Thu, 14 Nov 2019 16:05:12 +0800 Subject: [PATCH 40/99] docs: improve docs (#396) --- package.json | 1 + packages/core/README.md | 33 +++++++++++++++++++++++++++++++++ packages/core/README.zh-cn.md | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/package.json b/package.json index be354c04d7f..14140f78b56 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "lerna": "^3.10.1", "lint-staged": "^8.2.1", "majo": "^0.7.1", + "markdown-toc": "^1.2.0", "moment": "^2.24.0", "onchange": "^5.2.0", "prettier": "^1.18.2", diff --git a/packages/core/README.md b/packages/core/README.md index 521a0295080..3cb40412566 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -9,6 +9,39 @@ English | [简体中文](./README.zh-cn.md) > - Manage the Validator status > - Manage dependencies between Form, Field, and Vaidator +### Table Of Contents + + + +- [Backdrop](#backdrop) +- [Design Concept](#design-concept) +- [Core highlights](#core-highlights) +- [Architecture diagram](#architecture-diagram) +- [Terminology explanation](#terminology-explanation) +- [API](#api) + * [`createForm`](#createform) + * [`registerValidationFormats`](#registervalidationformats) + * [`registerValidationRules`](#registervalidationrules) + * [`registerValidationMTEngine`](#registervalidationmtengine) + * [`setValidationLanguage`](#setvalidationlanguage) + * [`setValidationLocale`](#setvalidationlocale) +- [Classes](#classes) + * [`new FormPath()`](#new-formpath) + * [`new FormLifeCyle()`](#new-formlifecyle) +- [Enums](#enums) + * [Lifecycletypes](#lifecycletypes) +- [Interfaces](#interfaces) + * [Iformcreatoroptions](#iformcreatoroptions) + * [Iform](#iform) + * [Imutators](#imutators) + * [The Validator](#the-validator) + * [IFormState](#iformstate) + * [IFieldState](#ifieldstate) + * [IVirtualFieldState](#ivirtualfieldstate) + * [IField/IVirtualField](#ifieldivirtualfield) + + + ### Backdrop In the middle and back-end field, the core is two scenarios, **One is data entry, the other is Data Query + data presentation** , Whether it is data entry or data query, it is implemented by using forms. Only the forms of data presentation are diversified, but they are the easiest to reuse and abstract. Only forms, it involves a lot of Interactive Logic. Therefore, as long as we fundamentally solve the form problem, most of the middle and back-end scenarios are basically solved. UForm is born for this. diff --git a/packages/core/README.zh-cn.md b/packages/core/README.zh-cn.md index 6a1020cf23e..d019a470733 100644 --- a/packages/core/README.zh-cn.md +++ b/packages/core/README.zh-cn.md @@ -6,6 +6,41 @@ > - 管理Validator状态 > - 管理Form/Field/Vaidator之间的依赖关系 + + +### 目录 + + + +- [背景](#%E8%83%8C%E6%99%AF) +- [设计理念](#%E8%AE%BE%E8%AE%A1%E7%90%86%E5%BF%B5) +- [核心亮点](#%E6%A0%B8%E5%BF%83%E4%BA%AE%E7%82%B9) +- [架构图](#%E6%9E%B6%E6%9E%84%E5%9B%BE) +- [术语解释](#%E6%9C%AF%E8%AF%AD%E8%A7%A3%E9%87%8A) +- [API](#api) + * [`createForm`](#createform) + * [`registerValidationFormats`](#registervalidationformats) + * [`registerValidationRules`](#registervalidationrules) + * [`registerValidationMTEngine`](#registervalidationmtengine) + * [`setValidationLanguage`](#setvalidationlanguage) + * [`setValidationLocale`](#setvalidationlocale) +- [Classes](#classes) + * [`new FormPath()`](#new-formpath) + * [`new FormLifeCyle()`](#new-formlifecyle) +- [Enums](#enums) + * [LifeCycleTypes](#lifecycletypes) +- [Interfaces](#interfaces) + * [IFormCreatorOptions](#iformcreatoroptions) + * [IForm](#iform) + * [Imutators](#imutators) + * [Validator](#validator) + * [IFormState](#iformstate) + * [IFieldState](#ifieldstate) + * [IVirtualFieldState](#ivirtualfieldstate) + * [IField/IVirtualField](#ifieldivirtualfield) + + + ### 背景 中后台领域,核心就是两种场景,**一个是数据录入,一个是数据查询+数据展现**,不管是数据录入还是数据查询,都是借助表单来实现,只有数据展现的形式是比较多样化的,但是却是最容易复用和抽象的,只有表单,会涉及大量的交互逻辑,所以,只要我们根本上解决了表单问题,对于中后台场景,基本上解决了大部分中后台场景问题,UForm,就是为此而诞生的。 From 88fd3eb796fb09b19e62fc6592cefc4d694b9c99 Mon Sep 17 00:00:00 2001 From: Janry Date: Thu, 14 Nov 2019 17:08:28 +0800 Subject: [PATCH 41/99] Fix docs typo (#397) --- packages/core/README.md | 23 +++++++++++++++-------- packages/core/README.zh-cn.md | 6 +++++- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/core/README.md b/packages/core/README.md index 3cb40412566..7bec262c393 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,4 +1,4 @@ -# @ Uform/core +# @uform/core English | [简体中文](./README.zh-cn.md) @@ -9,6 +9,13 @@ English | [简体中文](./README.zh-cn.md) > - Manage the Validator status > - Manage dependencies between Form, Field, and Vaidator +### Install + +```bash +npm install --save @uform/core +``` + + ### Table Of Contents @@ -31,10 +38,10 @@ English | [简体中文](./README.zh-cn.md) - [Enums](#enums) * [Lifecycletypes](#lifecycletypes) - [Interfaces](#interfaces) - * [Iformcreatoroptions](#iformcreatoroptions) - * [Iform](#iform) + * [IFormCreatorOptions](#iformcreatoroptions) + * [IForm](#iform) * [Imutators](#imutators) - * [The Validator](#the-validator) + * [Validation](#the-validator) * [IFormState](#iformstate) * [IFieldState](#ifieldstate) * [IVirtualFieldState](#ivirtualfieldstate) @@ -44,7 +51,7 @@ English | [简体中文](./README.zh-cn.md) ### Backdrop -In the middle and back-end field, the core is two scenarios, **One is data entry, the other is Data Query + data presentation** , Whether it is data entry or data query, it is implemented by using forms. Only the forms of data presentation are diversified, but they are the easiest to reuse and abstract. Only forms, it involves a lot of Interactive Logic. Therefore, as long as we fundamentally solve the form problem, most of the middle and back-end scenarios are basically solved. UForm is born for this. +In the middle and back-end field, the core is two scenarios, **one is data entry, one is data query + data presentation**, whether it is data entry or data query, it is realized by means of form, from the perspective of implementation complexity, The complexity of the two is similar, because the data rendering level will inevitably have extremely complex renderings (such as Tree Table, etc.), but the data rendering is the easiest to reuse and abstract, only the form, will involve a lot of interactive logic So, as long as we solve the form problem fundamentally, for the mid- and back-stage scenes, most of the mid- and back-stage scene problems are solved. UForm is born for this purpose. @@ -580,7 +587,7 @@ enum LifeCycleTypes { ### Interfaces -#### Iformcreatoroptions +#### IFormCreatorOptions @@ -625,7 +632,7 @@ interface IFormCreatorOptions { -#### Iform +#### IForm @@ -869,7 +876,7 @@ interface IMutators { -#### The Validator +#### Validation diff --git a/packages/core/README.zh-cn.md b/packages/core/README.zh-cn.md index d019a470733..04f00280711 100644 --- a/packages/core/README.zh-cn.md +++ b/packages/core/README.zh-cn.md @@ -6,7 +6,11 @@ > - 管理Validator状态 > - 管理Form/Field/Vaidator之间的依赖关系 +### 安装 +```bash +npm install --save @uform/core +``` ### 目录 @@ -43,7 +47,7 @@ ### 背景 -中后台领域,核心就是两种场景,**一个是数据录入,一个是数据查询+数据展现**,不管是数据录入还是数据查询,都是借助表单来实现,只有数据展现的形式是比较多样化的,但是却是最容易复用和抽象的,只有表单,会涉及大量的交互逻辑,所以,只要我们根本上解决了表单问题,对于中后台场景,基本上解决了大部分中后台场景问题,UForm,就是为此而诞生的。 +中后台领域,核心就是两种场景,**一个是数据录入,一个是数据查询+数据展现**,不管是数据录入还是数据查询,都是借助表单来实现的,从实现复杂度来看,两者复杂度相差不多,因为数据呈现层面难免会有极度复杂的呈现形式(比如Tree Table等等),但是,数据呈现却是最容易复用和抽象的,只有表单,会涉及大量的交互逻辑,所以,只要我们根本上解决了表单问题,对于中后台场景,基本上解决了大部分中后台场景问题,UForm,就是为此而诞生的。 ### 设计理念 From 66e03bc11b46741fa0d0fbb5e6df363a7ba644f1 Mon Sep 17 00:00:00 2001 From: Janry Date: Thu, 14 Nov 2019 19:34:29 +0800 Subject: [PATCH 42/99] fix(@uform/antd/next): fix typings (#398) --- packages/antd/src/compat/Form.tsx | 2 +- packages/antd/src/compat/FormItem.tsx | 2 +- packages/antd/src/compat/context.tsx | 8 ++----- packages/antd/src/compat/index.ts | 8 +++---- packages/antd/src/index.tsx | 6 +++--- packages/antd/src/types.ts | 4 ++-- .../src/components/SchemaField.tsx | 21 ++++++++----------- packages/react-schema-renderer/src/index.tsx | 2 ++ 8 files changed, 24 insertions(+), 29 deletions(-) diff --git a/packages/antd/src/compat/Form.tsx b/packages/antd/src/compat/Form.tsx index 521739893c5..cbc0d528f17 100644 --- a/packages/antd/src/compat/Form.tsx +++ b/packages/antd/src/compat/Form.tsx @@ -5,7 +5,7 @@ import { IFormItemTopProps } from '../types' import { FormItemProvider } from './context' import { normalizeCol } from '../shared' -export const CompatNextForm: React.FC< +export const CompatAntdForm: React.FC< FormProps & IFormItemTopProps > = props => { return ( diff --git a/packages/antd/src/compat/FormItem.tsx b/packages/antd/src/compat/FormItem.tsx index 67f19610e7f..67d38bcf8e0 100644 --- a/packages/antd/src/compat/FormItem.tsx +++ b/packages/antd/src/compat/FormItem.tsx @@ -71,7 +71,7 @@ export const FormItemProps = ({ children, ...props }) => ( ) -export const CompatNextFormItem: React.FC = props => { +export const CompatAntdFormItem: React.FC = props => { const { prefixCls, labelAlign, labelCol, wrapperCol } = useFormItem() const help = computeHelp(props) const label = computeLabel(props) diff --git a/packages/antd/src/compat/context.tsx b/packages/antd/src/compat/context.tsx index e3f38383c73..8905ea7ff08 100644 --- a/packages/antd/src/compat/context.tsx +++ b/packages/antd/src/compat/context.tsx @@ -5,22 +5,18 @@ const FormItemContext = createContext({}) export const FormItemProvider: React.FC = ({ children, - prefix, - size, + prefixCls, labelAlign, labelCol, inline, - labelTextAlign, wrapperCol }) => ( diff --git a/packages/antd/src/compat/index.ts b/packages/antd/src/compat/index.ts index 1eca2fb65ed..8afea05f282 100644 --- a/packages/antd/src/compat/index.ts +++ b/packages/antd/src/compat/index.ts @@ -2,9 +2,9 @@ import { registerFormComponent, registerFormItemComponent } from '@uform/react-schema-renderer' -import { CompatNextForm } from './Form' -import { CompatNextFormItem } from './FormItem' +import { CompatAntdForm } from './Form' +import { CompatAntdFormItem } from './FormItem' -registerFormComponent(CompatNextForm) +registerFormComponent(CompatAntdForm) -registerFormItemComponent(CompatNextFormItem) +registerFormItemComponent(CompatAntdFormItem) diff --git a/packages/antd/src/index.tsx b/packages/antd/src/index.tsx index 7056bbbed78..0cda03521a4 100644 --- a/packages/antd/src/index.tsx +++ b/packages/antd/src/index.tsx @@ -3,13 +3,13 @@ import { SchemaMarkupForm, SchemaMarkupField } from '@uform/react-schema-renderer' -import { INextSchemaFormProps, INextSchemaFieldProps } from './types' +import { IAntdSchemaFormProps, IAntdSchemaFieldProps } from './types' import './fields' import './compat' export * from '@uform/react-schema-renderer' export * from './components' export * from './types' export { mapStyledProps, mapTextComponent } from './shared' -export const SchemaForm: React.FC = SchemaMarkupForm as any -export const Field: React.FC = SchemaMarkupField +export const SchemaForm: React.FC = SchemaMarkupForm as any +export const Field: React.FC = SchemaMarkupField export default SchemaForm diff --git a/packages/antd/src/types.ts b/packages/antd/src/types.ts index 879cfba78ae..1862a88a983 100644 --- a/packages/antd/src/types.ts +++ b/packages/antd/src/types.ts @@ -13,11 +13,11 @@ import { StyledComponent } from 'styled-components' type ColSpanType = number | string -export type INextSchemaFormProps = ISchemaFormProps & +export type IAntdSchemaFormProps = ISchemaFormProps & FormProps & IFormItemTopProps -export type INextSchemaFieldProps = IMarkupSchemaFieldProps +export type IAntdSchemaFieldProps = IMarkupSchemaFieldProps export interface ISubmitProps extends ButtonProps { onSubmit?: ISchemaFormProps['onSubmit'] diff --git a/packages/react-schema-renderer/src/components/SchemaField.tsx b/packages/react-schema-renderer/src/components/SchemaField.tsx index 379ac3c4b0a..82a2c2fd7a9 100644 --- a/packages/react-schema-renderer/src/components/SchemaField.tsx +++ b/packages/react-schema-renderer/src/components/SchemaField.tsx @@ -133,16 +133,13 @@ export const SchemaField: React.FunctionComponent = ( return renderFieldDelegate(props => { const stateComponent = props.schema.getExtendsComponent() || props.schema.type + if (!isStr(stateComponent)) + throw new Error('Can not found any form components.') const renderComponent = (): React.ReactElement => React.createElement( formRegistry.formItemComponent, props, - isStr(stateComponent) - ? React.createElement( - formRegistry.fields[stateComponent], - props - ) - : React.createElement(stateComponent, props) + React.createElement(formRegistry.fields[stateComponent], props) ) if (isFn(schemaRenderer)) { return schemaRenderer({ ...props, renderComponent }) @@ -153,13 +150,13 @@ export const SchemaField: React.FunctionComponent = ( return renderVirtualFieldDelegate(props => { const stateComponent = props.schema.getExtendsComponent() || props.schema.type + if (!isStr(stateComponent)) + throw new Error('Can not found any form components.') const renderComponent = () => - isStr(stateComponent) - ? React.createElement( - formRegistry.virtualFields[stateComponent], - props - ) - : React.createElement(stateComponent, props) + React.createElement( + formRegistry.virtualFields[stateComponent], + props + ) if (isFn(schemaRenderer)) { return schemaRenderer({ ...props, renderComponent }) diff --git a/packages/react-schema-renderer/src/index.tsx b/packages/react-schema-renderer/src/index.tsx index 118b190a965..8b2cc7cd7e6 100644 --- a/packages/react-schema-renderer/src/index.tsx +++ b/packages/react-schema-renderer/src/index.tsx @@ -2,6 +2,7 @@ import { createAsyncSchemaFormActions, createSchemaFormActions } from './shared/actions' +import { Field as InternalField, Form as InternalForm } from '@uform/react' export * from '@uform/react' export * from './components/SchemaField' export * from './components/SchemaForm' @@ -14,3 +15,4 @@ export * from './types' export const createFormActions = createSchemaFormActions export const createAsyncFormActions = createAsyncSchemaFormActions +export { InternalField, InternalForm } From 4cf4bb8a34b7c7377261884f13949cead067de47 Mon Sep 17 00:00:00 2001 From: quirkyvar Date: Sat, 16 Nov 2019 23:54:10 +0800 Subject: [PATCH 43/99] feature(@uform/react): finish hook unittest (#400) --- package.json | 2 +- packages/antd/src/index.tsx | 4 +- packages/next/src/index.tsx | 4 +- packages/react/src/__tests__/actions.spec.tsx | 4 +- packages/react/src/__tests__/form.spec.tsx | 4 +- .../react/src/__tests__/useDirty.spec.tsx | 24 +++ .../react/src/__tests__/useField.spec.tsx | 202 ++++++++++++++++-- .../src/__tests__/useVirtualField.spec.tsx | 105 +++++++++ packages/react/src/components/Field.tsx | 4 +- packages/react/src/hooks/useField.ts | 9 +- packages/react/src/types.ts | 2 +- 11 files changed, 335 insertions(+), 29 deletions(-) create mode 100644 packages/react/src/__tests__/useVirtualField.spec.tsx diff --git a/package.json b/package.json index 14140f78b56..cb9f4d0a911 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "sort-api": "node ./scripts/sort-api-table.js", "test": "npm run lint && jest", "test:core": "jest --watch packages/core/src/__tests__/*.spec.ts", - "test:hook": "jest --watch packages/react/src/__tests__/*.spec.ts", + "test:hook": "jest --watch packages/react/src/__tests__/*.spec.tsx", "test:prod": "cross-env TEST_ENV=production npm run build && jest", "doc:core": "doc-scripts start -i packages/core", "doc:react": "doc-scripts start -i packages/react", diff --git a/packages/antd/src/index.tsx b/packages/antd/src/index.tsx index 0cda03521a4..650d557f424 100644 --- a/packages/antd/src/index.tsx +++ b/packages/antd/src/index.tsx @@ -10,6 +10,8 @@ export * from '@uform/react-schema-renderer' export * from './components' export * from './types' export { mapStyledProps, mapTextComponent } from './shared' -export const SchemaForm: React.FC = SchemaMarkupForm as any +export const SchemaForm: React.FC< + IAntdSchemaFormProps +> = SchemaMarkupForm as any export const Field: React.FC = SchemaMarkupField export default SchemaForm diff --git a/packages/next/src/index.tsx b/packages/next/src/index.tsx index 7056bbbed78..85d9569b902 100644 --- a/packages/next/src/index.tsx +++ b/packages/next/src/index.tsx @@ -10,6 +10,8 @@ export * from '@uform/react-schema-renderer' export * from './components' export * from './types' export { mapStyledProps, mapTextComponent } from './shared' -export const SchemaForm: React.FC = SchemaMarkupForm as any +export const SchemaForm: React.FC< + INextSchemaFormProps +> = SchemaMarkupForm as any export const Field: React.FC = SchemaMarkupField export default SchemaForm diff --git a/packages/react/src/__tests__/actions.spec.tsx b/packages/react/src/__tests__/actions.spec.tsx index 86e253f63b0..2de21ea76b0 100644 --- a/packages/react/src/__tests__/actions.spec.tsx +++ b/packages/react/src/__tests__/actions.spec.tsx @@ -5,11 +5,11 @@ import { Field, createFormActions, createAsyncFormActions, - IFieldProps + IFieldStateUIProps } from '../index' import { IFormActions, IFormAsyncActions } from '../types' -const Input: React.FC = props => ( +const Input: React.FC = props => ( {({ state, mutators }) => (
    diff --git a/packages/react/src/__tests__/form.spec.tsx b/packages/react/src/__tests__/form.spec.tsx index 1e936e8a3d9..725ca09efbf 100644 --- a/packages/react/src/__tests__/form.spec.tsx +++ b/packages/react/src/__tests__/form.spec.tsx @@ -4,11 +4,11 @@ import { Field, createFormActions, FormEffectHooks, - IFieldProps + IFieldStateUIProps } from '../index' import { render } from '@testing-library/react' -const Input: React.FC = props => ( +const Input: React.FC = props => ( {({ state, mutators }) => (
    diff --git a/packages/react/src/__tests__/useDirty.spec.tsx b/packages/react/src/__tests__/useDirty.spec.tsx index 3e6603b7f78..01aad8a7236 100644 --- a/packages/react/src/__tests__/useDirty.spec.tsx +++ b/packages/react/src/__tests__/useDirty.spec.tsx @@ -59,4 +59,28 @@ describe('useDirty hook',()=>{ data: dirtyProps, }) }) + + // test('fields change(object)', ()=>{ + // let dirtyProps = { user: { username: 'abcd' } } + // const { result, rerender } = renderHook(() => useDirty(dirtyProps, ['user'])) + // expect(result.current).toEqual({ + // num: 0, + // dirtys: { + // user: false, + // }, + // data: dirtyProps, + // }) + + // dirtyProps.user.username = 'edgh' + // rerender() + + // expect(result.current).toEqual({ + // num: 1, + // dirtys: { + // user: true + // }, + // data: dirtyProps, + // }) + // }) + }) \ No newline at end of file diff --git a/packages/react/src/__tests__/useField.spec.tsx b/packages/react/src/__tests__/useField.spec.tsx index cfbcecec706..8523193c940 100644 --- a/packages/react/src/__tests__/useField.spec.tsx +++ b/packages/react/src/__tests__/useField.spec.tsx @@ -1,21 +1,195 @@ -describe('test all apis',()=>{ - //todo - test('basic',()=>{ - //todo +import React from 'react' +import { act, renderHook } from '@testing-library/react-hooks' +import FormContext from '../context'; +import useForm from '../hooks/useForm' +import useField from '../hooks/useField' +import { createForm } from '..'; +import { FormLifeCycle, LifeCycleTypes } from '@uform/core'; + +describe('useField hook',()=>{ + test('form is required', ()=>{ + expect(() => { + useField({}) + }).toThrow() + }) + + test('basic ', ()=>{ + let globalForm + let globalGraph + const formInstance = createForm({ + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FORM_GRAPH_CHANGE, (graph) => { + globalGraph = graph + }) + ] + }) + const formWrapper = ({ children }) => { + const form = useForm({ + form: formInstance + }) + globalForm = form + return {children} + } + + const { result } = renderHook(() => useField({ name: 'username' }), { wrapper: formWrapper }) + expect(result.current.form).toEqual(globalForm) + expect(result.current.state.props).toEqual({}) + expect(result.current.state).toEqual({ + ...globalGraph.get('username').getState(), + errors: '', + mounted: false, + }) + }) + + test('update', async ()=>{ + let globalForm + const formWrapper = ({ children }) => { + const form = useForm({}) + globalForm = form + return {children} + } + + const { result, waitForNextUpdate } = renderHook(() => useField({ name: 'username' }), { wrapper: formWrapper }) + expect(result.current.state.value).toEqual(undefined) + act(() => { + globalForm.setFormState(state => state.values.username = 'abcd') + }) + + // forceUpdate will trigger in raf, use waitForNextUpdate + expect(result.current.state.value).toEqual(undefined) + await waitForNextUpdate() + expect(result.current.state.value).toEqual('abcd') + }) + + test('mounted change', async ()=>{ + const formWrapper = ({ children }) => { + const form = useForm({}) + return {children} + } + + const { result, rerender } = renderHook(() => useField({ name: 'username' }), { wrapper: formWrapper }) + expect(result.current.state.mounted).toEqual(false) + rerender() + expect(result.current.state.mounted).toEqual(true) + }) + + test('dirty', async ()=>{ + const formWrapper = ({ children }) => { + const formInstance = createForm({ + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FIELD_CHANGE, (field) => { + + }) + ] + }) + const form = useForm({ + form: formInstance + }) + return {children} + } + + const initialProps = { + name: 'username', + props: { disabled: true }, + required: false, + editable: true, + rules: [], + } + const { result, waitForNextUpdate, rerender } = renderHook(() => useField(initialProps), { wrapper: formWrapper }) + expect(result.current.props).toEqual({ disabled: true }) + expect(result.current.state.required).toEqual(false) + expect(result.current.state.editable).toEqual(true) + expect(result.current.state.rules).toEqual([]) + + initialProps.required = true + initialProps.editable = false + initialProps.props = { disabled: false } + initialProps.rules = [() => ({ type: 'warning', message: 'warning msg' })] + + expect(result.current.props).toEqual({ disabled: true }) + expect(result.current.state.required).toEqual(false) + expect(result.current.state.editable).toEqual(true) + expect(result.current.state.rules).toEqual([]) + + rerender() + await waitForNextUpdate() + + expect(result.current.props).toEqual(initialProps.props) + expect(result.current.state.required).toEqual(initialProps.required) + expect(result.current.state.editable).toEqual(initialProps.editable) + expect(result.current.state.rules).toEqual([...initialProps.rules, { required: true }]) + }) + + test('extented mutator', ()=>{ + const formWrapper = ({ children }) => { + const form = useForm({}) + return {children} + } + + const getValueFromEvent = (e) => (e.target.value) + const eventValue = { target: { value: 'abc' }} + const { result: result1, rerender: rerender1 } = renderHook(() => useField({ name: 'username' }), { wrapper: formWrapper }) + expect(result1.current.state.value).toEqual(undefined) + act(() => { + result1.current.mutators.change(eventValue) + }) + rerender1() + expect(result1.current.state.value).toEqual(eventValue) + + const { result: result2, rerender: rerender2 } = renderHook(() => useField({ name: 'username', getValueFromEvent }), { wrapper: formWrapper }) + expect(result2.current.state.value).toEqual(undefined) + act(() => { + result2.current.mutators.change(eventValue) + }) + rerender2() + expect(result2.current.state.value).toEqual('abc') }) -}) -describe('major scenes',()=>{ - //todo - test('basic',()=>{ - //todo + test('triggerType mutator onChange', async()=>{ + const formWrapper = ({ children }) => { + const form = useForm({}) + return {children} + } + + const fieldProps = { name: 'username', required: true } + const { result: result1, waitForNextUpdate: waitForNextUpdate1 } = renderHook(() => useField(fieldProps), { wrapper: formWrapper }) + expect(result1.current.state.errors).toEqual('') + expect(result1.current.state.value).toEqual(undefined) + result1.current.mutators.change('') + await waitForNextUpdate1() + expect(result1.current.state.value).toEqual('') + expect(result1.current.state.errors).toEqual('') + + const { result: result2, waitForNextUpdate: waitForNextUpdate2 } = renderHook(() => useField({ ...fieldProps, triggerType: 'onChange' }), { wrapper: formWrapper }) + expect(result2.current.state.errors).toEqual('') + expect(result2.current.state.value).toEqual(undefined) + + result2.current.mutators.change('') + await waitForNextUpdate2() + expect(result2.current.state.value).toEqual('') + expect(result2.current.state.errors).toEqual('This field is required') }) -}) + test('triggerType mutator onBlur', async()=>{ + const formWrapper = ({ children }) => { + const form = useForm({}) + return {children} + } + + const fieldProps = { name: 'username', required: true } + const { result: result1, rerender } = renderHook(() => useField(fieldProps), { wrapper: formWrapper }) + expect(result1.current.state.errors).toEqual('') + result1.current.mutators.blur() + rerender() + expect(result1.current.state.errors).toEqual('') + + const { result: result2, waitForNextUpdate: waitForNextUpdate2 } = renderHook(() => useField({ ...fieldProps, triggerType: 'onBlur' }), { wrapper: formWrapper }) + expect(result2.current.state.errors).toEqual('') -describe('bugfix',()=>{ - //todo - test('basic',()=>{ - //todo + act(() => { + result2.current.mutators.blur() + }) + await waitForNextUpdate2() + expect(result2.current.state.errors).toEqual('This field is required') }) }) \ No newline at end of file diff --git a/packages/react/src/__tests__/useVirtualField.spec.tsx b/packages/react/src/__tests__/useVirtualField.spec.tsx new file mode 100644 index 00000000000..f4d74068d78 --- /dev/null +++ b/packages/react/src/__tests__/useVirtualField.spec.tsx @@ -0,0 +1,105 @@ +import React from 'react' +import { act, renderHook } from '@testing-library/react-hooks' +import FormContext from '../context'; +import useForm from '../hooks/useForm' +import useVirtualField from '../hooks/useVirtualField' +import { createForm } from '..'; +import { FormLifeCycle, LifeCycleTypes } from '@uform/core'; + +describe('useVirtualField hook',()=>{ + test('form is required', ()=>{ + expect(() => { + useVirtualField({}) + }).toThrow() + }) + + test('basic ', ()=>{ + let globalForm + let globalGraph + const formInstance = createForm({ + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FORM_GRAPH_CHANGE, (graph) => { + globalGraph = graph + }) + ] + }) + const formWrapper = ({ children }) => { + const form = useForm({ + form: formInstance + }) + globalForm = form + return {children} + } + + const { result } = renderHook(() => useVirtualField({ name: 'username' }), { wrapper: formWrapper }) + expect(result.current.form).toEqual(globalForm) + expect(result.current.state.props).toEqual({}) + expect(result.current.state).toEqual({ + ...globalGraph.get('username').getState(), + mounted: false, + }) + }) + + test('update', ()=>{ + let globalForm + const formWrapper = ({ children }) => { + const form = useForm({}) + globalForm = form + return {children} + } + + const { result, rerender } = renderHook(() => useVirtualField({ name: 'username' }), { wrapper: formWrapper }) + expect(result.current.state.visible).toEqual(true) + act(() => { + globalForm.setFieldState('username', state => state.visible = false) + }) + + // forceUpdate will trigger in raf, use waitForNextUpdate + expect(result.current.state.visible).toEqual(true) + rerender() + expect(result.current.state.visible).toEqual(false) + }) + + test('mounted change', async ()=>{ + const formWrapper = ({ children }) => { + const form = useForm({}) + return {children} + } + + const { result, rerender } = renderHook(() => useVirtualField({ name: 'username' }), { wrapper: formWrapper }) + expect(result.current.state.mounted).toEqual(false) + rerender() + expect(result.current.state.mounted).toEqual(true) + }) + + test('dirty', async ()=>{ + const formWrapper = ({ children }) => { + const formInstance = createForm({ + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FIELD_CHANGE, (field) => { + + }) + ] + }) + const form = useForm({ + form: formInstance + }) + return {children} + } + + const initialProps = { + name: 'username', + props: { disabled: true }, + } + const { result, waitForNextUpdate, rerender } = renderHook(() => useVirtualField(initialProps), { wrapper: formWrapper }) + expect(result.current.props).toEqual({ disabled: true }) + initialProps.props = { disabled: false } + + expect(result.current.props).toEqual({ disabled: true }) + + rerender() + await waitForNextUpdate() + + expect(result.current.props).toEqual(initialProps.props) + }) +}) \ No newline at end of file diff --git a/packages/react/src/components/Field.tsx b/packages/react/src/components/Field.tsx index 2045eeb5c23..d5f75d29b95 100644 --- a/packages/react/src/components/Field.tsx +++ b/packages/react/src/components/Field.tsx @@ -1,9 +1,9 @@ import React from 'react' import { useField } from '../hooks/useField' import { isFn } from '@uform/shared' -import { IFieldProps } from '../types' +import { IFieldStateUIProps } from '../types' -export const Field: React.FC = props => { +export const Field: React.FC = props => { const { state, props: innerProps, mutators, form } = useField(props) if (!state.visible || !state.display) return if (isFn(props.children)) { diff --git a/packages/react/src/hooks/useField.ts b/packages/react/src/hooks/useField.ts index fa5d4700ac8..39876b8d174 100644 --- a/packages/react/src/hooks/useField.ts +++ b/packages/react/src/hooks/useField.ts @@ -1,7 +1,6 @@ import { useMemo, useEffect, useRef, useContext } from 'react' import { each, isFn } from '@uform/shared' import { - IFieldStateProps, IFieldState, IForm, IField, @@ -10,10 +9,10 @@ import { import { raf, getValueFromEvent } from '../shared' import { useDirty } from './useDirty' import { useForceUpdate } from './useForceUpdate' -import { IFieldHook, IFieldProps } from '../types' +import { IFieldHook, IFieldStateUIProps } from '../types' import FormContext from '../context' -const extendMutators = (mutators: IMutators, props: IFieldProps): IMutators => { +const extendMutators = (mutators: IMutators, props: IFieldStateUIProps): IMutators => { return { ...mutators, change: (...args) => { @@ -24,7 +23,7 @@ const extendMutators = (mutators: IMutators, props: IFieldProps): IMutators => { }, blur: () => { mutators.blur() - if (props.triggerType === 'onBlur') { + if (props.triggerType === 'onBlur') { mutators.validate() } } @@ -32,7 +31,7 @@ const extendMutators = (mutators: IMutators, props: IFieldProps): IMutators => { } export const useField = ( - options: IFieldStateProps & { triggerType?: 'onChange' | 'onBlur' } + options: IFieldStateUIProps ): IFieldHook => { const forceUpdate = useForceUpdate() const dirty = useDirty(options, ['props', 'rules', 'required', 'editable']) diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 39712cf1153..6a92e9fe1fe 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -68,7 +68,7 @@ export interface IVirtualFieldAPI { props: {} } -export interface IFieldProps extends IFieldStateProps { +export interface IFieldStateUIProps extends IFieldStateProps { triggerType?: 'onChange' | 'onBlur' getValueFromEvent?: (...args: any[]) => any children?: React.ReactElement | ((api: IFieldAPI) => React.ReactElement) From ded9b93792fd72d33451eed5537cc75e73ff23ac Mon Sep 17 00:00:00 2001 From: Janry Date: Tue, 19 Nov 2019 10:29:33 +0800 Subject: [PATCH 44/99] docs(@uform/core): improve docs (#402) --- packages/antd/src/index.tsx | 4 +- packages/core/README.md | 17 +- packages/core/README.zh-cn.md | 458 +++++++++--------- packages/core/src/index.ts | 41 +- packages/next/src/index.tsx | 4 +- packages/react-schema-renderer/README.md | 2 - .../react-schema-renderer/README.zh-cn.md | 68 +++ packages/validator/src/types.ts | 17 +- 8 files changed, 355 insertions(+), 256 deletions(-) delete mode 100644 packages/react-schema-renderer/README.md create mode 100644 packages/react-schema-renderer/README.zh-cn.md diff --git a/packages/antd/src/index.tsx b/packages/antd/src/index.tsx index 650d557f424..0cda03521a4 100644 --- a/packages/antd/src/index.tsx +++ b/packages/antd/src/index.tsx @@ -10,8 +10,6 @@ export * from '@uform/react-schema-renderer' export * from './components' export * from './types' export { mapStyledProps, mapTextComponent } from './shared' -export const SchemaForm: React.FC< - IAntdSchemaFormProps -> = SchemaMarkupForm as any +export const SchemaForm: React.FC = SchemaMarkupForm as any export const Field: React.FC = SchemaMarkupField export default SchemaForm diff --git a/packages/core/README.md b/packages/core/README.md index 7bec262c393..12854e03244 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -907,9 +907,24 @@ interface IMutators { }> } +type InternalFormats = + | 'url' + | 'email' + | 'ipv6' + | 'ipv4' + | 'idcard' + | 'taodomain' + | 'qq' + | 'phone' + | 'money' + | 'zh' + | 'date' + | 'zip' + | string + interface ValidateDescription {      // Regular rule type -     format?: string +     format?: InternalFormats      // Custom validator      validator?: CustomValidator      // Is it required? diff --git a/packages/core/README.zh-cn.md b/packages/core/README.zh-cn.md index 04f00280711..2dccf58ef18 100644 --- a/packages/core/README.zh-cn.md +++ b/packages/core/README.zh-cn.md @@ -1,10 +1,11 @@ # @uform/core -> 表单状态核心管理包(不依赖任何第三方UI框架),在该包中,它主要做了: + +> 表单状态核心管理包(不依赖任何第三方 UI 框架),在该包中,它主要做了: > -> - 管理Form状态 -> - 管理Field状态 -> - 管理Validator状态 -> - 管理Form/Field/Vaidator之间的依赖关系 +> - 管理 Form 状态 +> - 管理 Field 状态 +> - 管理 Validator 状态 +> - 管理 Form/Field/Validator 之间的依赖关系 ### 安装 @@ -22,42 +23,48 @@ npm install --save @uform/core - [架构图](#%E6%9E%B6%E6%9E%84%E5%9B%BE) - [术语解释](#%E6%9C%AF%E8%AF%AD%E8%A7%A3%E9%87%8A) - [API](#api) - * [`createForm`](#createform) - * [`registerValidationFormats`](#registervalidationformats) - * [`registerValidationRules`](#registervalidationrules) - * [`registerValidationMTEngine`](#registervalidationmtengine) - * [`setValidationLanguage`](#setvalidationlanguage) - * [`setValidationLocale`](#setvalidationlocale) + - [`createForm`](#createform) + - [`registerValidationFormats`](#registervalidationformats) + - [`registerValidationRules`](#registervalidationrules) + - [`registerValidationMTEngine`](#registervalidationmtengine) + - [`setValidationLanguage`](#setvalidationlanguage) + - [`setValidationLocale`](#setvalidationlocale) - [Classes](#classes) - * [`new FormPath()`](#new-formpath) - * [`new FormLifeCyle()`](#new-formlifecyle) + - [`new FormPath()`](#new-formpath) + - [`new FormLifeCyle()`](#new-formlifecyle) - [Enums](#enums) - * [LifeCycleTypes](#lifecycletypes) + - [LifeCycleTypes](#lifecycletypes) - [Interfaces](#interfaces) - * [IFormCreatorOptions](#iformcreatoroptions) - * [IForm](#iform) - * [Imutators](#imutators) - * [Validator](#validator) - * [IFormState](#iformstate) - * [IFieldState](#ifieldstate) - * [IVirtualFieldState](#ivirtualfieldstate) - * [IField/IVirtualField](#ifieldivirtualfield) + - [IFormCreatorOptions](#iformcreatoroptions) + - [IForm](#iform) + - [Imutators](#imutators) + - [Validator](#validator) + - [IFormState](#iformstate) + - [IFieldState](#ifieldstate) + - [IVirtualFieldState](#ivirtualfieldstate) + - [IField/IVirtualField](#ifieldivirtualfield) ### 背景 -中后台领域,核心就是两种场景,**一个是数据录入,一个是数据查询+数据展现**,不管是数据录入还是数据查询,都是借助表单来实现的,从实现复杂度来看,两者复杂度相差不多,因为数据呈现层面难免会有极度复杂的呈现形式(比如Tree Table等等),但是,数据呈现却是最容易复用和抽象的,只有表单,会涉及大量的交互逻辑,所以,只要我们根本上解决了表单问题,对于中后台场景,基本上解决了大部分中后台场景问题,UForm,就是为此而诞生的。 +中后台领域,核心就是两种场景,**一个是数据录入,一个是数据查询+数据展现**,不管 +是数据录入还是数据查询,都是借助表单来实现的,从实现复杂度来看,两者复杂度相差不 +多,因为数据呈现层面难免会有极度复杂的呈现形式(比如 Tree Table 等等),但是,数据 +呈现却是最容易复用和抽象的,只有表单,会涉及大量的交互逻辑,所以,只要我们根本上 +解决了表单问题,对于中后台场景,基本上解决了大部分中后台场景问题,UForm,就是为 +此而诞生的。 ### 设计理念 -**Anything comes from Observable Grpah.** +**Anything comes from Observable Graph.** ### 核心亮点 -- 时间旅行,借助首创Observable Graph,可以记录任意时刻的全量状态,也可以将状态回滚至任意时刻,这样的能力在,重事务型应用与本地调试上可以发挥出最大价值 +- 时间旅行,借助首创 Observable Graph,可以记录任意时刻的全量状态,也可以将状态 + 回滚至任意时刻,这样的能力在,重事务型应用与本地调试上可以发挥出最大价值 - 高效更新,精确渲染,无需整树渲染 -- 内置immer.js,智能降级,无需关心浏览器兼容性 +- 内置 immer.js,智能降级,无需关心浏览器兼容性 - 更加完备的生命周期钩子 - 更加完备的校验引擎 - validateFirst 校验 @@ -73,19 +80,24 @@ npm install --save @uform/core ![](https://img.alicdn.com/tfs/TB18LXHlVP7gK0jSZFjXXc5aXXa-1428-926.png) - - ### 术语解释 -**FormPath/FormPathPattern** 是一个抽象数据路径形式,FormPath是路径类,FormPathPattern是可以被FormPath解析的路径形式,在这里主要使用了 [cool-path](https://github.com/janrywang/cool-path) 路径解析匹配,求值取值能力 +**FormPath/FormPathPattern** 是一个抽象数据路径形式,FormPath 是路径类 +,FormPathPattern 是可以被 FormPath 解析的路径形式,在这里主要使用了 +[cool-path](https://github.com/janrywang/cool-path) 路径解析匹配,求值取值能力 -**VirtualField** 是一个特殊的Field数据结构,它与Field的差异就是,它不管理value,也就是说,它与Form的value是没有关联性的,通常我们使用它,更多的是作为代理一个UI容器的状态,比如:UForm中的布局组件FormBlock,它会在整个Form Grpah中作为一个独立节点而存在,但是这个节点类型就是一个VirtualField,但是最终数据提交的时候,FormBlock并不会污染提交数据的数据结构。 +**VirtualField** 是一个特殊的 Field 数据结构,它与 Field 的差异就是,它不管理 +value,也就是说,它与 Form 的 value 是没有关联性的,通常我们使用它,更多的是作为 +代理一个 UI 容器的状态,比如:UForm 中的布局组件 FormBlock,它会在整个 Form +Graph 中作为一个独立节点而存在,但是这个节点类型就是一个 VirtualField,但是最终 +数据提交的时候,FormBlock 并不会污染提交数据的数据结构。 -**Observable Graph** 是Form独有的观察者树,借助观察者树,可以实现很多表单相关的内部联动逻辑 +**Observable Graph** 是 Form 独有的观察者树,借助观察者树,可以实现很多表单相关 +的内部联动逻辑 -**Data Path** 是Field/VirtualField的name属性,它是作为数据路径而存在 +**Data Path** 是 Field/VirtualField 的 name 属性,它是作为数据路径而存在 -**Node Path** 是Field/VirtualFIeld的path属性,它是作为节点路径而存在 +**Node Path** 是 Field/VirtualField 的 path 属性,它是作为节点路径而存在 对于数据路径和节点路径,我们可以看下面这张图: @@ -93,12 +105,15 @@ npm install --save @uform/core 如果存在这样一棵树的话,那么: -- c字段的name属性则是a.c,path属性是a.b.c -- b字段的name属性是a.b,path属性是a.b -- d字段的name属性是a.d,path属性是a.d -- e字段的name属性是a.d.e,path属性是a.d.e +- c 字段的 name 属性则是 a.c,path 属性是 a.b.c +- b 字段的 name 属性是 a.b,path 属性是 a.b +- d 字段的 name 属性是 a.d,path 属性是 a.d +- e 字段的 name 属性是 a.d.e,path 属性是 a.d.e -这一来解释之后,我们就大概明白了,只要在某个节点路径中,存在VirtualField,那么它的数据路径就会略过VirtualField,但是,对于VirtualField自身这个节点,它的name属性,是包含它自身的节点标识的,这就是为什么b字段的name属性是a.b的原因 +这一来解释之后,我们就大概明白了,只要在某个节点路径中,存在 VirtualField,那么 +它的数据路径就会略过 VirtualField,但是,对于 VirtualField 自身这个节点,它的 +name 属性,是包含它自身的节点标识的,这就是为什么 b 字段的 name 属性是 a.b 的原 +因 ### API @@ -106,7 +121,7 @@ npm install --save @uform/core #### `createForm` -> 创建一个Form实例 +> 创建一个 Form 实例 **签名** @@ -120,25 +135,23 @@ createForm(options?: IFormCreatorOptions): IForm import { createForm } from '@uform/core' const form = createForm({ - values:{}, - initialValues:{}, - onChange:(values)=>{ + values: {}, + initialValues: {}, + onChange: values => { console.log(values) } }) const aa = form.registerField({ - path:"aa" + path: 'aa' }) -aa.setState(state=>{ +aa.setState(state => { state.value = 123 }) -console.log(form.getFormState(state=>state.values)) //{aa:123} +console.log(form.getFormState(state => state.values)) //{aa:123} ``` - - #### `registerValidationFormats` > 注册正则校验规则集 @@ -151,39 +164,39 @@ registerValidationFormats(formats:{ }) : void ``` - - **用法** ```typescript -import { createForm,registerValidationFormats } from '@uform/core' +import { createForm, registerValidationFormats } from '@uform/core' registerValidationFormats({ number: /^[+-]?\d+(\.\d+)?$/ }) const form = createForm({ - values:{}, - initialValues:{}, - onChange:(values)=>{ + values: {}, + initialValues: {}, + onChange: values => { console.log(values) } }) const aa = form.registerField({ - path:"aa", - rules:[{ - format:"number", - message:'This field is not a number.' - }] + path: 'aa', + rules: [ + { + format: 'number', + message: 'This field is not a number.' + } + ] }) -aa.setState(state=>{ +aa.setState(state => { state.value = 'hello world' }) form.validate() -console.log(form.getFormState(state=>state.errors)) +console.log(form.getFormState(state => state.errors)) /** [{ path: 'aa', @@ -192,11 +205,10 @@ console.log(form.getFormState(state=>state.errors)) **/ ``` - - #### `registerValidationRules` -> 注册校验规则集,与注册formats的差别是,它可以注册复杂校验规则,但是formats只是正则表达式 +> 注册校验规则集,与注册 formats 的差别是,它可以注册复杂校验规则,但是 formats +> 只是正则表达式 **签名** @@ -211,7 +223,7 @@ registerValidationRules( **用法** ```typescript -import { createForm,registerValidationRules } from '@uform/core' +import { createForm, registerValidationRules } from '@uform/core' registerValidationRules({ custom: value => { @@ -241,7 +253,7 @@ aa.setState(state => { }) form.validate() -console.log(form.getFormState(state =>state.errors)) +console.log(form.getFormState(state => state.errors)) /** [{ path: 'aa', @@ -307,8 +319,6 @@ console.log(form.getFormState(state =>state.errors)) **/ ``` - - #### `setValidationLanguage` > 设置国际化语言类型 @@ -327,8 +337,6 @@ import { setValidationLanguage } from '@uform/core' setValidationLanguage('en-US') ``` - - #### `setValidationLocale` > 设置语言包 @@ -351,14 +359,12 @@ setValidationLocale(locale: ILocales) => void import { setValidationLocale } from '@uform/core' setValidationLocale({ - 'en-US':{ - required:"This field is required." + 'en-US': { + required: 'This field is required.' } }) ``` - - ### Classes #### `new FormPath()` @@ -405,8 +411,6 @@ const form = createForm({ }) ``` - - ### Enums --- @@ -419,49 +423,47 @@ enum LifeCycleTypes { * Form LifeCycle **/ - ON_FORM_WILL_INIT = 'onFormWillInit', //表单预初始化触发 - ON_FORM_INIT = 'onFormInit', //表单初始化触发 - ON_FORM_CHANGE = 'onFormChange', //表单变化时触发 - ON_FORM_MOUNT = 'onFormMount', //表单挂载时触发 - ON_FORM_UNMOUNT = 'onFormUnmount', //表单卸载时触发 - ON_FORM_SUBMIT = 'onFormSubmit', //表单提交时触发 - ON_FORM_RESET = 'onFormReset', //表单重置时触发 - ON_FORM_SUBMIT_START = 'onFormSubmitStart', //表单提交开始时触发 - ON_FORM_SUBMIT_END = 'onFormSubmitEnd', //表单提交结束时触发 - ON_FORM_VALUES_CHANGE = 'onFormValuesChange', //表单值变化时触发 - ON_FORM_INITIAL_VALUES_CHANGE = 'onFormInitialValuesChange',//表单初始值变化时触发 - ON_FORM_VALIDATE_START = 'onFormValidateStart', //表单校验开始时触发 - ON_FORM_VALIDATE_END = 'onFormValidateEnd', //表单校验结束时触发 - ON_FORM_INPUT_CHANGE = 'onFormInputChange', //表单事件触发时触发,用于只监控人工操作 + ON_FORM_WILL_INIT = 'onFormWillInit', //表单预初始化触发 + ON_FORM_INIT = 'onFormInit', //表单初始化触发 + ON_FORM_CHANGE = 'onFormChange', //表单变化时触发 + ON_FORM_MOUNT = 'onFormMount', //表单挂载时触发 + ON_FORM_UNMOUNT = 'onFormUnmount', //表单卸载时触发 + ON_FORM_SUBMIT = 'onFormSubmit', //表单提交时触发 + ON_FORM_RESET = 'onFormReset', //表单重置时触发 + ON_FORM_SUBMIT_START = 'onFormSubmitStart', //表单提交开始时触发 + ON_FORM_SUBMIT_END = 'onFormSubmitEnd', //表单提交结束时触发 + ON_FORM_VALUES_CHANGE = 'onFormValuesChange', //表单值变化时触发 + ON_FORM_INITIAL_VALUES_CHANGE = 'onFormInitialValuesChange', //表单初始值变化时触发 + ON_FORM_VALIDATE_START = 'onFormValidateStart', //表单校验开始时触发 + ON_FORM_VALIDATE_END = 'onFormValidateEnd', //表单校验结束时触发 + ON_FORM_INPUT_CHANGE = 'onFormInputChange', //表单事件触发时触发,用于只监控人工操作 /** * FormGraph LifeCycle **/ - ON_FORM_GRAPH_CHANGE = 'onFormGraphChange', //表单观察者树变化时触发 + ON_FORM_GRAPH_CHANGE = 'onFormGraphChange', //表单观察者树变化时触发 /** * Field LifeCycle **/ - ON_FIELD_WILL_INIT = 'onFieldWillInit', //字段预初始化时触发 - ON_FIELD_INIT = 'onFieldInit', //字段初始化时触发 - ON_FIELD_CHANGE = 'onFieldChange', //字段变化时触发 - ON_FIELD_INPUT_CHANGE = 'onFieldInputChange', //字段事件触发时触发,用于只监控人工操作 - ON_FIELD_VALUE_CHANGE = 'onFieldValueChange', //字段值变化时触发 - ON_FIELD_INITIAL_VALUE_CHANGE = 'onFieldInitialValueChange',//字段初始值变化时触发 - ON_FIELD_MOUNT = 'onFieldMount', //字段挂载时触发 - ON_FIELD_UNMOUNT = 'onFieldUnmount' //字段卸载时触发 + ON_FIELD_WILL_INIT = 'onFieldWillInit', //字段预初始化时触发 + ON_FIELD_INIT = 'onFieldInit', //字段初始化时触发 + ON_FIELD_CHANGE = 'onFieldChange', //字段变化时触发 + ON_FIELD_INPUT_CHANGE = 'onFieldInputChange', //字段事件触发时触发,用于只监控人工操作 + ON_FIELD_VALUE_CHANGE = 'onFieldValueChange', //字段值变化时触发 + ON_FIELD_INITIAL_VALUE_CHANGE = 'onFieldInitialValueChange', //字段初始值变化时触发 + ON_FIELD_MOUNT = 'onFieldMount', //字段挂载时触发 + ON_FIELD_UNMOUNT = 'onFieldUnmount' //字段卸载时触发 } ``` - - ### Interfaces --- #### IFormCreatorOptions -> createForm参数对象协议 +> createForm 参数对象协议 ```typescript interface IFormCreatorOptions { @@ -488,11 +490,9 @@ interface IFormCreatorOptions { } ``` - - #### IForm -> 通过createForm创建出来的Form实例对象API +> 通过 createForm 创建出来的 Form 实例对象 API ```typescript interface IForm { @@ -504,22 +504,25 @@ interface IForm { submit( onSubmit?: (values: IFormState['values']) => any | Promise ): Promise<{ - validated: IFormValidateResult - payload: any //onSubmit回调函数返回值 + validated: IFormValidateResult + payload: any //onSubmit回调函数返回值 }> - + /* * 清空错误消息,可以通过传FormPathPattern来批量或精确控制要清空的字段, * 比如clearErrors("*(aa,bb,cc)") */ clearErrors: (pattern?: FormPathPattern) => void - + /* * 获取状态变化情况,主要用于在表单生命周期钩子内判断当前生命周期中有哪些状态发生了变化, * 比如hasChanged(state,'value.aa') */ - hasChanged(target: IFormState | IFieldState | IVirtualFieldState, path: FormPathPattern): boolean - + hasChanged( + target: IFormState | IFieldState | IVirtualFieldState, + path: FormPathPattern + ): boolean + /* * 重置表单 */ @@ -529,27 +532,30 @@ interface IForm { //强制校验 validate?: boolean //重置范围,用于批量或者精确控制要重置的字段 - selector?: FormPathPattern + selector?: FormPathPattern }): Promise - + /* * 校验表单 */ - validate(path?: FormPathPattern, options?: { - //是否悲观校验,如果当前字段遇到第一个校验错误则停止后续校验流程 - first?:boolean - }): Promise - + validate( + path?: FormPathPattern, + options?: { + //是否悲观校验,如果当前字段遇到第一个校验错误则停止后续校验流程 + first?: boolean + } + ): Promise + /* * 设置表单状态 */ setFormState( //操作回调 - callback?: (state: IFormState) => any, + callback?: (state: IFormState) => any, //是否不触发事件 silent?: boolean ): void - + /* * 获取表单状态 */ @@ -557,7 +563,7 @@ interface IForm { //transformer callback?: (state: IFormState) => any ): any - + /* * 设置字段状态 */ @@ -569,8 +575,8 @@ interface IForm { //是否不触发事件 silent?: boolean ): void - - /* + + /* * 获取字段状态 */ getFieldState( @@ -579,14 +585,14 @@ interface IForm { //transformer callback?: (state: IFieldState) => any ): any - + /* * 注册字段 */ registerField(props: { - //节点路径 + //节点路径 path?: FormPathPattern - //数据路径 + //数据路径 name?: string //字段值 value?: any @@ -605,76 +611,72 @@ interface IForm { //字段是否走脏检查 useDirty?: boolean //字段状态计算容器,主要用于扩展核心联动规则 - computeState?: (draft: IFieldState, prevState: IFieldState) => void + computeState?: (draft: IFieldState, prevState: IFieldState) => void }): IField - + /* * 注册虚拟字段 */ registerVirtualField(props: { - //节点路径 + //节点路径 path?: FormPathPattern - //数据路径 + //数据路径 name?: string //字段扩展属性 props?: any //字段是否走脏检查 useDirty?: boolean //字段状态计算容器,主要用于扩展核心联动规则 - computeState?: (draft: IFieldState, prevState: IFieldState) => void + computeState?: (draft: IFieldState, prevState: IFieldState) => void }): IVirtualField - + /* * 创建字段数据操作器,后面会详细解释返回的API */ createMutators(field: IField): IMutators - + /* * 获取表单观察者树 */ getFormGraph(): IFormGraph - + /* * 设置表单观察者树 */ setFormGraph(graph: IFormGraph): void - + /* * 监听表单生命周期 */ - subscribe(callback?: ({ - type, - payload - }: { - type: string - payload: any - }) => void): number - + subscribe( + callback?: ({ type, payload }: { type: string; payload: any }) => void + ): number + /* * 取消监听表单生命周期 */ unsubscribe(id: number): void - + /* * 触发表单自定义生命周期 */ notify: (type: string, payload?: T) => void - + /* * 设置字段值 */ setFieldValue(path?: FormPathPattern, value?: any): void - + /* * 获取字段值 */ getFieldValue(path?: FormPathPattern): any - + /* * 设置字段初始值 */ setFieldInitialValue(path?: FormPathPattern, value?: any): void - + /* * 获取字段初始值 */ @@ -682,11 +684,9 @@ interface IForm { } ``` - - #### Imutators -> 通过createMutators创建出来的实例API,主要用于操作字段数据 +> 通过 createMutators 创建出来的实例 API,主要用于操作字段数据 ```typescript interface IMutators { @@ -700,9 +700,9 @@ interface IMutators { validate(): Promise //当前字段的值是否在Form的values属性中存在 exist(index?: number | string): boolean - + /**数组操作方法**/ - + //追加数据 push(value?: any): any[] //弹出尾部数据 @@ -721,83 +721,97 @@ interface IMutators { moveDown(index: number): any[] //上移 moveUp(index: number): any[] - } ``` - - #### Validator > 这里主要列举校验相关的中间类型签名 ```typescript -type CustomValidator = (value: any, rescription?: ValidateDescription) => ValidateResponse -type SyncValidateResponse = null | string | boolean | { - type?: 'error' | 'warning'; - message: string; -}; +type CustomValidator = ( + value: any, + rescription?: ValidateDescription +) => ValidateResponse +type SyncValidateResponse = + | null + | string + | boolean + | { + type?: 'error' | 'warning' + message: string + } type AsyncValidateResponse = Promise -type ValidateResponse = SyncValidateResponse | AsyncValidateResponse; +type ValidateResponse = SyncValidateResponse | AsyncValidateResponse interface IFormValidateResult { - errors: Array<{ - path: string; - messages: string[]; - }>; - warnings: Array<{ - path: string; - messages: string[]; - }>; + errors: Array<{ + path: string + messages: string[] + }> + warnings: Array<{ + path: string + messages: string[] + }> } +type InternalFormats = + | 'url' + | 'email' + | 'ipv6' + | 'ipv4' + | 'idcard' + | 'taodomain' + | 'qq' + | 'phone' + | 'money' + | 'zh' + | 'date' + | 'zip' + | string + interface ValidateDescription { - //正则规则类型 - format?: string; - //自定义校验规则 - validator?: CustomValidator; - //是否必填 - required?: boolean; - //自定以正则 - pattern?: RegExp | string; - //最大长度规则 - max?: number; - //最大数值规则 - maximum?: number; - //封顶数值规则 - exclusiveMaximum?: number; - //封底数值规则 - exclusiveMinimum?: number; - //最小数值规则 - minimum?: number; - //最小长度规则 - min?: number; - //长度规则 - len?: number; - //是否校验空白符 - whitespace?: boolean; - //枚举校验规则 - enum?: any[]; - //自定义错误文案 - message?: string; - //自定义校验规则 - [key: string]: any; + //正则规则类型 + format?: InternalFormats + //自定义校验规则 + validator?: CustomValidator + //是否必填 + required?: boolean + //自定以正则 + pattern?: RegExp | string + //最大长度规则 + max?: number + //最大数值规则 + maximum?: number + //封顶数值规则 + exclusiveMaximum?: number + //封底数值规则 + exclusiveMinimum?: number + //最小数值规则 + minimum?: number + //最小长度规则 + min?: number + //长度规则 + len?: number + //是否校验空白符 + whitespace?: boolean + //枚举校验规则 + enum?: any[] + //自定义错误文案 + message?: string + //自定义校验规则 + [key: string]: any } ``` - - - - #### IFormState -> Form核心状态 +> Form 核心状态 ```typescript interface IFormState { - /**只读属性**/ - + //是否处于原始态,只有values===initialValues时,pristine为true pristine: boolean //是否合法,只要errors长度大于0的时候valid为false @@ -812,9 +826,9 @@ interface IFormState { errors: string[] //告警消息列表 warnings: string[] - + /**可写属性**/ - + //是否处于加载态,可写状态,只要validating为true时,该状态也会为true,为false时同理 loading: boolean //是否处于初始态 @@ -834,17 +848,14 @@ interface IFormState { } ``` - - #### IFieldState -> 核心Field状态 +> 核心 Field 状态 ```typescript interface IFieldState { - /**只读属性**/ - + //状态名称,FieldState displayName?: string //数据路径 @@ -869,9 +880,9 @@ interface IFieldState { active: boolean //是否访问过,字段触发onBlur事件的时候,它会被触发为true visited: boolean - + /**可写属性**/ - + //是否可见,注意:该状态如果为false,那么字段的值不会被提交,同时UI不会显示 visible: boolean //是否展示,注意:该状态如果为false,那么字段的值会提交,UI不会展示,类似于表单隐藏域 @@ -903,17 +914,14 @@ interface IFieldState { } ``` - - #### IVirtualFieldState -> 虚拟Field核心状态 +> 虚拟 Field 核心状态 ```typescript interface IVirtualFieldState { - /**只读状态**/ - + //状态名称,VirtualFieldState displayName: string //字段数据路径 @@ -922,9 +930,9 @@ interface IVirtualFieldState { path: string //是否已经初始化 initialized: boolean - + /**可写状态**/ - + //是否可见,注意:该状态如果为false,UI不会显示,数据也不会提交(因为它是VirtualField) visible: boolean //是否展示,注意:该状态如果为false,UI不会显示,数据也不会提交(因为它是VirtualField) @@ -938,11 +946,9 @@ interface IVirtualFieldState { } ``` - - #### IField/IVirtualField -> 通过registerField/registerVirtualField创建出来的实例API +> 通过 registerField/registerVirtualField 创建出来的实例 API ```typescript interface IField/IVirtualField { @@ -964,5 +970,3 @@ interface IField/IVirtualField { getDirtyInfo: () => StateDirtyMap } ``` - - diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 40a70d362b7..698c79385fc 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -294,14 +294,15 @@ export function createForm( let nodePath = FormPath.parse(path || name) let dataPath = transformDataPath(nodePath) let field: IVirtualField - const createField = () => { - let field: IVirtualField - field = new VirtualFieldState({ - nodePath, - dataPath, - computeState, - useDirty: isValid(useDirty) ? useDirty : options.useDirty - }) + const createField = (field?: IVirtualField) => { + field = + field || + new VirtualFieldState({ + nodePath, + dataPath, + computeState, + useDirty: isValid(useDirty) ? useDirty : options.useDirty + }) field.subscription = { notify: onVirtualFieldChange({ field, path: nodePath }) } @@ -316,8 +317,8 @@ export function createForm( } if (graph.exist(nodePath)) { field = graph.get(nodePath) + field = createField(field) if (isField(field)) { - field = createField() graph.replace(nodePath, field) } } else { @@ -342,14 +343,15 @@ export function createForm( let field: IField let nodePath = FormPath.parse(path || name) let dataPath = transformDataPath(nodePath) - const createField = () => { - let field: IField - field = new FieldState({ - nodePath, - dataPath, - computeState, - useDirty: isValid(useDirty) ? useDirty : options.useDirty - }) + const createField = (field?: IField) => { + field = + field || + new FieldState({ + nodePath, + dataPath, + computeState, + useDirty: isValid(useDirty) ? useDirty : options.useDirty + }) field.subscription = { notify: onFieldChange({ field, path: nodePath }) } @@ -402,8 +404,8 @@ export function createForm( } if (graph.exist(nodePath)) { field = graph.get(nodePath) - if (isVirtualField(nodePath)) { - field = createField() + field = createField(field) + if (isVirtualField(field)) { graph.replace(nodePath, field) } } else { @@ -1035,6 +1037,7 @@ export function createForm( state.subscription = { notify: onFormChange } + graph.appendNode('', state) state.setState((state: IFormState) => { state.initialized = true diff --git a/packages/next/src/index.tsx b/packages/next/src/index.tsx index 85d9569b902..7056bbbed78 100644 --- a/packages/next/src/index.tsx +++ b/packages/next/src/index.tsx @@ -10,8 +10,6 @@ export * from '@uform/react-schema-renderer' export * from './components' export * from './types' export { mapStyledProps, mapTextComponent } from './shared' -export const SchemaForm: React.FC< - INextSchemaFormProps -> = SchemaMarkupForm as any +export const SchemaForm: React.FC = SchemaMarkupForm as any export const Field: React.FC = SchemaMarkupField export default SchemaForm diff --git a/packages/react-schema-renderer/README.md b/packages/react-schema-renderer/README.md deleted file mode 100644 index 0f89496d1c6..00000000000 --- a/packages/react-schema-renderer/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# @uform/react-schema-renderer -> UForm React实现 \ No newline at end of file diff --git a/packages/react-schema-renderer/README.zh-cn.md b/packages/react-schema-renderer/README.zh-cn.md new file mode 100644 index 00000000000..4858564264b --- /dev/null +++ b/packages/react-schema-renderer/README.zh-cn.md @@ -0,0 +1,68 @@ +# @uform/react-schema-renderer +> Schema渲染引擎,该包主要依赖了@uform/react,它的职责很简单,核心就做了两件事情: +> +> - 解析Form Schema协议,递归渲染 +> - 管理自定义组件 + +### 安装 + +```bash +npm install --save @uform/react-schema-renderer +``` + +### 目录 + + + +### 使用方式 +--- + +#### 快速开始 + +#### JSON Schema驱动表单渲染 + +#### JSchema驱动表单渲染 + +#### 快速接入第三方组件库 + +#### 使用自定义组件建立自己的表单组件生态 + +#### 使用VirtualBox组件建立自己的表单布局生态 + +### 高级教程 +--- + +#### 如何实现自己的递归渲染组件? + +#### 如何实现超复杂自定义组件? + +### 协议 +--- + +#### 理念 + +#### 架构 + +#### 数据结构协议 + +#### 布局协议 + +#### 顺序协议 + +#### 校验协议 + +#### UI协议 + +#### 逻辑协议 + +### API +--- + +### Components +--- + + +### Interfaces +--- + + diff --git a/packages/validator/src/types.ts b/packages/validator/src/types.ts index 98319e8b9fb..3c2e8b41003 100644 --- a/packages/validator/src/types.ts +++ b/packages/validator/src/types.ts @@ -17,8 +17,23 @@ export type ValidateFormatsMap = { [key in string]: RegExp } +export type InternalFormats = + | 'url' + | 'email' + | 'ipv6' + | 'ipv4' + | 'idcard' + | 'taodomain' + | 'qq' + | 'phone' + | 'money' + | 'zh' + | 'date' + | 'zip' + | string + export interface ValidateDescription { - format?: string + format?: InternalFormats validator?: CustomValidator required?: boolean pattern?: RegExp | string From 570a81c391bbfc551c8ffbba77f8a2ab52ceb4d6 Mon Sep 17 00:00:00 2001 From: quirkyvar Date: Wed, 20 Nov 2019 00:02:20 +0800 Subject: [PATCH 45/99] [WIP]doc(@uform/react): add react doc (#401) --- packages/react/README.md | 1825 +++++++++++++++++++++++++++--- packages/react/README.zh-cn.md | 1842 +++++++++++++++++++++++++++++++ packages/validator/src/types.ts | 4 +- scripts/docs.js | 1 + 4 files changed, 3502 insertions(+), 170 deletions(-) create mode 100644 packages/react/README.zh-cn.md diff --git a/packages/react/README.md b/packages/react/README.md index 749d5be34d4..2d10c6ccd95 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -1,12 +1,72 @@ # @uform/react -> UForm React Pure Package +> @uform/react is based on `react` and @uform/core is already built in. It provide API to manuplate form state and components for rendering support. +> it mainly includes: +> +> - Form +> - Field +> - VirtualField +> - FormaSpy +> - FormProvider +> - FormConsumer(deprecated,pls using FormSpy) +> - createFormActions (create sync API to manuplate form state) +> - createAsyncFormActions (create async API to manuplate form state) +> - FormEffectHooks (LifeCycles Hook) +### Install -### ArrayStringList +```bash +npm install --save @uform/react +``` + +### Table Of Contents + + + +- [`Usage`](#Usage) + - [`Quick Start`](#Quick-Start) + - [`Basic Field`](#Basic-Field) + - [`Validation`](#Validation) + - [`Object Field`](#Object-Field) + - [`ArrayField`](#ArrayField) + - [`ArrayField`](#ArrayField) + - [`Combo`](#Combo) + - [`Provide and FormSpy`](#Provide-and-FormSpy) +- [Components](#components) + - [`Form`](#Form) + - [`Field`](#Field) + - [`VirtualField`](#VirtualField) + - [`FormSpy`](#FormSpy) + - [`FormProvider`](#FormProvider) + - [`FormConsumer(deprecated,pls using FormSpy)`](<#FormConsumer(deprecated,pls-using-FormSpy)>) +- [API](#API) + - [`createFormActions`](#createFormActions) + - [`createAsyncFormActions`](#createAsyncFormActions) + - [`FormEffectHooks`](#FormEffectHooks) + - [`createEffectHook`](#createEffectHook) +- [Interfaces](#Interfaces) + - [`IFormActions`](#IFormActions) + - [`IFormAsyncActions`](#IFormAsyncActions) + - [`IFieldState`](#IFieldState) + - [`IVirtualFieldState`](#IVirtualFieldState) + - [`SyncValidateResponse`](#SyncValidateResponse) + - [`AsyncValidateResponse`](#AsyncValidateResponse) + - [`ValidateResponse`](#ValidateResponse) + - [`InternalFormats`](#InternalFormats) + - [`CustomValidator`](#CustomValidator) + - [`ValidateDescription`](#ValidateDescription) + - [`ValidateArrayRules`](#ValidateArrayRules) + - [`ValidatePatternRules`](#ValidatePatternRules) + +### Usage + +--- + +#### Quick Start ```jsx -import React, { useState } from 'react' +import React from 'react' +import ReactDOM from 'react-dom' import { Form, Field, @@ -18,9 +78,52 @@ import { FormEffectHooks } from './src' +const { onFormInit$, onFormInputChange$, onFieldInputChange$ } = FormEffectHooks const actions = createFormActions() +const App = () => { + return ( +
    { + onFormInit$().subscribe(() => { + console.log('initialized') + }) + onFieldInputChange$().subscribe(state => { + console.log('field change', state) + }) + }} + onChange={() => {}} + > + + + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + + +
    + ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### Basic Field -const Input = props => ( +Example:Show you how to bind the `` field and subsequent examples are based on this field + +```tsx +const InputField = props => ( {({ state, mutators }) => (
    @@ -37,84 +140,230 @@ const Input = props => ( )} ) +``` -const { onFormInit$, onFormInputChange$, onFieldInputChange$ } = FormEffectHooks +#### Validation + +Example:required validation + error type validation + warning type validation + custom validation +The type of rules is [ValidatePatternRules](#ValidatePatternRules) which is [InternalFormats](#InternalFormats) | [CustomValidator](#CustomValidator) | [ValidateDescription](#ValidateDescription) | [ValidateArrayRules](#ValidateArrayRules) + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) const App = () => { - const [values, setValues] = useState({}) - const [editable, setEditable] = useState(true) return ( - -
    { - onFormInit$().subscribe(() => { - console.log('初始化') - }) - onFieldInputChange$().subscribe(state => { - console.log('输入变化', state) - }) + +
    required validation
    + username + + +
    error type validation
    + age + + val === undefiend + ? { type: 'error', message: 'age is required' } + : undefined + ]} + /> + +
    warning type validation
    + gender + + val === undefiend + ? { type: 'warning', message: 'gender is required' } + : undefined + ]} + /> + +
    built-in validation default to error type validation
    + id + + +
    custom validation
    + verifyCode + + +
    + +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### Object Field + +Example:User info `user(username, age)` + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const App = () => { + return ( +
    + user + {}} > - - {({ state, mutators }) => { - return ( -
    - {state.value.map((item, index) => { - return ( - - - - + {({ state, mutators }) => { + return ( + + {Object.keys(state.value).map(key => { + if (!mutators.exist(key)) return + + return ( +
    + {key} + + +
    + ) + })} + + -
    - ) - }} -
    - -
    + }} + > + print + + + ) + }} + + ) } ReactDOM.render(, document.getElementById('root')) ``` -### ArrayObjectList +#### ArrayField + +Example:Id list ```jsx -import React, { useState } from 'react' -import { - Form, - Field, - FormPath, - createFormActions, - FormSpy, - FormProvider, - FormConsumer, - FormEffectHooks -} from './src' +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions } from './src' const actions = createFormActions() - -const Input = props => ( +const InputField = props => ( {({ state, mutators }) => ( -
    + ( onBlur={mutators.blur} onFocus={mutators.focus} /> - {state.errors} - {state.warnings} -
    + {state.errors} + {state.warnings} + )}
    ) -const { onFormInit$, onFormInputChange$, onFieldInputChange$ } = FormEffectHooks - const App = () => { - const [values, setValues] = useState({}) - const [editable, setEditable] = useState(true) return ( - -
    { - onFormInit$().subscribe(() => { - console.log('初始化') - }) - onFieldInputChange$().subscribe(state => { - console.log('输入变化', state) - }) + + + {({ state, mutators }) => { + return ( + + {state.value.map((item, index) => { + return ( +
    + + +
    + ) + })} + +
    + ) }} - onChange={() => {}} +
    +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### ArrayField<Object> + +Example:User list + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const App = () => { + return ( +
    + - - {({ state, mutators }) => { - return ( -
    - {state.value.map((item, index) => { - return ( - - - { - if (value == '123') { - return { - type: 'warning', - message: '这个是一个提示' - } - } - }} - /> - - - ) - })} - -
    - ) - }} -
    - -
    + {({ state, mutators }) => { + return ( + + {state.value.map((item, index) => { + return ( +
    + + {({ state: innerState, mutators: innerMutator }) => { + return ( + + {Object.keys(innerState.value).map(key => { + if (!innerMutator.exist(key)) return + return ( + + + + + ) + })} + + + ) + }} + + + +
    + ) + })} + + +
    + ) + }} + + ) } ReactDOM.render(, document.getElementById('root')) ``` -### Dynamic Object +#### Combo + +Example:Combo value of username and age. Check [FormSpy](#FormSpy) for more inforation. ```jsx -import React, { useState } from 'react' -import { - Form, - Field, - FormPath, - createFormActions, - FormSpy, - FormProvider, - FormConsumer, - FormEffectHooks -} from './src' +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, FormSpy } from './src' const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const App = () => { + return ( +
    + + + + + + {({ state, form }) => { + return ( +
    + name: {form.getFieldValue('username')} +
    + age: {form.getFieldValue('age')} +
    + ) + }} +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### Provide and FormSpy -const Input = props => ( +```typescript +Dictionary +--app + |---components + |---customForm +``` + +Example:Cross-file consumption form state, Check [FormProvider](#FormProvider) and [FormSpy](#FormSpy) for more infomation. + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, FormSpy, FormProvider } from './src' + +const actions = createFormActions() +const InputField = props => ( {({ state, mutators }) => ( -
    + ( onBlur={mutators.blur} onFocus={mutators.focus} /> - {state.errors} - {state.warnings} -
    + {state.errors} + {state.warnings} + )}
    ) -const { onFormInit$, onFormInputChange$, onFieldInputChange$ } = FormEffectHooks +const CustomForm = () => { + return ( +
    + + + + + + ) +} const App = () => { - const [values, setValues] = useState({}) - const [editable, setEditable] = useState(true) return ( -
    { - onFormInit$().subscribe(() => { - console.log('初始化') - }) - onFieldInputChange$().subscribe(state => { - console.log('输入变化', state) - }) + + + {({ state, form }) => { + return ( +
    + name: {form.getFieldValue('username')} +
    + age: {form.getFieldValue('age')} +
    + ) }} - onChange={() => {}} - > - +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Components + +--- + +#### Form + +> `` Props + +```typescript +interface IFormProps { + // Form value + value?: any + defaultValue?: any // Form initial value + initialValues?: any + // formAPI + actions?: IFormActions | IFormAsyncActions + // effect + effects?: IFormEffect + // IForm instance + form?: IForm // Form change event callback + onChange?: (values: Value) => void // Form submission event callback + onSubmit?: (values: Value) => void | Promise // Form reset event callback + onReset?: () => void // Form verification failure event callback + onValidateFailed?: (valideted: IFormValidateResult) => void + children?: React.ReactElement | ((form: IForm) => React.ReactElement) + // Whether to use the dirty check, the default will go immer accurate update + useDirty?: boolean + // Is it editable, overall control in the Form dimension + editable?: boolean + // Whether to go pessimistic check, stop the subsequent check when the first check fails + validateFirst?: boolean +} +``` + +#### Field + +> `` Props + +```typescript +interface IFieldStateUIProps { + // Node path + path?: FormPathPattern // Node path + nodePath?: FormPathPattern // Data path + dataPath?: FormPathPattern // Data path + name?: string // Field value, is equal to values[0] + value?: any // Field multi-parameter value, such as when the field onChange trigger, the event callback passed multi-parameter data, then the value of all parameters will be stored here + values?: any[] // Initial value + initialValue?: any // field extension properties + props?: FieldProps // Check the rules, the specific type description refers to the following documents + rules?: ValidatePatternRules[] // Is it required? + required?: boolean // Is it editable? + editable?: boolean // Whether to use the dirty check, the default will go immer accurate update + useDirty?: boolean + // Field state calculation container, mainly used to extend the core linkage rules + computeState?: (draft: IFieldState, prevState: IFieldState) => void + // type of trigger validation + triggerType?: 'onChange' | 'onBlur' + // get value from browser event(eg. e.target.value) + getValueFromEvent?: (...args: any[]) => any + children?: React.ReactElement | ((api: IFieldAPI) => React.ReactElement) +} +``` + +**Usage** + +Example:All type of field + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const App = () => { + return ( + +
    +
    Basic Field
    + + {({ state, mutator }) => { + return + }} + +
    + +
    +
    Object Field
    + {({ state, mutators }) => { return ( {Object.keys(state.value).map(key => { if (!mutators.exist(key)) return + return ( - - +
    + {key} + - +
    ) })}
    ) }}
    - +
    + +
    +
    ArrayField Field
    + + {({ state, mutators }) => { + return ( + + {state.value.map((item, index) => { + return ( +
    + + +
    + ) + })} + +
    + ) + }} +
    +
    + +
    +
    ArrayObject Field
    + + {({ state, mutators }) => { + return ( + + {state.value.map((item, index) => { + return ( +
    + + {({ state: innerState, mutators: innerMutator }) => { + return ( + + {Object.keys(innerState.value).map(key => { + if (!innerMutator.exist(key)) return + return ( + + + + + ) + })} + + + ) + }} + + + +
    + ) + })} + +
    + ) + }} +
    + + +
    + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### VirtualField + +> `` Props + +```typescript +interface IVirtualFieldProps { + // Node path + path?: FormPathPattern // Node path + nodePath?: FormPathPattern // Data path + dataPath?: FormPathPattern // Data path + name?: string // Form extension properties + props?: FieldProps // Whether to use the dirty check, the default will go immer accurate update + useDirty?: boolean + // Field state calculation container, mainly used to extend the core linkage rules + computeState?: (draft: IFieldState, prevState: IFieldState) => void + children?: React.ReactElement | ((api: IFieldAPI) => React.ReactElement) +} +``` + +**Usage** + +Example:Setting `` size from 100x100 to 200x200 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, VirtualField } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const Layout = ({ children, width = '100px', height = '100px' }) => { + return ( +
    {children}
    + ) +} + +const App = () => { + return ( +
    + + {({ state, mutator }) => { + return ( + + {({ state: layoutState }) => { + return ( + + + + + + + ) + }} + + ) + }} + + + +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### FormSpy + +> `` Props + +```typescript +interface IFormSpyProps { + // selector, eg: [ LifeCycleTypes.ON_FORM_SUBMIT_START, LifeCycleTypes.ON_FORM_SUBMIT_END ] + selector?: string[] | string + // reducer + reducer?: ( + state: any, + action: { type: string; payload: any }, + form: IForm + ) => any + children?: React.ReactElement | ((api: IFormSpyAPI) => React.ReactElement) +} +``` + +**Usage** + +Example1: Form state change counter + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, FormSpy, LifeCycleTypes } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const App = () => { + return ( +
    + + + + + ({ + count: state.count ? state.count + 1 : 1 + })} + > + {({ state, type, form }) => { + return
    count: {state.count || 0}
    + }} +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) + +``` + +Example2:Combo + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, FormSpy } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const App = () => { + return ( +
    + + + + + + {({ state, form }) => { + return ( +
    + name: {form.getFieldValue('username')} +
    + age: {form.getFieldValue('age')} +
    + ) + }} +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### FormProvider + +> Used with FormSpy, often used in Cross-file consumption form state + +**Usage** + + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, FormSpy, FormProvider } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const CustomForm = () => { + return ( +
    + + + + + + ) +} + +const App = () => { + return ( + + + + {({ state, form }) => { + return ( +
    + name: {form.getFieldValue('username')} +
    + age: {form.getFieldValue('age')} +
    + ) + }} +
    ) } ReactDOM.render(, document.getElementById('root')) ``` + +#### FormConsumer(deprecated,pls using FormSpy) + +> `` Props + +```typescript +interface IFormConsumerProps { + // eg.[ LifeCycleTypes.ON_FORM_SUBMIT_START, LifeCycleTypes.ON_FORM_SUBMIT_END ] + selector?: string[] | string + children?: + | React.ReactElement + | ((api: IFormConsumerAPI) => React.ReactElement) +} +``` + +### API + +--- + +#### `createFormActions` + +> Return [IFormActions](#IFormActions) + +**Signature** + +```typescript +createFormActions(): IFormActions +``` + +**Usage** + +```typescript +import { createFormActions } from '@uform/react' + +const actions = createFormActions() +console.log(actions.getFieldValue('username')) +``` + +#### `createAsyncFormActions` + +> Return [IFormAsyncActions](#IFormAsyncActions) + +**Signature** + +```typescript +createAsyncFormActions(): IFormAsyncActions +``` + +**Usage** + +```typescript +import { createAsyncFormActions } from '@uform/react' + +const actions = createAsyncFormActions() +actions.getFieldValue('username').then(val => console.log(val)) +``` + +#### `FormEffectHooks` + +> Return all @uform/core lifeCycles hook which can be subscribe + +**Usage** + +```tsx +import { FormEffectHooks, Form } from '@uform/react' +const { + /** + * Form LifeCycle + **/ + // Form pre-initialization trigger + onFormWillInit$, + // Form initialization trigger + onFormInit$, + // Triggered when the form changes + onFormChange$, + // Triggered when the form event is triggered, used to monitor only manual operations + onFormInputChange$, + // Trigger when the form initial value changes + onFormInitialValueChange$, + // Triggered when the form is reset + onFormReset$, + // Triggered when the form is submitted + onFormSubmit$, + // Triggered when the form submission starts + onFormSubmitStart$, + // Triggered when the form submission ends + onFormSubmitEnd$, + // Triggered when the form is mounted + onFormMount$, + // Triggered when the form is unloaded + onFormUnmount$, + // Triggered when form validation begins + onFormValidateStart$, + // Triggered when the form validation ends + onFormValidateEnd$, + // Trigger when the form initial value changes + onFormValuesChange$, + /** + * FormGraph LifeCycle + **/ + // Triggered when the form observer tree changes + onFormGraphChange$, + /** + * Field LifeCycle + **/ + // Triggered when pre-initialized + onFieldWillInit$, + // Triggered when the field is initialized + onFieldInit$, + // Triggered when the field changes + onFieldChange$, + // Triggered when the field is mounted + onFieldMount$, + // Trigger when the field is unloaded + onFieldUnmount$, + // Triggered when the field event is triggered, used to monitor only manual operations + onFieldInputChange$, + // Triggered when the field value changes + onFieldValueChange$, + // Trigger when the initial value of the field changes + onFieldInitialValueChange$ +} = FormEffectHooks + +const App = () => { + return ( +
    { + onFormInit$().subscribe(() => { + console.log('initialized') + }) + }} + > + ... +
    + ) +} +``` + +#### createEffectHook + +> Custom your own hook by this api + +**Usage** + +```jsx +import { Form, createEffectHook, createFormActions } from './src' + +const actions = createFormActions() +const diyHook1$ = createEffectHook('diy1') +const diyHook2$ = createEffectHook('diy2') + +const App = () => { + return ( +
    { + diyHook1$().subscribe((payload) => { + console.log('diy1 hook triggered', payload) + }) + + diyHook2$().subscribe((payload) => { + console.log('diy2 hook triggered', payload) + }) + }} + > + + +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + + +### Interfaces + +--- + +#### IFormActions + +```typescript +interface IFormActions { + /* + * Form submission, if the callback parameter returns Promise, + * Then the entire submission process will hold and load is true. + * Wait for Promise resolve to trigger the form onFormSubmitEnd event while loading is false + */ + submit( + onSubmit?: (values: IFormState['values']) => any | Promise + ): Promise<{ + Validated: IFormValidateResult + Payload: any //onSubmit callback function return value + }> + /* + * Clear the error message, you can pass the FormPathPattern to batch or precise control of the field to be cleared. + * For example, clearErrors("*(aa,bb,cc)") + */ + clearErrors: (pattern?: FormPathPattern) => void + /* + * Get status changes, mainly used to determine which states in the current life cycle have changed in the form lifecycle hook. + * For example, hasChanged(state,'value.aa') + */ + hasChanged( + target: IFormState | IFieldState | IVirtualFieldState, + path: FormPathPattern + ): boolean + /* + * Reset form + */ + reset(options?: { + // Forced to empty + forceClear?: boolean // Forced check + validate?: boolean // Reset range for batch or precise control of the field to be reset + selector?: FormPathPattern + }): Promise + /* + * Validation form + */ + validate( + path?: FormPathPattern, + options?: { + // Is it pessimistic check, if the current field encounters the first verification error, stop the subsequent verification process + first?: boolean + } + ): Promise + /* + * Set the form status + */ + setFormState( // Operation callback + callback?: (state: IFormState) => any, // No trigger the event + silent?: boolean + ): void + /* + * Get form status + */ + getFormState( //transformer + callback?: (state: IFormState) => any + ): any + /* + * Set the field status + */ + setFieldState( // Field path + path: FormPathPattern, // Operation callback + callback?: (state: IFieldState) => void, // No trigger the event + silent?: boolean + ): void + /* + * Get the field status + */ + getFieldState( // Field path + path: FormPathPattern, // Transformer + callback?: (state: IFieldState) => any + ): any + /* + * Registration field + */ + registerField(props: { + // Node path + path?: FormPathPattern // Data path + name?: string // Field value + value?: any // Field multi-value + values?: any[] // Field initial value + initialValue?: any // Field extension properties + props?: any // Field check rule + rules?: ValidatePatternRules[] // Field is required + required?: boolean // Is the field editable? + editable?: boolean // Whether the field is dirty check + useDirty?: boolean // Field state calculation container, mainly used to extend the core linkage rules + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IField + /* + * Register virtual fields + */ + registerVirtualField(props: { + // Node path + path?: FormPathPattern // Data path + name?: string // Field extension properties + props?: any // Whether the field is dirty check + useDirty?: boolean // Field state calculation container, mainly used to extend the core linkage rules + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IVirtualField + /* + * Create a field data operator, which will explain the returned API in detail later. + */ + createMutators(field: IField): IMutators + /* + * Get the form observer tree + */ + getFormGraph(): IFormGraph + /* + * Set the form observer tree + */ + setFormGraph(graph: IFormGraph): void + /* + * Listen to the form life cycle + */ + subscribe( + callback?: ({ type, payload }: { type: string; payload: any }) => void + ): number + /* + * Cancel the listening form life cycle + */ + unsubscribe(id: number): void + /* + * Trigger form custom life cycle + */ + notify: (type: string, payload?: T) => void + /* + * Set the field value + */ + setFieldValue(path?: FormPathPattern, value?: any): void + /* + * Get the field value + */ + getFieldValue(path?: FormPathPattern): any + /* + * Set the initial value of the field + */ + setFieldInitialValue(path?: FormPathPattern, value?: any): void + /* + * Get the initial value of the field + */ + getFieldInitialValue(path?: FormPathPattern): any +} +``` + +#### IFormAsyncActions + +```typescript +interface IFormAsyncActions { + /* + * Form submission, if the callback parameter returns Promise, + * Then the entire submission process will hold and load is true. + * Wait for Promise resolve to trigger the form onFormSubmitEnd event while loading is false + */ + submit( + onSubmit?: (values: IFormState['values']) => void | Promise + ): Promise + /* + * Reset form + */ + reset(options?: IFormResetOptions): Promise + /* + * Get status changes, mainly used to determine which states in the current life cycle have changed in the form lifecycle hook. + * For example, hasChanged(state,'value.aa') + */ + hasChanged(target: any, path: FormPathPattern): Promise + /* + * Clear the error message, you can pass the FormPathPattern to batch or precise control of the field to be cleared. + * For example, clearErrors("*(aa,bb,cc)") + */ + clearErrors: (pattern?: FormPathPattern) => Promise + /* + * Validation form + */ + validate( + path?: FormPathPattern, + options?: { + // Is it pessimistic check, if the current field encounters the first verification error, stop the subsequent verification process + first?: boolean + } + ): Promise + /* + * Set the form state + */ + setFormState( + // Operation callback + callback?: (state: IFormState) => any, + // No trigger the event + silent?: boolean + ): Promise + /* + * Get form state + */ + getFormState( + //transformer + callback?: (state: IFormState) => any + ): Promise + /* + * Set the field state + */ + setFieldState( + // Field path + path: FormPathPattern, + // Operation callback + callback?: (state: IFieldState) => void, + // No trigger the event + silent?: boolean + ): Promise + /* + * Get the field state + */ + getFieldState( + // Field path + path: FormPathPattern, + //transformer + callback?: (state: IFieldState) => any + ): Promise + /* + * Get the form observer tree + */ + getFormGraph(): Promise + /* + * Set the form observer tree + */ + setFormGraph(graph: IFormGraph): Promise + /* + * Listen to the form life cycle + */ + subscribe(callback?: FormHeartSubscriber): Promise + /* + * Cancel the listening form life cycle + */ + unsubscribe(id: number): Promise + /* + * Trigger form custom life cycle + */ + notify: (type: string, payload: T) => Promise + dispatch: (type: string, payload: T) => void + /* + * Set the field value + */ + setFieldValue(path?: FormPathPattern, value?: any): Promise + /* + * Get the field value + */ + getFieldValue(path?: FormPathPattern): Promise + /* + * Set the initial value of the field + */ + setFieldInitialValue(path?: FormPathPattern, value?: any): Promise + /* + * Get the initial value of the field + */ + getFieldInitialValue(path?: FormPathPattern): Promise +} +``` + +#### IFieldState + +```typescript +interface IFieldState { + /**Read-only attribute**/ + // State name, FieldState + displayName?: string // Data path + name: string // Node path + path: string // Has been initialized + initialized: boolean // Is it in the original state, the state is true only when value===intialValues + pristine: boolean // Is it in a legal state, as long as the error length is greater than 0, the valid is false + valid: boolean // Is it illegal, as long as the error length is greater than 0, the valid is true + invalid: boolean // Is it in check state? + validating: boolean // Is it modified, if the value changes, the property is true, and will be true throughout the life of the field + modified: boolean // Is it touched? + touched: boolean // Is it activated, when the field triggers the onFocus event, it will be triggered to true, when onBlur is triggered, it is false + active: boolean // Have you ever visited, when the field triggers the onBlur event, it will be triggered to true + visited: boolean /** writable property**/ // Is it visible, note: if the state is false, then the value of the field will not be submitted, and the UI will not display + visible: boolean // Whether to show, note: if the state is false, then the value of the field will be submitted, the UI will not display, similar to the form hidden field + display: boolean // Is it editable? + editable: boolean // Is it in the loading state, note: if the field is in asynchronous verification, loading is true + loading: boolean // Field multi-parameter value, such as when the field onChange trigger, the event callback passed multi-parameter data, then the value of all parameters will be stored here + values: any[] // Field error message + errors: string[] // Field alert message + warnings: string[] // Field value, is equal to values[0] + value: any // Initial value + initialValue: any // Check the rules, the specific type description refers to the following documents + rules: ValidatePatternRules[] // Is it required? + required: boolean // Whether to mount + mounted: boolean // Whether to uninstall + unmounted: boolean // field extension properties + props: FieldProps +} +``` + +#### IVirtualFieldState + +```typescript +interface IVirtualFieldState { + /**Read-only status**/ + // State name, VirtualFieldState + displayName: string // Field data path + name: string // Field node path + path: string // Has been initialized + initialized: boolean /** writable status**/ // Is it visible, note: if the state is false, the UI will not be displayed, the data will not be submitted (because it is a VirtualField) + visible: boolean // Whether to show, note: if the state is false, the UI will not display, the data will not be submitted (because it is VirtualField) + display: boolean // Is it mounted? + mounted: boolean // Has been uninstalled + unmounted: boolean // field extension properties + props: FieldProps +} +``` + +#### SyncValidateResponse + +```typescript +declare type SyncValidateResponse = + | null + | string + | boolean + | { + type?: 'error' | 'warning' + message: string + } +``` + +#### AsyncValidateResponse + +```typescript +declare type AsyncValidateResponse = Promise +``` + +#### ValidateResponse + +```typescript +export declare type ValidateResponse = + | SyncValidateResponse + | AsyncValidateResponse +``` + +#### InternalFormats + +```typescript +type InternalFormats = + | 'url' + | 'email' + | 'ipv6' + | 'ipv4' + | 'idcard' + | 'taodomain' + | 'qq' + | 'phone' + | 'money' + | 'zh' + | 'date' + | 'zip' + | string +``` + +#### CustomValidator + +```typescript +declare type CustomValidator = ( + value: any, + rescription?: ValidateDescription +) => ValidateResponse +``` + +#### ValidateDescription + +```typescript +interface ValidateDescription { + // built-in rules,ref: string rules + format?: InternalFormats + // custom validation + validator?: CustomValidator + // required + required?: boolean + // pattern + pattern?: RegExp | string + // max length + max?: number + // maximum + maximum?: number + // exclusiveMaximum + exclusiveMaximum?: number + // exclusiveMinimum + exclusiveMinimum?: number + // minimum + minimum?: number + // min + min?: number + // length + len?: number + // whitespace + whitespace?: boolean + // enum + enum?: any[] + // error message + message?: string + [key: string]: any +} +``` + +#### ValidateArrayRules + +```typescript +declare type ValidateArrayRules = Array< + InternalFormats | CustomValidator | ValidateDescription +> +``` + +#### ValidatePatternRules + +```typescript +declare type ValidatePatternRules = + | InternalFormats + | CustomValidator + | ValidateDescription + | ValidateArrayRules +``` diff --git a/packages/react/README.zh-cn.md b/packages/react/README.zh-cn.md new file mode 100644 index 00000000000..2b554b058a0 --- /dev/null +++ b/packages/react/README.zh-cn.md @@ -0,0 +1,1842 @@ +# @uform/react + +> UForm 在 react 层的实现,内置表单状态核心管理(@uform/react), 通过结合 React 和核心管理机制,提供给开发者 API 可以快速操作表单,以及提供相应 UI 层渲染的支持。 +> @uform/react 中主要包含了以下部分: +> +> - Form 表单容器 +> - Field 表单字段 +> - VirtualField 虚拟表单字段 +> - FormaSpy 表单替身 +> - FormProvider 表单核心提供者 +> - FormConsumer 表单核心消费者(即将废弃,请使用 FormSpy) +> - createFormActions 创建表单核心操作 API 实例 +> - createAsyncFormActions 创建表单核心操作 API 实例(异步) +> - FormEffectHooks 表单生命周期 hook + +### 安装 + +```bash +npm install --save @uform/react +``` + +### 目录 + + + +- [使用方式](#使用方式) + - [`快速开始`](#快速开始) + - [`基础类型字段`](#基础类型字段) + - [`字段校验`](#字段校验) + - [`对象类型字段`](#对象类型字段) + - [`简单数组类型字段`](#简单数组类型字段) + - [`对象数组类型字段`](#对象数组类型字段) + - [`combo字段`](#combo字段) + - [`跨文件消费表单数据`](#跨文件消费表单数据) +- [Components](#components) + - [`Form`](#Form) + - [`Field`](#Field) + - [`VirtualField`](#VirtualField) + - [`FormSpy`](#FormSpy) + - [`FormProvider`](#FormProvider) + - [`FormConsumer(即将废弃,请使用FormSpy)`](<#FormConsumer(即将废弃,请使用FormSpy)>) +- [API](#API) + - [`createFormActions`](#createFormActions) + - [`createAsyncFormActions`](#createAsyncFormActions) + - [`FormEffectHooks`](#FormEffectHooks) + - [`createEffectHook`](#createEffectHook) +- [Interfaces](#Interfaces) + - [`IFormActions`](#IFormActions) + - [`IFormAsyncActions`](#IFormAsyncActions) + - [`IFieldState`](#IFieldState) + - [`IVirtualFieldState`](#IVirtualFieldState) + - [`SyncValidateResponse`](#SyncValidateResponse) + - [`AsyncValidateResponse`](#AsyncValidateResponse) + - [`ValidateResponse`](#ValidateResponse) + - [`InternalFormats`](#InternalFormats) + - [`CustomValidator`](#CustomValidator) + - [`ValidateDescription`](#ValidateDescription) + - [`ValidateArrayRules`](#ValidateArrayRules) + - [`ValidatePatternRules`](#ValidatePatternRules) + +### 使用方式 + +--- + +#### 快速开始 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + Form, + Field, + FormPath, + createFormActions, + FormSpy, + FormProvider, + FormConsumer, + FormEffectHooks +} from './src' + +const { onFormInit$, onFormInputChange$, onFieldInputChange$ } = FormEffectHooks +const actions = createFormActions() +const App = () => { + return ( +
    { + onFormInit$().subscribe(() => { + console.log('初始化') + }) + onFieldInputChange$().subscribe(state => { + console.log('输入变化', state) + }) + }} + onChange={() => {}} + > + + + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + + +
    + ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### 基础类型字段 + +示例:以输入框为例,如何快速绑定表单字段,后续例子都基于此字段拓展。 + +```typescript +const InputField = props => ( + + {({ state, mutators }) => ( +
    + + {state.errors} + {state.warnings} +
    + )} +
    +) +``` + +#### 字段校验 + +示例:必填校验 + error 类型校验 + warning 类型校验 + 自定义校验 +校验的类型可以是 [ValidatePatternRules](#ValidatePatternRules),即 [InternalFormats](#InternalFormats) | [CustomValidator](#CustomValidator) | [ValidateDescription](#ValidateDescription) | [ValidateArrayRules](#ValidateArrayRules) + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const App = () => { + return ( +
    +
    required validation
    + username + + +
    error type validation
    + age + + val === undefiend + ? { type: 'error', message: 'age is required' } + : undefined + ]} + /> + +
    warning type validation
    + gender + + val === undefiend + ? { type: 'warning', message: 'gender is required' } + : undefined + ]} + /> + +
    built-in validation default to error type validation
    + id + + +
    custom validation
    + verifyCode + + +
    + +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### 对象类型字段 + +示例:用户信息 `user(username, age)` + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const App = () => { + return ( +
    + user + + {({ state, mutators }) => { + return ( + + {Object.keys(state.value).map(key => { + if (!mutators.exist(key)) return + + return ( +
    + {key} + + +
    + ) + })} + + +
    + ) + }} +
    +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### 简单数组类型字段 + +示例:用户 id 列表,增删改查 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const App = () => { + return ( +
    + + {({ state, mutators }) => { + return ( + + {state.value.map((item, index) => { + return ( +
    + + +
    + ) + })} + +
    + ) + }} +
    +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### 对象数组类型字段 + +示例:用户 id 列表,增删改查 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const App = () => { + return ( +
    + + {({ state, mutators }) => { + return ( + + {state.value.map((item, index) => { + return ( +
    + + {({ state: innerState, mutators: innerMutator }) => { + return ( + + {Object.keys(innerState.value).map(key => { + if (!innerMutator.exist(key)) return + return ( + + + + + ) + })} + + + ) + }} + + + +
    + ) + })} + + +
    + ) + }} +
    +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### combo 字段 + +示例:combo username 和 age 字段, 更多用法,请点击[FormSpy](#FormSpy)查看 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, FormSpy } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const App = () => { + return ( +
    + + + + + + {({ state, form }) => { + return ( +
    + name: {form.getFieldValue('username')} +
    + age: {form.getFieldValue('age')} +
    + ) + }} +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### 跨文件消费表单数据 + +```typescript +文件目录 +--app + |---components + |---customForm +``` + +示例:跨文件消费表单数据, 更多用法,请参考[FormProvider](#FormProvider) 和 [FormSpy](#FormSpy) + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, FormSpy, FormProvider } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const CustomForm = () => { + return ( +
    + + + + + + ) +} + +const App = () => { + return ( + + + + {({ state, form }) => { + return ( +
    + name: {form.getFieldValue('username')} +
    + age: {form.getFieldValue('age')} +
    + ) + }} +
    +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Components + +--- + +#### Form + +> Form 组件属性定义 + +```typescript +interface IFormProps { + //值 + value?: any + defaultValue?: any + //初始值 + initialValues?: any + // formAPI + actions?: IFormActions | IFormAsyncActions + // 副作用 + effects?: IFormEffect + // IForm实例 + form?: IForm + onChange?: (values: Value) => void + onSubmit?: (values: Value) => void | Promise + onReset?: () => void + onValidateFailed?: (valideted: IFormValidateResult) => void + children?: React.ReactElement | ((form: IForm) => React.ReactElement) + //是否使用脏检查,默认会走immer精确更新 + useDirty?: boolean + // 是否可编辑,默认可编辑 + editable?: boolean + //是否走悲观校验,遇到第一个校验失败就停止后续校验 + validateFirst?: boolean +} +``` + +#### Field + +> Field 组件属性定义 + +```typescript +interface IFieldStateUIProps { + //节点路径 + path?: FormPathPattern + //节点路径 + nodePath?: FormPathPattern + //数据路径 + dataPath?: FormPathPattern + //数据路径 + name?: string + //字段值,与values[0]是恒定相等 + value?: any + //字段多参值,比如字段onChange触发时,给事件回调传了多参数据,那么这里会存储所有参数的值 + values?: any[] + //初始值 + initialValue?: any + //字段扩展属性 + props?: FieldProps + //校验规则,具体类型描述参考后面文档 + rules?: ValidatePatternRules[] + //是否必填 + required?: boolean + //字段是否可编辑 + editable?: boolean + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + // 触发校验类型 + triggerType?: 'onChange' | 'onBlur' + // 值格式化函数,从浏览器event中获取value + getValueFromEvent?: (...args: any[]) => any + children?: React.ReactElement | ((api: IFieldAPI) => React.ReactElement) +} +``` + +**用法** + +例子:各种类型的字段 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const App = () => { + return ( +
    +
    +
    Basic Field
    + + {({ state, mutator }) => { + return + }} + +
    + +
    +
    Object Field
    + + {({ state, mutators }) => { + return ( + + {Object.keys(state.value).map(key => { + if (!mutators.exist(key)) return + + return ( +
    + {key} + + +
    + ) + })} + +
    + ) + }} +
    +
    + +
    +
    ArrayField Field
    + + {({ state, mutators }) => { + return ( + + {state.value.map((item, index) => { + return ( +
    + + +
    + ) + })} + +
    + ) + }} +
    +
    + +
    +
    ArrayObject Field
    + + {({ state, mutators }) => { + return ( + + {state.value.map((item, index) => { + return ( +
    + + {({ state: innerState, mutators: innerMutator }) => { + return ( + + {Object.keys(innerState.value).map(key => { + if (!innerMutator.exist(key)) return + return ( + + + + + ) + })} + + + ) + }} + + + +
    + ) + })} + +
    + ) + }} +
    + + +
    + +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### VirtualField + +> VirtualField 组件属性定义 + +```typescript +interface IVirtualFieldProps { + //节点路径 + path?: FormPathPattern + //节点路径 + nodePath?: FormPathPattern + //数据路径 + dataPath?: FormPathPattern + //数据路径 + name?: string + //字段扩展属性 + props?: FieldProps + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + children?: React.ReactElement | ((api: IFieldAPI) => React.ReactElement) +} +``` + +**用法** + +例子:动态设置布局组件的属性 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, VirtualField } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const Layout = ({ children, width = '100px', height = '100px' }) => { + return ( +
    {children}
    + ) +} + +const App = () => { + return ( +
    + + {({ state, mutator }) => { + return ( + + {({ state: layoutState }) => { + return ( + + + + + + + ) + }} + + ) + }} + + + +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### FormSpy + +> FormSpy 组件属性定义 + +```typescript +interface IFormSpyProps { + // 选择器, 如:[ LifeCycleTypes.ON_FORM_SUBMIT_START, LifeCycleTypes.ON_FORM_SUBMIT_END ] + selector?: string[] | string + // reducer函数,状态叠加处理,action为当前命中的生命周期的数据 + reducer?: ( + state: any, + action: { type: string; payload: any }, + form: IForm + ) => any + children?: React.ReactElement | ((api: IFormSpyAPI) => React.ReactElement) +} +``` + +**用法** + +例子 1: 实现一个统计表单 values 改变的计数器 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, FormSpy, LifeCycleTypes } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const App = () => { + return ( +
    + + + + + ({ + count: state.count ? state.count + 1 : 1 + })} + > + {({ state, type, form }) => { + return
    count: {state.count || 0}
    + }} +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) + +``` + +例子 2:实现常用 combo 组件 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, FormSpy } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const App = () => { + return ( +
    + + + + + + {({ state, form }) => { + return ( +
    + name: {form.getFieldValue('username')} +
    + age: {form.getFieldValue('age')} +
    + ) + }} +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### FormProvider + +> 与 FormSpy 搭配使用,常用与跨文件通信 + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, FormSpy, FormProvider } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => ( + + + {state.errors} + {state.warnings} + + )} + +) + +const CustomForm = () => { + return ( +
    + + + + + + ) +} + +const App = () => { + return ( + + + + {({ state, form }) => { + return ( +
    + name: {form.getFieldValue('username')} +
    + age: {form.getFieldValue('age')} +
    + ) + }} +
    +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### FormConsumer(即将废弃,请使用 FormSpy) + +> FormConsumer 组件属性定义 + +```typescript +interface IFormConsumerProps { + // 选择器, 如:[ LifeCycleTypes.ON_FORM_SUBMIT_START, LifeCycleTypes.ON_FORM_SUBMIT_END ] + selector?: string[] | string + children?: + | React.ReactElement + | ((api: IFormConsumerAPI) => React.ReactElement) +} +``` + +### API + +--- + +#### `createFormActions` + +> 创建一个 [IFormActions](#IFormActions) 实例 + +**签名** + +```typescript +createFormActions(): IFormActions +``` + +**用法** + +```typescript +import { createFormActions } from '@uform/react' + +const actions = createFormActions() +console.log(actions.getFieldValue('username')) +``` + +#### `createAsyncFormActions` + +> 创建一个 [IFormAsyncActions](#IFormAsyncActions) 实例,成员方法 同[IFormActions](#IFormActions), +> 但是调用 API 返回的结果是异步的(promise)。 + +**签名** + +```typescript +createAsyncFormActions(): IFormAsyncActions +``` + +**用法** + +```typescript +import { createAsyncFormActions } from '@uform/react' + +const actions = createAsyncFormActions() +actions.getFieldValue('username').then(val => console.log(val)) +``` + +#### `FormEffectHooks` + +> 返回包含所有 UForm 生命周期的钩子函数,可以被监听消费 + +**用法** + +```typescript +import { FormEffectHooks, Form } from '@uform/react' +const { + /** + * Form LifeCycle + **/ + onFormWillInit$, // 表单预初始化触发 + onFormInit$, // 表单初始化触发 + onFormChange$, // 表单变化时触发 + onFormInputChange$, // 表单事件触发时触发,用于只监控人工操作 + onFormInitialValueChange$, // 表单初始值变化时触发 + onFormReset$, // 表单重置时触发 + onFormSubmit$, // 表单提交时触发 + onFormSubmitStart$, // 表单提交开始时触发 + onFormSubmitEnd$, // 表单提交结束时触发 + onFormMount$, // 表单挂载时触发 + onFormUnmount$, // 表单卸载时触发 + onFormValidateStart$, // 表单校验开始时触发 + onFormValidateEnd$, //表单校验结束时触发 + onFormValuesChange$, // 表单值变化时触发 + /** + * FormGraph LifeCycle + **/ + onFormGraphChange$, // 表单观察者树变化时触发 + /** + * Field LifeCycle + **/ + onFieldWillInit$, // 字段预初始化时触发 + onFieldInit$, // 字段初始化时触发 + onFieldChange$, // 字段变化时触发 + onFieldMount$, // 字段挂载时触发 + onFieldUnmount$, // 字段卸载时触发 + onFieldInputChange$, // 字段事件触发时触发,用于只监控人工操作 + onFieldValueChange$, // 字段值变化时触发 + onFieldInitialValueChange$ // 字段初始值变化时触发 +} = FormEffectHooks + +const App = () => { + return ( +
    { + onFormInit$().subscribe(() => { + console.log('初始化') + }) + }} + > + ... +
    + ) +} +``` + + +#### createEffectHook + +> 自定义hook + +**Usage** + +```jsx +import { Form, createEffectHook, createFormActions } from './src' + +const actions = createFormActions() +const diyHook1$ = createEffectHook('diy1') +const diyHook2$ = createEffectHook('diy2') + +const App = () => { + return ( +
    { + diyHook1$().subscribe((payload) => { + console.log('diy1 hook triggered', payload) + }) + + diyHook2$().subscribe((payload) => { + console.log('diy2 hook triggered', payload) + }) + }} + > + + +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Interfaces + +--- + +#### IFormActions + +```typescript +interface IFormActions { + /* + * 表单提交,如果回调参数返回Promise, + * 那么整个提交流程会hold住,同时loading为true, + * 等待Promise resolve才触发表单onFormSubmitEnd事件,同时loading为false + */ + submit( + onSubmit?: (values: IFormState['values']) => any | Promise + ): Promise<{ + validated: IFormValidateResult + payload: any //onSubmit回调函数返回值 + }> + + /* + * 清空错误消息,可以通过传FormPathPattern来批量或精确控制要清空的字段, + * 比如clearErrors("*(aa,bb,cc)") + */ + clearErrors: (pattern?: FormPathPattern) => void + + /* + * 获取状态变化情况,主要用于在表单生命周期钩子内判断当前生命周期中有哪些状态发生了变化, + * 比如hasChanged(state,'value.aa') + */ + hasChanged( + target: IFormState | IFieldState | IVirtualFieldState, + path: FormPathPattern + ): boolean + + /* + * 重置表单 + */ + reset(options?: { + //强制清空 + forceClear?: boolean + //强制校验 + validate?: boolean + //重置范围,用于批量或者精确控制要重置的字段 + selector?: FormPathPattern + }): Promise + + /* + * 校验表单 + */ + validate( + path?: FormPathPattern, + options?: { + //是否悲观校验,如果当前字段遇到第一个校验错误则停止后续校验流程 + first?: boolean + } + ): Promise + + /* + * 设置表单状态 + */ + setFormState( + //操作回调 + callback?: (state: IFormState) => any, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取表单状态 + */ + getFormState( + //transformer + callback?: (state: IFormState) => any + ): any + + /* + * 设置字段状态 + */ + setFieldState( + //字段路径 + path: FormPathPattern, + //操作回调 + callback?: (state: IFieldState) => void, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取字段状态 + */ + getFieldState( + //字段路径 + path: FormPathPattern, + //transformer + callback?: (state: IFieldState) => any + ): any + + /* + * 注册字段 + */ + registerField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段值 + value?: any + //字段多参值 + values?: any[] + //字段初始值 + initialValue?: any + //字段扩展属性 + props?: any + //字段校验规则 + rules?: ValidatePatternRules[] + //字段是否必填 + required?: boolean + //字段是否可编辑 + editable?: boolean + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IField + + /* + * 注册虚拟字段 + */ + registerVirtualField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段扩展属性 + props?: any + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IVirtualField + + /* + * 创建字段数据操作器,后面会详细解释返回的API + */ + createMutators(field: IField): IMutators + + /* + * 获取表单观察者树 + */ + getFormGraph(): IFormGraph + + /* + * 设置表单观察者树 + */ + setFormGraph(graph: IFormGraph): void + + /* + * 监听表单生命周期 + */ + subscribe( + callback?: ({ type, payload }: { type: string; payload: any }) => void + ): number + + /* + * 取消监听表单生命周期 + */ + unsubscribe(id: number): void + + /* + * 触发表单自定义生命周期 + */ + notify: (type: string, payload?: T) => void + + /* + * 设置字段值 + */ + setFieldValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段值 + */ + getFieldValue(path?: FormPathPattern): any + + /* + * 设置字段初始值 + */ + setFieldInitialValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段初始值 + */ + getFieldInitialValue(path?: FormPathPattern): any +} +``` + +#### IFormAsyncActions + +```typescript +interface IFormAsyncActions { + /* + * 表单提交,如果回调参数返回Promise, + * 那么整个提交流程会hold住,同时loading为true, + * 等待Promise resolve才触发表单onFormSubmitEnd事件,同时loading为false + */ + submit( + onSubmit?: (values: IFormState['values']) => void | Promise + ): Promise + /* + * 重置表单 + */ + reset(options?: IFormResetOptions): Promise + /* + * 获取状态变化情况,主要用于在表单生命周期钩子内判断当前生命周期中有哪些状态发生了变化, + * 比如hasChanged(state,'value.aa') + */ + hasChanged(target: any, path: FormPathPattern): Promise + /* + * 清空错误消息,可以通过传FormPathPattern来批量或精确控制要清空的字段, + * 比如clearErrors("*(aa,bb,cc)") + */ + clearErrors: (pattern?: FormPathPattern) => Promise + /* + * 校验表单 + */ + validate( + path?: FormPathPattern, + options?: { + //是否悲观校验,如果当前字段遇到第一个校验错误则停止后续校验流程 + first?: boolean + } + ): Promise + /* + * 设置表单状态 + */ + setFormState( + //操作回调 + callback?: (state: IFormState) => any, + //是否不触发事件 + silent?: boolean + ): Promise + /* + * 获取表单状态 + */ + getFormState( + //transformer + callback?: (state: IFormState) => any + ): Promise + /* + * 设置字段状态 + */ + setFieldState( + //字段路径 + path: FormPathPattern, + //操作回调 + callback?: (state: IFieldState) => void, + //是否不触发事件 + silent?: boolean + ): Promise + /* + * 获取字段状态 + */ + getFieldState( + //字段路径 + path: FormPathPattern, + //transformer + callback?: (state: IFieldState) => any + ): Promise + getFormGraph(): Promise + setFormGraph(graph: IFormGraph): Promise + subscribe(callback?: FormHeartSubscriber): Promise + unsubscribe(id: number): Promise + notify: (type: string, payload: T) => Promise + dispatch: (type: string, payload: T) => void + setFieldValue(path?: FormPathPattern, value?: any): Promise + getFieldValue(path?: FormPathPattern): Promise + setFieldInitialValue(path?: FormPathPattern, value?: any): Promise + getFieldInitialValue(path?: FormPathPattern): Promise +} +``` + +#### IFieldState + +```typescript +interface IFieldState { + /**只读属性**/ + + //状态名称,FieldState + displayName?: string + //数据路径 + name: string + //节点路径 + path: string + //是否已经初始化 + initialized: boolean + //是否处于原始态,只有value===intialValues时的时候该状态为true + pristine: boolean + //是否处于合法态,只要errors长度大于0的时候valid为false + valid: boolean + //是否处于非法态,只要errors长度大于0的时候valid为true + invalid: boolean + //是否处于校验态 + validating: boolean + //是否被修改,如果值发生变化,该属性为true,同时在整个字段的生命周期内都会为true + modified: boolean + //是否被触碰 + touched: boolean + //是否被激活,字段触发onFocus事件的时候,它会被触发为true,触发onBlur时,为false + active: boolean + //是否访问过,字段触发onBlur事件的时候,它会被触发为true + visited: boolean + + /**可写属性**/ + + //是否可见,注意:该状态如果为false,那么字段的值不会被提交,同时UI不会显示 + visible: boolean + //是否展示,注意:该状态如果为false,那么字段的值会提交,UI不会展示,类似于表单隐藏域 + display: boolean + //是否可编辑 + editable: boolean + //是否处于loading状态,注意:如果字段处于异步校验时,loading为true + loading: boolean + //字段多参值,比如字段onChange触发时,给事件回调传了多参数据,那么这里会存储所有参数的值 + values: any[] + //字段错误消息 + errors: string[] + //字段告警消息 + warnings: string[] + //字段值,与values[0]是恒定相等 + value: any + //初始值 + initialValue: any + //校验规则,具体类型描述参考后面文档 + rules: ValidatePatternRules[] + //是否必填 + required: boolean + //是否挂载 + mounted: boolean + //是否卸载 + unmounted: boolean + //字段扩展属性 + props: FieldProps +} +``` + +#### IVirtualFieldState + +> 虚拟 Field 核心状态 + +```typescript +interface IVirtualFieldState { + /**只读状态**/ + + //状态名称,VirtualFieldState + displayName: string + //字段数据路径 + name: string + //字段节点路径 + path: string + //是否已经初始化 + initialized: boolean + + /**可写状态**/ + + //是否可见,注意:该状态如果为false,UI不会显示,数据也不会提交(因为它是VirtualField) + visible: boolean + //是否展示,注意:该状态如果为false,UI不会显示,数据也不会提交(因为它是VirtualField) + display: boolean + //是否已挂载 + mounted: boolean + //是否已卸载 + unmounted: boolean + //字段扩展属性 + props: FieldProps +} +``` + +#### SyncValidateResponse + +```typescript +declare type SyncValidateResponse = + | null + | string + | boolean + | { + type?: 'error' | 'warning' + message: string + } +``` + +#### AsyncValidateResponse + +```typescript +declare type AsyncValidateResponse = Promise +``` + +#### ValidateResponse + +```typescript +export declare type ValidateResponse = + | SyncValidateResponse + | AsyncValidateResponse +``` + +#### InternalFormats + +```typescript +type InternalFormats = + | 'url' + | 'email' + | 'ipv6' + | 'ipv4' + | 'idcard' + | 'taodomain' + | 'qq' + | 'phone' + | 'money' + | 'zh' + | 'date' + | 'zip' + | string +``` + +#### CustomValidator + +```typescript +declare type CustomValidator = ( + value: any, + rescription?: ValidateDescription +) => ValidateResponse +``` + +#### ValidateDescription + +```typescript +interface ValidateDescription { + // 内置校验规则,参考string内置校验规则 + format?: InternalFormats + // 自定义校验规则 + validator?: CustomValidator + // 是否必填 + required?: boolean + // 匹配规则 + pattern?: RegExp | string + // 最大长度 + max?: number + // 最大值(大于) + maximum?: number + // 最大值(大于等于) + exclusiveMaximum?: number + // 最小值(小于等于) + exclusiveMinimum?: number + // 最小值(小于) + minimum?: number + // 最小长度 + min?: number + // 长度 + len?: number + // 空格 + whitespace?: boolean + // 是否包含在枚举列表中 + enum?: any[] + // 错误信息 + message?: string + [key: string]: any +} +``` + +#### ValidateArrayRules + +```typescript +declare type ValidateArrayRules = Array< + InternalFormats | CustomValidator | ValidateDescription +> +``` + +#### ValidatePatternRules + +```typescript +declare type ValidatePatternRules = + | InternalFormats + | CustomValidator + | ValidateDescription + | ValidateArrayRules +``` diff --git a/packages/validator/src/types.ts b/packages/validator/src/types.ts index 3c2e8b41003..e7f2fcf4a8b 100644 --- a/packages/validator/src/types.ts +++ b/packages/validator/src/types.ts @@ -53,11 +53,11 @@ export interface ValidateDescription { export type ValidateRules = ValidateDescription[] export type ValidateArrayRules = Array< - string | CustomValidator | ValidateDescription +InternalFormats | CustomValidator | ValidateDescription > export type ValidatePatternRules = - | string + | InternalFormats | CustomValidator | ValidateDescription | ValidateArrayRules diff --git a/scripts/docs.js b/scripts/docs.js index 9423d859cc4..8c8f61a6286 100644 --- a/scripts/docs.js +++ b/scripts/docs.js @@ -36,6 +36,7 @@ const createDocs = async () => { }) .reduce((buf, _path) => { const name = path.basename(_path) + return { ...buf, [`@uform/${name}`]: `${_path}/src` From dff959c87c62c2c63c401b819788fade7c5da847 Mon Sep 17 00:00:00 2001 From: Janry Date: Wed, 20 Nov 2019 00:35:50 +0800 Subject: [PATCH 46/99] feat: support useFormEffects (#403) --- packages/antd/src/fields/table.tsx | 4 +- .../react-schema-renderer/README.zh-cn.md | 484 +++++++++++++++++- .../src/components/SchemaMarkup.tsx | 11 +- packages/react/src/components/FormSpy.tsx | 63 +-- packages/react/src/hooks/useForm.ts | 2 +- packages/react/src/hooks/useFormEffects.ts | 25 + packages/react/src/hooks/useFormSpy.ts | 67 +++ packages/react/src/index.ts | 1 + packages/react/src/shared.ts | 2 +- packages/react/src/types.ts | 6 +- packages/validator/src/types.ts | 2 +- 11 files changed, 589 insertions(+), 78 deletions(-) create mode 100644 packages/react/src/hooks/useFormEffects.ts create mode 100644 packages/react/src/hooks/useFormSpy.ts diff --git a/packages/antd/src/fields/table.tsx b/packages/antd/src/fields/table.tsx index 01be7668509..19a0c896190 100644 --- a/packages/antd/src/fields/table.tsx +++ b/packages/antd/src/fields/table.tsx @@ -5,7 +5,7 @@ import { SchemaField, Schema } from '@uform/react-schema-renderer' -import { toArr, isFn, isArr } from '@uform/shared' +import { toArr, isFn, isArr, FormPath } from '@uform/shared' import { ArrayList } from '@uform/react-shared-components' import { CircleButton, TextButton } from '../components/Button' import { Table, Form, Icon } from 'antd' @@ -54,7 +54,7 @@ const FormTableField = styled( render: (value: any, record: any, index: number) => { return ( - + ) } diff --git a/packages/react-schema-renderer/README.zh-cn.md b/packages/react-schema-renderer/README.zh-cn.md index 4858564264b..a15eda567b5 100644 --- a/packages/react-schema-renderer/README.zh-cn.md +++ b/packages/react-schema-renderer/README.zh-cn.md @@ -19,23 +19,499 @@ npm install --save @uform/react-schema-renderer #### 快速开始 +如果您是直接基于@uform/react-schema-renderer做开发的,那么您必须在开发前将自定义组件注册到渲染器里去,否则,我们的JSON-Schema协议是不能渲染表单的。所以: + +```jsx +import { SchemaForm, registerFormField,connect } from '@uform/react-schema-renderer' + +registerFormField('string',connect()(({value,onChange})=>{ + return +})) + +export default ()=>{ + return ( + + ) +} +``` + +大工告成,这个就是最简单的用法,核心就是注册组件,然后使用协议渲染。**需要注意一点是,我们在注册组件的时候使用了connect函数,这个connect函数的功能就是,让任意一个组件,只要支持value/onChange API的,都可以快速注册到SchemaForm里面去,同时,connect函数也屏蔽了Field API,所以使用了connect函数的组件,是不能做更加强大的扩展的,详细的connect API后面会有介绍**。同时,还有一个要注意的就是,如果我们要接入一套组件库的,业内大多数组件库其实都是有自己的Form和FormItem组件的,他们核心是用于控制样式,FormItem控制表单局部样式,Form控制全局表单样式,所以在生产环境下,其实我们还需要注册Form和FormItem组件,这样才能做到样式与原有解决方案的一致性,具体如何注册,我们会在后面有详细介绍。 + #### JSON Schema驱动表单渲染 +说到JSON Schema,上面一个例子其实已经涉及了,当然,它并不够复杂,我们看一个较为复杂的例子: + +```tsx +import { SchemaForm } from '@uform/react-schema-renderer' + +registerFormField('string',connect()(({value,onChange})=>{ + return +})) + +registerFormField('array',()=>{ + return //... +}) + +export default ()=>{ + return ( + + ) +} +``` + +上面的代码是一段伪代码,因为我们并没有注册array类型的自定义组件,这里先暂时不讲如何注册array类型的自定义组件,我们核心是分析JSON Schema是如何驱动表单渲染的。在这份JSON Schema中,我们主要使用了properties和items属性用来描述复杂数据结构,这就是JSON Schema最核心的特性,注意:**在SchemaForm中,内置了object的properties的递归渲染,但是并没有内置array的items递归渲染**,主要原因是,array的递归渲染会涉及很多样式需求,并不方便内置,所以最好还是留给开发者自己实现,所以,后面我们会详细介绍如何实现自增列表的递归渲染需求。 + #### JSchema驱动表单渲染 -#### 快速接入第三方组件库 +JSchema就是在jsx中以一种更优雅的写法来描述JSON Schema,我们可以针对以上例子用JSchema实现一版: + +```tsx +import { SchemaForm,Field } from '@uform/react-schema-renderer' + +export default ()=>{ + return ( + + + + + + + + ) +} +``` + +可以看到,使用JSchema在代码中描述JSON Schema比起JSON而言变得更加优雅了,大大的提高了代码可维护性。 + +#### 非单例注册组件 + +在前面的例子中,我们使用了registerFormField API来注册了自定义组件,这种方式是单例注册的方式,它的主要优点就是方便,但是也会存在一些问题,就是单例容易受污染,特别是在SPA页面中,如果不同页面的开发者是不一样的,因为共享同一个内存环境,那么A开发者可能会注册B开发者同名的自定义组件,这样就很容易导致线上故障,所以,我们更加推荐用户使用非单例注册方式: + +```jsx +import React,{ useMemo } from 'react' +import { SchemaForm, registerFormField,connect } from '@uform/react-schema-renderer' + +const StringField = connect()(({value,onChange})=>{ + return +}) -#### 使用自定义组件建立自己的表单组件生态 +const useFields = ()=>useMemo(()=>{ + string:StringField +}) -#### 使用VirtualBox组件建立自己的表单布局生态 +export default ()=>{ + return ( + + ) +} +``` + +在上面的例子中,我们主要是在SchemaForm的props维度来传递自定义组件,这样就能保证是实例级注册了,这样的形式对SPA非常友好,同时,**需要注意的是我们抽象了一个useFields的React Hook,它主要用于解决组件多次渲染的时候不会影响React Virtual DOM重新计算,从而避免表单组件重复渲染**。 ### 高级教程 --- -#### 如何实现自己的递归渲染组件? +#### 如何接入第三方组件库? + +因为@uform/react-schema-renderer是一个基础库,默认不会集成任何组件库的,所以我们在实际业务开发中,如果要基于它来定制,那么就必须得面对接入第三方组件库的问题。如何接入第三方组件库,我们分为以下几步: + +- 接入Form/FormItem组件 +- 接入组件库表单组件 +- 实现表单布局组件 +- 实现自增列表组件 + +下面就让我们一步步的来接入第三方组件库吧!这里我们主要以antd组件库为例子。 + +#### 如何接入Form/FormItem组件? + +接入方式目前提供了全局注册机制与单例注册机制,全局注册主要使用registerFormComponent和registerFormItemComponent两个API来注册,单例注册则是直接在SchemaForm属性上传formComponent和formItemComponent。如果是SPA场景,推荐使用单例注册的方式,下面看看例子: + +```tsx +import { + SchemaForm, + registerFormComponent, + registerFormItemComponent +} from '@uform/react-schema-renderer' +import { Form } from 'antd' + +export const CompatFormComponent = ({children,...props})=>{ + return
    {children}
    //很简单的使用Form组件,props是SchemaForm组件的props,这里会直接透传 +}) + +export const CompatFormItemComponent = ({children,...props})=>{ + const messages = [].concat(props.errors || [], props.warnings || []) + let status = '' + if (props.loading) { + status = 'validating' + } + if (props.invalid) { + status = 'error' + } + if (props.warnings && props.warnings.length) { + status = 'warning' + } + return ( + + {children} + + ) +} + +/*** +全局注册方式 +registerFormComponent(CompatFormComponent) +registerFormItemComponent(CompatFormItemComponent) +***/ + +//单例注册方式 +export default ()=>{ + return ( + +} + +``` + +我们可以看到,扩展表单整体或局部的样式,仅仅只需要通过扩展Form/FormItem组件就可以轻松解决了,这里需要注意的是,FormItem组件接收到的props有点复杂,不用担心,后面会列出详细props API,现在我们只需要知道大概是如何注册的就行了。 + +#### 如何接入表单组件? + +因为组件库的所有组件都是原子型组件,同时大部分都兼容了value/onChange规范,所以我们可以借助connect函数快速接入组件库的组件,通常,我们接入组件库组件,大概要做3件事情: + +- 处理状态映射,将uform内部的loading/error状态映射到该组件属性上,当然,**前提是要求组件必须支持loading或error这类的样式** +- 处理详情态样式,将uform内部的editable状态,映射到一个PreviewText组件上去,用于更友好更干净的展示数据 +- 处理组件枚举态,我们想一下,**JSON Schema,每一个节点都应该支持enum属性的**,如果配了enum属性,我们最好都以Select形式来展现,所以我们需要处理一下组件枚举态 + +咱们以InputNumber为例演示一下: + +```tsx +import { connect, registerFormField } from '@uform/react-schema-renderer' +import { InputNumber } from 'antd' + +const mapTextComponent = ( + Target: React.JSXElementConstructor, + props: any = {}, + fieldProps: any = {} +): React.JSXElementConstructor => { + const { editable } = fieldProps + if (editable !== undefined) { + if (editable === false) { + return PreviewText + } + } + if (Array.isArray(props.dataSource)) { + return Select + } + return Target +} + +const mapStyledProps = ( + props: IConnectProps, + fieldProps: MergedFieldComponentProps +) => { + const { loading, errors } = fieldProps + if (loading) { + props.state = props.state || 'loading' + } else if (errors && errors.length) { + props.state = 'error' + } +} + +const acceptEnum = (component: React.JSXElementConstructor) => { + return ({ dataSource, ...others }) => { + if (dataSource) { + return React.createElement(Select, { dataSource, ...others }) + } else { + return React.createElement(component, others) + } + } +} + +registerFormField( + 'number', + connect({ + getProps: mapStyledProps,//处理状态映射 + getComponent: mapTextComponent//处理详情态 + })(acceptEnum(InputNumber))//处理枚举态 +) + +``` + +在这个例子中,我们深度使用了connect函数,其实connect就是一个HOC,在渲染阶段,它可以在组件渲染过程中加入一些中间处理逻辑,帮助动态分发。当然,connect还有很多API,后面会详细介绍。 + +#### 如何处理表单布局? + +JSON Schema描述表单数据结构,其实是天然支持的,但是表单最终还是落在UI层面的,可惜在UI层面上我们有很多组件其实并不能作为JSON Schema的一个具体数据节点,它仅仅只是一个UI节点。所以,想要在JSON Schema中描述复杂布局,怎么做? + +现在uform的做法是,抽象了一个叫**虚拟节点**的概念,用户在代码层面上指定某个JSON Schema x-component为虚拟节点之后,后面不管是在渲染,还是在数据处理,还是最终数据提交,只要解析到这个节点是虚拟节点,都不会将它当做一个正常的数据节点。所以,有了这个虚拟节点的概念,我们就可以在JSON Schema中描述各种复杂布局,下面让我们试着写一个布局组件: + +```tsx +import { SchemaForm,registerVirtualBox } from '@uform/react-schema-renderer' +import { Card } from 'antd' + +registerVirtualBox('card',({children,...props})=>{ + return {children} +}) + +export default ()=>{ + return ( + + ) +} +``` + +从这段伪代码中我们可以看到card就是一个正常的Object Schema节点,只是需要指定一个x-component为card,这样就能和registerVirtualBox注册的card匹配上,就达到了虚拟节点的效果,所以,不管你把JSON Schema中的属性名改为什么,都不会影响最终的提交的数据结构。**这里需要注意的是x-component-props是直接透传到registerVirtualBox的回调函数参数上的。** 这是JSON Schema形式的使用,我们还有JSchema的使用方式: + +```tsx +import { SchemaForm,createVirtualBox } from '@uform/react-schema-renderer' +import { Card } from 'antd' + +const Card = createVirtualBox('card',({children,...props})=>{ + return {children} +}) + +export default ()=>{ + return ( + + + + + + + + + + ) +} +``` + +从这个例子中我们可以看到,借助createVirtualBox API可以快速创建一个布局组件,同时在JSchema中直接使用。**其实createVirtualBox的内部实现很简单,还是使用了registerVitualBox和Field**: + +```tsx +export function createVirtualBox( + key: string, + component?: React.JSXElementConstructor> +) { + registerVirtualBox( + key, + component + ? ({ props,schema, children }) => { + return React.createElement(component, { + ...schema.getExtendsComponentProps(), + children + }) + } + : () => + ) + const VirtualBox: React.FC = ({ + children, + name, + ...props + }) => { + return ( + + {children} + + ) + } + return VirtualBox +} +``` + +前面介绍的注册布局组件的方式,其实都是单例注册,如果我们需要实例形式的注册,还是与前面说的方式类似 + +```tsx + +const Card = ({children,...props})=>{ + return {children} +} + +export default ()=>{ + return ( + + ) +} +``` + + + +#### 如何实现递归渲染组件? + +什么叫递归渲染组件?其实就是**实现了JSON Schema中properties和items的组件**,像`type:"string"` 这种节点就属于原子节点,不属于递归渲染组件。其实像前面说的布局组件,其实它也是属于递归渲染组件,只是它固定了渲染模式,所以可以很简单的注册。所以,我们大部分想要实现递归渲染的场景,可能实际业务场景中,更多的是在`type:"array"`这种场景才会去实现递归渲染,下面我们会详细介绍自增列表组件的实现方式。 + +#### 如何实现自增列表组件? + +自增列表它主要有几个特点: + +- 有独立样式 +- 支持递归渲染子组件 +- 支持数组项的新增,删除,上移,下移 +- 不能使用connect函数包装,因为必须调用Field API + +为了帮助大家更好的理解如何实现自增列表组件,我们就不实现具体样式了,更多的是教大家如何实现递归渲染和,数组项的操作。下面我们看伪代码: + +```tsx +import React, { Fragment } from 'react' +import { + registerFormField, + SchemaField, + FormPath +} from '@uform/react-schema-renderer' + +//不用connect包装 +registerFormField('array',({value,path,mutators})=>{ + + const emptyUI = + + const listUI = value.map((item,index)=>{ + return ( +
    + + + + +
    + ) + }) + + return ( + value.length == 0 ? emptyUI : listUI + ) +}) +``` + +看到了没,要实现一个带递归渲染的自增列表组件,超级简单,反而如果要实现相关的样式就会有点麻烦,总之核心就是使用了SchemaField这个组件和mutators API,具体API会在后面详细介绍。 #### 如何实现超复杂自定义组件? +这个问题,在老版UForm中基本无解,恰好也是因为我们这边的业务复杂度高到一定程度之后,我们自己被这个问题给受限制了,所以必须得想办法解决这个问题,下面我们可以定义一下,什么才是超复杂自定义组件: + +- 组件内部存在大量表单组件,同时内部也存在大量联动关系 +- 组件内部存在私有的服务端动态渲染方案 + +- 组件内部有复杂布局结构 + +就这3点,基本上满足超复杂自定义组件的特征了,对于这种场景,为什么我们通过正常的封装自定义组件的形式不能解决问题呢?其实主要是受限于校验,没法整体校验,所以,我们需要一个能聚合大量字段处理逻辑的能力,那么,我们该如何解决呢? + ### 协议 --- diff --git a/packages/react-schema-renderer/src/components/SchemaMarkup.tsx b/packages/react-schema-renderer/src/components/SchemaMarkup.tsx index a57712d4c19..c61e25819de 100644 --- a/packages/react-schema-renderer/src/components/SchemaMarkup.tsx +++ b/packages/react-schema-renderer/src/components/SchemaMarkup.tsx @@ -71,17 +71,14 @@ SchemaMarkupForm.displayName = 'SchemaMarkupForm' export function createVirtualBox( key: string, - component?: React.JSXElementConstructor> + component?: React.JSXElementConstructor ) { registerVirtualBox( key, component - ? ({ props, children }) => { - return React.createElement(component, { - ...props['x-props'], - ...props['x-component-props'], - children - }) + ? ({ schema, children }) => { + const props = schema.getExtendsComponentProps() + return React.createElement(component, props, children) } : () => ) diff --git a/packages/react/src/components/FormSpy.tsx b/packages/react/src/components/FormSpy.tsx index a88b98dd370..b8fa7d7ff26 100644 --- a/packages/react/src/components/FormSpy.tsx +++ b/packages/react/src/components/FormSpy.tsx @@ -1,66 +1,11 @@ -import { - useContext, - useMemo, - useRef, - useEffect, - useCallback, - useState, - useReducer -} from 'react' -import { FormHeartSubscriber, LifeCycleTypes } from '@uform/core' -import { isFn, isStr, FormPath, isArr } from '@uform/shared' +import React from 'react' +import { isFn } from '@uform/shared' import { IFormSpyProps } from '../types' -import FormContext, { BroadcastContext } from '../context' +import { useFormSpy } from '../hooks/useFormSpy' export const FormSpy: React.FunctionComponent = props => { - const broadcast = useContext(BroadcastContext) - const form = useContext(FormContext) - const initializedRef = useRef(false) - const subscriberId = useRef() - const [type, setType] = useState(LifeCycleTypes.ON_FORM_INIT) - const [state, dispatch] = useReducer( - (state, action) => props.reducer(state, action, form), - {} - ) - const subscriber = useCallback(({ type, payload }) => { - if (initializedRef.current) return - setTimeout(() => { - if (isStr(props.selector) && FormPath.parse(props.selector).match(type)) { - setType(type) - dispatch({ - type, - payload - }) - } else if (isArr(props.selector) && props.selector.indexOf(type) > -1) { - setType(type) - dispatch({ - type, - payload - }) - } - }) - }, []) - useMemo(() => { - initializedRef.current = true - if (form) { - subscriberId.current = form.subscribe(subscriber) - } else if (broadcast) { - subscriberId.current = broadcast.subscribe(subscriber) - } - initializedRef.current = false - }, []) - useEffect(() => { - return () => { - if (form) { - form.unsubscribe(subscriberId.current) - } else if (broadcast) { - broadcast.unsubscribe(subscriberId.current) - } - } - }, []) if (isFn(props.children)) { - const formApi = form ? form : broadcast && broadcast.getContext() - return props.children({ form: formApi, type, state }) + return props.children(useFormSpy(props)) } else { return props.children } diff --git a/packages/react/src/hooks/useForm.ts b/packages/react/src/hooks/useForm.ts index 64d4fd78188..84798f88d9f 100644 --- a/packages/react/src/hooks/useForm.ts +++ b/packages/react/src/hooks/useForm.ts @@ -77,7 +77,7 @@ export const useForm = < }) const lifecycles = [ new FormLifeCycle( - ({ type, payload }: { type: string; payload: IModel }) => { + ({ type, payload }) => { dispatch.lazy(type, () => { return isStateModel(payload) ? payload.getState() : payload }) diff --git a/packages/react/src/hooks/useFormEffects.ts b/packages/react/src/hooks/useFormEffects.ts new file mode 100644 index 00000000000..aeee34fb8f6 --- /dev/null +++ b/packages/react/src/hooks/useFormEffects.ts @@ -0,0 +1,25 @@ +import { useContext, useEffect } from 'react' +import { isStateModel, LifeCycleTypes } from '@uform/core' +import FormContext from '../context' +import { useEva } from 'react-eva' +import { IFormEffect } from '../types' +import { createFormEffects } from '../shared' + + +export function useFormEffects(effects: IFormEffect) { + const form = useContext(FormContext) + const { dispatch } = useEva({ + effects: createFormEffects(effects, form) + }) + useEffect(() => { + const subscribeId = form.subscribe(({ type, payload }) => { + dispatch.lazy(type, () => { + return isStateModel(payload) ? payload.getState() : payload + }) + }) + dispatch(LifeCycleTypes.ON_FORM_INIT, form.getFormState()) + return () => { + form.unsubscribe(subscribeId) + } + }, []) +} diff --git a/packages/react/src/hooks/useFormSpy.ts b/packages/react/src/hooks/useFormSpy.ts new file mode 100644 index 00000000000..1c2000f194f --- /dev/null +++ b/packages/react/src/hooks/useFormSpy.ts @@ -0,0 +1,67 @@ +import { + useContext, + useMemo, + useRef, + useEffect, + useCallback, + useState, + useReducer +} from 'react' +import { FormHeartSubscriber, LifeCycleTypes } from '@uform/core' +import { isStr, FormPath, isArr } from '@uform/shared' +import { IFormSpyProps } from '../types' +import FormContext, { BroadcastContext } from '../context' + +export const useFormSpy = (props: IFormSpyProps) => { + const broadcast = useContext(BroadcastContext) + const form = useContext(FormContext) + const initializedRef = useRef(false) + const subscriberId = useRef() + const [type, setType] = useState(LifeCycleTypes.ON_FORM_INIT) + const [state, dispatch] = useReducer( + (state, action) => props.reducer(state, action, form), + {} + ) + const subscriber = useCallback(({ type, payload }) => { + if (initializedRef.current) return + setTimeout(() => { + if (isStr(props.selector) && FormPath.parse(props.selector).match(type)) { + setType(type) + dispatch({ + type, + payload + }) + } else if (isArr(props.selector) && props.selector.indexOf(type) > -1) { + setType(type) + dispatch({ + type, + payload + }) + } + }) + }, []) + useMemo(() => { + initializedRef.current = true + if (form) { + subscriberId.current = form.subscribe(subscriber) + } else if (broadcast) { + subscriberId.current = broadcast.subscribe(subscriber) + } + initializedRef.current = false + }, []) + useEffect(() => { + return () => { + if (form) { + form.unsubscribe(subscriberId.current) + } else if (broadcast) { + broadcast.unsubscribe(subscriberId.current) + } + } + }, []) + const formApi = form ? form : broadcast && broadcast.getContext() + return { + form: formApi, + type, + state + } +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 7427cdcb193..df70b023a5d 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -14,6 +14,7 @@ export * from './components/FormConsumer' export * from './hooks/useForm' export * from './hooks/useField' export * from './hooks/useVirtualField' +export * from './hooks/useFormEffects' export * from './types' export { diff --git a/packages/react/src/shared.ts b/packages/react/src/shared.ts index b2c65bbf607..0bde589128a 100644 --- a/packages/react/src/shared.ts +++ b/packages/react/src/shared.ts @@ -166,7 +166,7 @@ export const env = { export const [raf, caf] = getScheduler() -export const createFormEffects = ( +export const createFormEffects = ( effects: IFormEffect | null, actions: Actions ) => { diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 6a92e9fe1fe..95f243d7ca9 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -14,7 +14,7 @@ import { } from '@uform/core' import { FormPathPattern } from '@uform/shared' import { Observable } from 'rxjs/internal/Observable' -export interface IFormEffect { +export interface IFormEffect { ( selector: IFormExtendsEffectSelector, actions: Actions @@ -30,14 +30,14 @@ export interface IFormEffectSelector { export type IFormExtendsEffectSelector< Payload = any, - Actions = {} + Actions = any > = IFormEffectSelector & Actions export interface IFormProps< Value = {}, DefaultValue = {}, EffectPayload = any, - EffectActions = {} + EffectActions = any > { value?: Value defaultValue?: DefaultValue diff --git a/packages/validator/src/types.ts b/packages/validator/src/types.ts index e7f2fcf4a8b..7d91d809f2c 100644 --- a/packages/validator/src/types.ts +++ b/packages/validator/src/types.ts @@ -53,7 +53,7 @@ export interface ValidateDescription { export type ValidateRules = ValidateDescription[] export type ValidateArrayRules = Array< -InternalFormats | CustomValidator | ValidateDescription + InternalFormats | CustomValidator | ValidateDescription > export type ValidatePatternRules = From 7b73255bcbdce8266b59ff542f4b165fe547198f Mon Sep 17 00:00:00 2001 From: Janry Date: Wed, 20 Nov 2019 17:13:49 +0800 Subject: [PATCH 47/99] fix(@uform/core): fix required (#404) --- docs/Examples/antd/Layout.md | 28 +++--- docs/Examples/next/Layout.md | 7 +- packages/antd/src/components/FormItemGrid.tsx | 10 +- packages/antd/src/components/FormStep.tsx | 55 ++++++----- packages/antd/src/components/FormTextBox.tsx | 6 +- packages/core/src/__tests__/index.spec.ts | 6 +- packages/core/src/state/field.ts | 91 ++++++++++++------- packages/next/src/components/FormStep.tsx | 51 +++++------ .../react-schema-renderer/README.zh-cn.md | 8 +- .../src/components/SchemaField.tsx | 8 +- 10 files changed, 147 insertions(+), 123 deletions(-) diff --git a/docs/Examples/antd/Layout.md b/docs/Examples/antd/Layout.md index f60367b3517..0fff0803570 100644 --- a/docs/Examples/antd/Layout.md +++ b/docs/Examples/antd/Layout.md @@ -164,7 +164,7 @@ import Printer from '@uform/printer' import 'antd/dist/antd.css' const App = () => { - const [state, setState] = useState({ editable: true }) + const [state, setState] = useState({ editable: true }) return ( { ​​ - - ​ + ​ + 提交​ - - 重置​ + + 重置 @@ -241,6 +241,7 @@ import { FormPath, FormBlock, FormLayout, + FormTextBox, createFormActions } from '@uform/antd' import { Button } from 'antd' @@ -269,10 +270,8 @@ const App = () => ( ReactDOM.render(, document.getElementById('root')) ``` - ## 分步表单 - ```jsx import { SchemaForm, @@ -314,15 +313,14 @@ export default () => ( state.visible = false }) }) - FormStep.useEffects(['step-1', 'step-2', 'step-3']) }} > diff --git a/docs/Examples/next/Layout.md b/docs/Examples/next/Layout.md index 17f3c605a23..6a7aef7ccac 100644 --- a/docs/Examples/next/Layout.md +++ b/docs/Examples/next/Layout.md @@ -300,15 +300,14 @@ export default () => ( state.visible = false }) }) - FormStep.useEffects(['step-1', 'step-2', 'step-3']) }} > diff --git a/packages/antd/src/components/FormItemGrid.tsx b/packages/antd/src/components/FormItemGrid.tsx index 6f6e7fcd662..65888cfa91b 100644 --- a/packages/antd/src/components/FormItemGrid.tsx +++ b/packages/antd/src/components/FormItemGrid.tsx @@ -1,5 +1,5 @@ import React, { Fragment } from 'react' -import { CompatNextFormItem } from '../compat/FormItem' +import { CompatAntdFormItem } from '../compat/FormItem' import { createVirtualBox } from '@uform/react-schema-renderer' import { toArr } from '@uform/shared' import { Row, Col } from 'antd' @@ -58,9 +58,9 @@ export const FormItemGrid = createVirtualBox< if (title) { return ( - + {grids} - + ) } return {grids} @@ -73,9 +73,9 @@ export const FormGridRow = createVirtualBox( const grids = {props.children} if (title) { return ( - + {grids} - + ) } return grids diff --git a/packages/antd/src/components/FormStep.tsx b/packages/antd/src/components/FormStep.tsx index 0d8faa117b9..e7da9be9c79 100644 --- a/packages/antd/src/components/FormStep.tsx +++ b/packages/antd/src/components/FormStep.tsx @@ -2,12 +2,11 @@ import React, { useState, useMemo, useRef } from 'react' import { createControllerBox, ISchemaVirtualFieldComponentProps, - FormPathPattern, createEffectHook, - createFormActions + FormEffectHooks, + useFormEffects } from '@uform/react-schema-renderer' import { toArr } from '@uform/shared' -import { Observable } from 'rxjs/internal/Observable' import { Steps } from 'antd' import { IFormStep } from '../types' @@ -27,35 +26,15 @@ const EffectHooks = { }>(StateMap.ON_FORM_STEP_CURRENT_CHANGE) } -const useEffects = (relations: FormPathPattern[]) => { - const actions = createFormActions() - return EffectHooks.onStepCurrentChange$().subscribe(({ value }) => { - relations.forEach((pattern, index) => { - setTimeout(()=>{ - actions.setFieldState(pattern, (state: any) => { - state.display = index === value - }) - }) - }) - }) -} - -type StepComponentExtendsProps = StateMap & { - useEffects: ( - relations: FormPathPattern[] - ) => Observable<{ - value: number - preValue: number - }> -} +type StepComponentExtendsProps = StateMap export const FormStep: React.FC & StepComponentExtendsProps = createControllerBox( 'step', - ({ props, form }: ISchemaVirtualFieldComponentProps) => { + ({ form, schema }: ISchemaVirtualFieldComponentProps) => { const [current, setCurrent] = useState(0) const ref = useRef(current) - const { dataSource, ...stepProps } = props['x-component-props'] || {} + const { dataSource, ...stepProps } = schema.getExtendsComponentProps() const items = toArr(dataSource) const update = (cur: number) => { form.notify(StateMap.ON_FORM_STEP_CURRENT_CHANGE, { @@ -64,6 +43,26 @@ export const FormStep: React.FC & }) setCurrent(cur) } + useFormEffects(({ setFieldState }) => { + FormEffectHooks.onFormInit$().subscribe(() => { + items.forEach(({ name }, index) => { + setFieldState(name, (state: any) => { + state.display = index === current + }) + }) + }) + EffectHooks.onStepCurrentChange$().subscribe(({ value }) => { + items.forEach(({ name }, index) => { + if (!name) + throw new Error('FormStep dataSource must include `name` property') + setTimeout(() => { + setFieldState(name, (state: any) => { + state.display = index === value + }) + }) + }) + }) + }) useMemo(() => { update(ref.current) form.subscribe(({ type, payload }) => { @@ -102,6 +101,4 @@ export const FormStep: React.FC & } ) as any -Object.assign(FormStep, StateMap, EffectHooks, { - useEffects -}) +Object.assign(FormStep, StateMap, EffectHooks) diff --git a/packages/antd/src/components/FormTextBox.tsx b/packages/antd/src/components/FormTextBox.tsx index 07bc59a314a..771177680f0 100644 --- a/packages/antd/src/components/FormTextBox.tsx +++ b/packages/antd/src/components/FormTextBox.tsx @@ -2,7 +2,7 @@ import React, { useRef, useLayoutEffect } from 'react' import { createControllerBox } from '@uform/react-schema-renderer' import { IFormTextBox } from '../types' import { toArr } from '@uform/shared' -import { CompatNextFormItem } from '../compat/FormItem' +import { CompatAntdFormItem } from '../compat/FormItem' import styled from 'styled-components' export const FormTextBox = createControllerBox( @@ -96,14 +96,14 @@ export const FormTextBox = createControllerBox( if (!title) return textChildren return ( - {textChildren} - + ) })` display: flex; diff --git a/packages/core/src/__tests__/index.spec.ts b/packages/core/src/__tests__/index.spec.ts index 37b83d86b7c..3fe78924ffe 100644 --- a/packages/core/src/__tests__/index.spec.ts +++ b/packages/core/src/__tests__/index.spec.ts @@ -1430,7 +1430,7 @@ describe('validator', () => { values: {}, initialValues: {}, onChange: values => { - // console.log(values) + // console.log(values) } }) @@ -1470,7 +1470,7 @@ describe('validator', () => { values: {}, initialValues: {}, onChange: values => { - // console.log(values) + // console.log(values) } }) @@ -1509,7 +1509,7 @@ describe('validator', () => { values: {}, initialValues: {}, onChange: values => { - // console.log(values) + // console.log(values) } }) diff --git a/packages/core/src/state/field.ts b/packages/core/src/state/field.ts index dd5b3af6991..de542cd8fb3 100644 --- a/packages/core/src/state/field.ts +++ b/packages/core/src/state/field.ts @@ -1,5 +1,5 @@ import { createStateModel } from '../shared/model' -import { clone, toArr, isValid, isEqual, FormPath, isFn } from '@uform/shared' +import { toArr, isValid, isEqual, FormPath, isFn } from '@uform/shared' import { IFieldState, IFieldStateProps } from '../types' /** * 核心数据结构,描述表单字段的所有状态 @@ -82,44 +82,71 @@ export const FieldState = createStateModel( } } - readRules({ rules, required }: IFieldStateProps, prevState: IFieldState) { - let newRules = isValid(rules) ? clone(toArr(rules)) : this.state.rules - let newRequired = required !== undefined ? required : false - const currentRulesRequired = this.readRequired(newRules) - const prevRulesRequired = this.readRequired(prevState.rules) - if (prevState.required !== newRequired) { - if (!newRules.some(rule => rule && rule.required !== undefined)) { - newRules.push({ required: newRequired }) - } else { - newRules = newRules.reduce((buf: any[], item: any) => { - const keys = Object.keys(item || {}) - if (item.required !== undefined) { - if (item.message !== undefined) { - if (keys.length > 2) { - return { - ...item, - required: newRequired + syncRulesByRequired(rules: any[], required: boolean) { + if (isValid(required)) { + if (rules.length) { + if (!rules.some(rule => rule && isValid(rule.required))) { + rules.push({ required }) + } else { + rules = rules.reduce((buf: any[], item: any) => { + const keys = Object.keys(item || {}) + if (item.required !== undefined) { + if (item.message !== undefined) { + if (keys.length > 2) { + return buf.concat({ + ...item, + required + }) } } else { - return buf - } - } else { - if (keys.length > 1) { - return { - ...item, - required: newRequired + if (keys.length > 1) { + return buf.concat({ + ...item, + required + }) } - } else { - return buf } } - } - return buf.concat(item) - }, []) + if (isValid(item.required)) { + return buf.concat({ + ...item, + required + }) + } + return buf.concat(item) + }, []) + } + } else { + if (required === true) { + rules.push({ + required + }) + } } } - if (currentRulesRequired !== prevRulesRequired) { - newRequired = currentRulesRequired + return rules + } + + readRules({ rules, required }: IFieldStateProps, prevState: IFieldState) { + let newRules = isValid(rules) ? toArr(rules) : this.state.rules + let newRequired = isValid(required) ? required : false + const currentRuleRequired = this.readRequired(newRules) + const prevRuleRequired = this.readRequired(prevState.rules) + const ruleRequiredChanged = currentRuleRequired !== prevRuleRequired + const requiredChanged = !isEqual(required, prevState.required) + + if (ruleRequiredChanged && !requiredChanged) { + if (isValid(currentRuleRequired)) { + newRequired = currentRuleRequired + } + } else if (requiredChanged && !ruleRequiredChanged) { + newRules = this.syncRulesByRequired(newRules, newRequired) + } else if (ruleRequiredChanged && requiredChanged) { + if (isValid(currentRuleRequired)) { + newRequired = currentRuleRequired + } + } else { + newRules = this.syncRulesByRequired(newRules, newRequired) } return { rules: newRules, diff --git a/packages/next/src/components/FormStep.tsx b/packages/next/src/components/FormStep.tsx index 00d4b9a46de..739fc3d8135 100644 --- a/packages/next/src/components/FormStep.tsx +++ b/packages/next/src/components/FormStep.tsx @@ -2,12 +2,11 @@ import React, { useState, useMemo, useRef } from 'react' import { createControllerBox, ISchemaVirtualFieldComponentProps, - FormPathPattern, createEffectHook, - createFormActions + useFormEffects, + FormEffectHooks } from '@uform/react-schema-renderer' import { toArr } from '@uform/shared' -import { Observable } from 'rxjs/internal/Observable' import { Step } from '@alifd/next' import { IFormStep } from '../types' @@ -27,27 +26,7 @@ const EffectHooks = { }>(StateMap.ON_FORM_STEP_CURRENT_CHANGE) } -const useEffects = (relations: FormPathPattern[]) => { - const actions = createFormActions() - return EffectHooks.onStepCurrentChange$().subscribe(({ value }) => { - relations.forEach((pattern, index) => { - setTimeout(()=>{ - actions.setFieldState(pattern, (state: any) => { - state.display = index === value - }) - }) - }) - }) -} - -type StepComponentExtendsProps = StateMap & { - useEffects: ( - relations: FormPathPattern[] - ) => Observable<{ - value: number - preValue: number - }> -} +type StepComponentExtendsProps = StateMap export const FormStep: React.FC & StepComponentExtendsProps = createControllerBox( @@ -64,6 +43,26 @@ export const FormStep: React.FC & }) setCurrent(cur) } + useFormEffects(({ setFieldState }) => { + FormEffectHooks.onFormInit$().subscribe(() => { + items.forEach(({ name }, index) => { + setFieldState(name, (state: any) => { + state.display = index === current + }) + }) + }) + EffectHooks.onStepCurrentChange$().subscribe(({ value }) => { + items.forEach(({ name }, index) => { + if (!name) + throw new Error('FormStep dataSource must include `name` property') + setTimeout(() => { + setFieldState(name, (state: any) => { + state.display = index === value + }) + }) + }) + }) + }) useMemo(() => { update(ref.current) form.subscribe(({ type, payload }) => { @@ -102,6 +101,4 @@ export const FormStep: React.FC & } ) as any -Object.assign(FormStep, StateMap, EffectHooks, { - useEffects -}) +Object.assign(FormStep, StateMap, EffectHooks) diff --git a/packages/react-schema-renderer/README.zh-cn.md b/packages/react-schema-renderer/README.zh-cn.md index a15eda567b5..1e5d905f91b 100644 --- a/packages/react-schema-renderer/README.zh-cn.md +++ b/packages/react-schema-renderer/README.zh-cn.md @@ -510,7 +510,13 @@ registerFormField('array',({value,path,mutators})=>{ - 组件内部有复杂布局结构 -就这3点,基本上满足超复杂自定义组件的特征了,对于这种场景,为什么我们通过正常的封装自定义组件的形式不能解决问题呢?其实主要是受限于校验,没法整体校验,所以,我们需要一个能聚合大量字段处理逻辑的能力,那么,我们该如何解决呢? +就这3点,基本上满足超复杂自定义组件的特征了,对于这种场景,为什么我们通过正常的封装自定义组件的形式不能解决问题呢?其实主要是受限于校验,没法整体校验,所以,我们需要一个能聚合大量字段处理逻辑的能力,下面我们来看看具体方案: + +```tsx + +``` + + ### 协议 --- diff --git a/packages/react-schema-renderer/src/components/SchemaField.tsx b/packages/react-schema-renderer/src/components/SchemaField.tsx index 82a2c2fd7a9..49e1e0cdd25 100644 --- a/packages/react-schema-renderer/src/components/SchemaField.tsx +++ b/packages/react-schema-renderer/src/components/SchemaField.tsx @@ -1,6 +1,6 @@ import React, { useContext, Fragment } from 'react' import { Field, VirtualField, IFieldState } from '@uform/react' -import { FormPath, isFn, isStr, isEqual } from '@uform/shared' +import { FormPath, isFn, isStr, isEqual, isValid } from '@uform/shared' import { ISchemaFieldProps, ISchemaFieldComponentProps, @@ -18,13 +18,13 @@ const computeSchemaState = (draft: IFieldState, prevState: IFieldState) => { const prevRules = prevSchema.getExtendsRules() const currentEditable = schema.getExtendsEditable() const prevEditable = prevSchema.getExtendsEditable() - if (!isEqual(currentRequired, prevRequired)) { + if (isValid(currentRequired) && !isEqual(currentRequired, prevRequired)) { draft.required = currentRequired } - if (!isEqual(currentRules, prevRules)) { + if (isValid(currentRules) && !isEqual(currentRules, prevRules)) { draft.rules = currentRules } - if (!isEqual(currentEditable, prevEditable)) { + if (isValid(currentEditable) && !isEqual(currentEditable, prevEditable)) { draft.selfEditable = currentEditable } } From 70a5b67ac8e16f4dd87137ace8e5ce483b8b196e Mon Sep 17 00:00:00 2001 From: Harry Yu Date: Thu, 21 Nov 2019 14:55:30 +1300 Subject: [PATCH 48/99] docs: fix typo and improve docs (#405) --- packages/core/README.md | 1033 ++++++++++++--------------------- packages/core/README.zh-cn.md | 34 +- 2 files changed, 403 insertions(+), 664 deletions(-) diff --git a/packages/core/README.md b/packages/core/README.md index 12854e03244..545dc1e2277 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -2,12 +2,12 @@ English | [简体中文](./README.zh-cn.md) -> The form state core management package does not rely on any third-party UI frameworks. In this package, it mainly includes: +> The form state core management package does not rely on any third-party UI frameworks. This package will provide the following features: > > - Manage Form status -> - Management Field status +> - Manage Field status > - Manage the Validator status -> - Manage dependencies between Form, Field, and Vaidator +> - Manage dependencies between Form, Field, and Validator ### Install @@ -15,7 +15,6 @@ English | [简体中文](./README.zh-cn.md) npm install --save @uform/core ``` - ### Table Of Contents @@ -26,50 +25,44 @@ npm install --save @uform/core - [Architecture diagram](#architecture-diagram) - [Terminology explanation](#terminology-explanation) - [API](#api) - * [`createForm`](#createform) - * [`registerValidationFormats`](#registervalidationformats) - * [`registerValidationRules`](#registervalidationrules) - * [`registerValidationMTEngine`](#registervalidationmtengine) - * [`setValidationLanguage`](#setvalidationlanguage) - * [`setValidationLocale`](#setvalidationlocale) + - [`createForm`](#createform) + - [`registerValidationFormats`](#registervalidationformats) + - [`registerValidationRules`](#registervalidationrules) + - [`registerValidationMTEngine`](#registervalidationmtengine) + - [`setValidationLanguage`](#setvalidationlanguage) + - [`setValidationLocale`](#setvalidationlocale) - [Classes](#classes) - * [`new FormPath()`](#new-formpath) - * [`new FormLifeCyle()`](#new-formlifecyle) + - [`new FormPath()`](#new-formpath) + - [`new FormLifeCyle()`](#new-formlifecyle) - [Enums](#enums) - * [Lifecycletypes](#lifecycletypes) + - [Lifecycletypes](#lifecycletypes) - [Interfaces](#interfaces) - * [IFormCreatorOptions](#iformcreatoroptions) - * [IForm](#iform) - * [Imutators](#imutators) - * [Validation](#the-validator) - * [IFormState](#iformstate) - * [IFieldState](#ifieldstate) - * [IVirtualFieldState](#ivirtualfieldstate) - * [IField/IVirtualField](#ifieldivirtualfield) + - [IFormCreatorOptions](#iformcreatoroptions) + - [IForm](#iform) + - [IMutators](#imutators) + - [Validation](#the-validator) + - [IFormState](#iformstate) + - [IFieldState](#ifieldstate) + - [IVirtualFieldState](#ivirtualfieldstate) + - [IField/IVirtualField](#ifieldivirtualfield) -### Backdrop - -In the middle and back-end field, the core is two scenarios, **one is data entry, one is data query + data presentation**, whether it is data entry or data query, it is realized by means of form, from the perspective of implementation complexity, The complexity of the two is similar, because the data rendering level will inevitably have extremely complex renderings (such as Tree Table, etc.), but the data rendering is the easiest to reuse and abstract, only the form, will involve a lot of interactive logic So, as long as we solve the form problem fundamentally, for the mid- and back-stage scenes, most of the mid- and back-stage scene problems are solved. UForm is born for this purpose. - - - -### Design Concept - +### Background +There are two main scenarios in the middle and back-end field, **one is data entry, one is data query + data presentation**, whether it is data entry or data query, it is realized by means of form, from the perspective of implementation complexity, the complexity of them is similar, because the data rendering level will inevitably have extremely complex renderings (such as Tree Table, etc.), but the data rendering is easier to reuse and abstract, only the Form requirements will involve a lot of interactive logic. So, as long as we solve the Form problem fundamentally, for the mid- and back-stage scenes, most of the mid- and back-stage scene problems are solved. -**Anything comes from Observable Grpah.** +UForm is born for this purpose. +### Design +**Anything comes from Observable Graph.** -### Core highlights +### Core highlights - - -- Time travel, with the help of the first Observable Graph, can record the full state at any time, can also roll back the state to any time, such ability in, retransaction applications and local debugging can maximize their value. +- Time travel, with the help of the Observable Graph, can record the full state at any time, can also roll back the state to any time, such abilities will maximize the performance in heavy transaction applications and local debugging scenarios. - Efficient update, accurate rendering, no full tree rendering required -- Built-in immer. js, intelligent degradation, no need to care about browser compatibility +- Built-in immer.js, intelligent degradation, no need to care about browser compatibility - More complete life cycle hook - More complete verification engine @@ -85,84 +78,52 @@ In the middle and back-end field, the core is two scenarios, **one is data entry - Provides state management capabilities beyond the basic form state model. - - -### Architecture diagram - - +### Architecture diagram ![img](https://img.alicdn.com/tfs/TB18LXHlVP7gK0jSZFjXXc5aXXa-1428-926.png) - - -### Terminology explanation - - +### Terminology explanation **FormPath/FormPathPattern** Is an abstract data path form, FormPath is a path class, and FormPathPattern is a path form that can be parsed by FormPath. [Cool-path](https://github.com/janrywang/cool-path) Path parsing matching, ability to evaluate values - - -**The virtual field** Is a special Field data structure. The difference between the Field and the Field is that it does not manage values. That is to say, it has no relevance to the value of the Form. Usually we use it, more importantly, it acts as a proxy for the status of a UI container. For example, the layout component FormBlock in UForm exists as an independent node in the whole Form Grpah, however, this node type is a VirtualField, but when the final data is submitted, the FormBlock does not pollute the data structure of the submitted data. - - +**The virtual field** Is a special Field data structure. The difference between the Field and the Field is that it does not manage values. That is to say, it has no relevance to the value of the Form. Usually we use it, more importantly, it acts as a proxy for the status of a UI container. For example, the layout component FormBlock in UForm exists as an independent node in the whole Form Graph, however, this node type is a VirtualField, but when the final data is submitted, the FormBlock does not pollute the data structure of the submitted data. **Observable Graph** Form is a unique Observer Tree. With the help of the observer tree, many forms-related internal linkage logic can be implemented. - - **Data Path** Is the name attribute of Field/VirtualField, which exists as the data path. - - -**Node Path** Is the path attribute of Field/VirtualFIeld, which exists as the node path. - - +**Node Path** Is the path attribute of Field/VirtualField, which exists as the node path. For the data path and node path, we can look at the following figure: - - ![img](https://img.alicdn.com/tfs/TB1.rAamG61gK0jSZFlXXXDKFXa-1496-898.png) - - If there exists such a tree, then: - - - The name attribute of field c is a.c, and the path attribute is a.b.c. - The name attribute of field B is a.b, and the path attribute is a.b. - The name attribute of field d is a.d, and the path attribute is a.d. - The name attribute of field e is a.d.e, and the path attribute is a.d.e. - - After this explanation, we roughly understand that as long as VirtualField exists in a node path, its data path will skip VirtualField. However, for VirtualField itself, its name attribute contains its own node identification, which is why the name attribute of field B is a.b. - - -### API +### API #### `createForm` > Create a Form instance - - **Signature** ``` createForm(options?: IFormCreatorOptions): IForm ``` - - **Usage** ``` import { createForm } from '@uform/core' - + const form = createForm({ values:{}, initialValues:{}, @@ -181,16 +142,10 @@ aa.setState(state=>{ console.log(form.getFormState(state=>state.values)) //{aa:123} ``` - - #### `registerValidationFormats` - - > Register a regular verification rule set - - **Signature** ``` @@ -199,17 +154,15 @@ registerValidationFormats(formats:{ }) : void ``` - - **Usage** ``` import { createForm,registerValidationFormats } from '@uform/core' - + registerValidationFormats({ number: /^[+-]?\d+(\.\d+)?$/ }) - + const form = createForm({ values:{}, initialValues:{}, @@ -231,25 +184,19 @@ aa.setState(state=>{ }) form.validate() -console.log(form.getFormState(state=>state.errors)) +console.log(form.getFormState(state=>state.errors)) /** -[{ +[{ path: 'aa', - messages: [ 'This field is not a number.' ] + messages: [ 'This field is not a number.' ] }] **/ ``` - - #### `registerValidationRules` - - > The difference between registering a verification rule set and registering formats is that it can register complex verification rules, but the formats are just regular expressions. - - **Signature** ``` @@ -260,19 +207,17 @@ registerValidationRules( ) : void ``` - - **Usage** ``` import { createForm,registerValidationRules } from '@uform/core' - + registerValidationRules({ custom: value => { return value === '123' ? 'This field can not be 123' : '' } }) - + const form = createForm({ values: {}, initialValues: {}, @@ -304,35 +249,27 @@ console.log(form.getFormState(state =>state.errors)) **/ ``` - - #### `registerValidationMTEngine` - - > Register a verification message template engine - - **Signature** ``` registerValidationMTEngine(callback:(message,context)=>any) : void ``` - - **Usage** ``` import { createForm,registerValidationMTEngine } from '@uform/core' - + registerValidationMTEngine((message,context)=>{ return message.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, $0) => { return FormPath.getIn(context, $0) }) }) - + const form = createForm({ values: {}, initialValues: {}, @@ -369,24 +306,16 @@ console.log(form.getFormState(state =>state.errors)) **/ ``` - - #### `setValidationLanguage` - - > Set the international language type - - **Signature** ``` setValidationLanguage(lang: string): void ``` - - **Usage** ``` @@ -395,16 +324,10 @@ import { setValidationLanguage } from '@uform/core' setValidationLanguage('en-US') ``` - - #### `setValidationLocale` - - > Set a language pack - - **Signature** ``` @@ -417,8 +340,6 @@ interface ILocales { setValidationLocale(locale: ILocales) => void ``` - - **Usage** ``` @@ -431,49 +352,33 @@ setValidationLocale({ }) ``` - - -### Classes - - +### Classes #### `new FormPath()` - - > The form path engine is responsible for path analysis, matching, evaluation, value, deconstruction evaluation, and deconstruction value. - - For more information, see: [ https://github.com/janrywang/cool-path ](https://github.com/janrywang/cool-path) +#### `new FormLifeCycle()` - -#### `new FormLifeCyle()` - - - -> Create a lifecycle listener - - +> Create a life cycle listener **Signature** ``` type FormLifeCycleHandler = (payload: T, context: any) => void -new FormLifeCyle(handler: FormLifeCycleHandler) -new FormLifeCyle(...type: LifeCycleTypes, handler: FormLifeCycleHandler...) -new FormLifeCyle(handlerMap: { [key: LifeCycleTypes]: FormLifeCycleHandler }) +new FormLifeCycle(handler: FormLifeCycleHandler) +new FormLifeCycle(...type: LifeCycleTypes, handler: FormLifeCycleHandler...) +new FormLifeCycle(handlerMap: { [key: LifeCycleTypes]: FormLifeCycleHandler }) ``` - - **Usage** ```typescript import { createForm,FormLifeCycle,LifeCycleTypes } from '@uform/core' - + const form = createForm({ lifecycles:[ new FormLifeCycle(({type:LifeCycleTypes,payload:IForm | IField | IVirtualField })=>{ @@ -493,351 +398,269 @@ new FormLifeCyle(handlerMap: { [key: LifeCycleTypes]: FormLifeCycleHandler CreateForm parameter object protocol - - ```typescript interface IFormCreatorOptions { -   // Form initial value -   initialValues?: {} - -   // Form value -   values?: {} - -   // Lifecycle listener, here mainly introduced to the instantiated object of FormLifeCycle -   lifecycles?: FormLifeCycle[] - -   // Is it editable, overall control in the Form dimension -   editable?: boolean | ((name: string) => boolean) - -   // Whether to use the dirty check, the default will go immer accurate update -   useDirty?: boolean - -   // Whether to go pessimistic check, stop the subsequent check when the first check fails -   validateFirst?: boolean - -   // Form change event callback -   onChange?: (values: IFormState['values']) => void - -   // Form submission event callback -   onSubmit?: (values: IFormState['values']) => any | Promise - -   // Form reset event callback -   onReset?: () => void - -   // Form verification failure event callback -   onValidateFailed?: (validated: IFormValidateResult) => void -} + // Form initial value + initialValues?: {} // Form value -``` + values?: {} // LifeCycle listener, here mainly introduced to the instantiated object of FormLifeCycle + lifecycles?: FormLifeCycle[] // Is it editable, overall control in the Form dimension + editable?: boolean | ((name: string) => boolean) // Whether to use the dirty check, the default will go immer accurate update -#### IForm + useDirty?: boolean // Whether to go pessimistic check, stop the subsequent check when the first check fails + validateFirst?: boolean // Form change event callback + onChange?: (values: IFormState['values']) => void // Form submission event callback -> Form instance object API created by using createForm - + onSubmit?: (values: IFormState['values']) => any | Promise // Form reset event callback + onReset?: () => void // Form verification failure event callback -```typescript -interface IForm { -  /* -   * Form submission, if the callback parameter returns Promise, -   * Then the entire submission process will hold and load is true. -   * Wait for Promise resolve to trigger the form onFormSubmitEnd event while loading is false -   */ -   submit( -      onSubmit?: (values: IFormState['values']) => any | Promise -    ): Promise<{ -       Validated: IFormValidateResult -       Payload: any //onSubmit callback function return value -   }> -    -   /* -    * Clear the error message, you can pass the FormPathPattern to batch or precise control of the field to be cleared. -    * For example, clearErrors("*(aa,bb,cc)") -    */ -   clearErrors: (pattern?: FormPathPattern) => void -    -   /* -    * Get status changes, mainly used to determine which states in the current life cycle have changed in the form lifecycle hook. -    * For example, hasChanged(state,'value.aa') -    */ -   hasChanged(target: IFormState | IFieldState | IVirtualFieldState, path: FormPathPattern): boolean -    -   /* -    * Reset form -    */ -   reset(options?: { -     // Forced to empty -     forceClear?: boolean -     // Forced check -     validate?: boolean -     // Reset range for batch or precise control of the field to be reset -     selector?: FormPathPattern -   }): Promise -    -   /* -    * Validation form -    */ -   validate(path?: FormPathPattern, options?: { -     // Is it pessimistic check, if the current field encounters the first verification error, stop the subsequent verification process -     first?:boolean -   }): Promise -    -   /* -    * Set the form status -    */ -   setFormState( -     // Operation callback -     callback?: (state: IFormState) => any, -     // No trigger the event -     silent?: boolean -   ): void -    -   /* -    * Get form status -    */ -   getFormState( -     //transformer -     callback?: (state: IFormState) => any -   ): any -    -   /* -    * Set the field status -    */ -   setFieldState( -     // Field path -     path: FormPathPattern, -     // Operation callback -     callback?: (state: IFieldState) => void, -     // No trigger the event -     silent?: boolean -   ): void -    -   /* -    * Get the field status -    */ -   getFieldState( -     // Field path -     path: FormPathPattern, -     // Transformer -     callback?: (state: IFieldState) => any -   ): any -    -   /* -    * Registration field -    */ -   registerField(props: { -    // Node path -    path?: FormPathPattern -    // Data path -    name?: string -    // Field value -    value?: any -    // Field multi-value -    values?: any[] -    // Field initial value -    initialValue?: any -    // Field extension properties -    props?: any -    // Field check rule -    rules?: ValidatePatternRules[] -    // Field is required -    required?: boolean -    // Is the field editable? -    editable?: boolean -    // Whether the field is dirty check -    useDirty?: boolean -    // Field state calculation container, mainly used to extend the core linkage rules -    computeState?: (draft: IFieldState, prevState: IFieldState) => void -  }): IField -   -  /* -   * Register virtual fields -   */ -  registerVirtualField(props: { -    // Node path -    path?: FormPathPattern -    // Data path -    name?: string -    // Field extension properties -    props?: any -    // Whether the field is dirty check -    useDirty?: boolean -    // Field state calculation container, mainly used to extend the core linkage rules -    computeState?: (draft: IFieldState, prevState: IFieldState) => void -  }): IVirtualField -   -  /* -   * Create a field data operator, which will explain the returned API in detail later. -   */ -  createMutators(field: IField): IMutators -   -  /* -   * Get the form observer tree -   */ -  getFormGraph(): IFormGraph -   -  /* -   * Set the form observer tree -   */ -  setFormGraph(graph: IFormGraph): void -   -  /* -   * Listen to the form life cycle -   */ -  subscribe(callback?: ({ -    type, -    payload -  }: { -    type: string -    payload: any -  }) => void): number -   -  /* -   * Cancel the listening form life cycle -   */ -  unsubscribe(id: number): void -   -  /* -   * Trigger form custom life cycle -   */ -  notify: (type: string, payload?: T) => void -   -  /* -   * Set the field value -   */ -  setFieldValue(path?: FormPathPattern, value?: any): void -   -  /* -   * Get the field value -   */ -  getFieldValue(path?: FormPathPattern): any -   -  /* -   * Set the initial value of the field -   */ -  setFieldInitialValue(path?: FormPathPattern, value?: any): void -   -  /* -   * Get the initial value of the field -   */ -  getFieldInitialValue(path?: FormPathPattern): any + onValidateFailed?: (validated: IFormValidateResult) => void } ``` +#### IForm +> Form instance object API created by using createForm -#### Imutators - +```typescript +interface IForm { + /* + * Form submission, if the callback parameter returns Promise, + * Then the entire submission process will hold and load is true. + * Wait for Promise resolve to trigger the form onFormSubmitEnd event while loading is false + */ + submit( + onSubmit?: (values: IFormState['values']) => any | Promise + ): Promise<{ + Validated: IFormValidateResult + Payload: any //onSubmit callback function return value + }> + /* + * Clear the error message, you can pass the FormPathPattern to batch or precise control of the field to be cleared. + * For example, clearErrors("*(aa,bb,cc)") + */ + clearErrors: (pattern?: FormPathPattern) => void + /* + * Get status changes, mainly used to determine which states in the current life cycle have changed in the form lifecycle hook. + * For example, hasChanged(state,'value.aa') + */ + hasChanged( + target: IFormState | IFieldState | IVirtualFieldState, + path: FormPathPattern + ): boolean + /* + * Reset form + */ + reset(options?: { + // Forced to empty + forceClear?: boolean // Forced check + validate?: boolean // Reset range for batch or precise control of the field to be reset + selector?: FormPathPattern + }): Promise + /* + * Validation form + */ + validate( + path?: FormPathPattern, + options?: { + // Is it pessimistic check, if the current field encounters the first verification error, stop the subsequent verification process + first?: boolean + } + ): Promise + /* + * Set the form status + */ + setFormState( // Operation callback + callback?: (state: IFormState) => any, // No trigger the event + silent?: boolean + ): void + /* + * Get form status + */ + getFormState( //transformer + callback?: (state: IFormState) => any + ): any + /* + * Set the field status + */ + setFieldState( // Field path + path: FormPathPattern, // Operation callback + callback?: (state: IFieldState) => void, // No trigger the event + silent?: boolean + ): void + /* + * Get the field status + */ + getFieldState( // Field path + path: FormPathPattern, // Transformer + callback?: (state: IFieldState) => any + ): any + /* + * Registration field + */ + registerField(props: { + // Node path + path?: FormPathPattern // Data path + name?: string // Field value + value?: any // Field multi-value + values?: any[] // Field initial value + initialValue?: any // Field extension properties + props?: any // Field check rule + rules?: ValidatePatternRules[] // Field is required + required?: boolean // Is the field editable? + editable?: boolean // Whether the field is dirty check + useDirty?: boolean // Field state calculation container, mainly used to extend the core linkage rules + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IField + /* + * Register virtual fields + */ + registerVirtualField(props: { + // Node path + path?: FormPathPattern // Data path + name?: string // Field extension properties + props?: any // Whether the field is dirty check + useDirty?: boolean // Field state calculation container, mainly used to extend the core linkage rules + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IVirtualField + /* + * Create a field data operator, which will explain the returned API in detail later. + */ + createMutators(field: IField): IMutators + /* + * Get the form observer tree + */ + getFormGraph(): IFormGraph + /* + * Set the form observer tree + */ + setFormGraph(graph: IFormGraph): void + /* + * Listen to the form life cycle + */ + subscribe( + callback?: ({ type, payload }: { type: string; payload: any }) => void + ): number + /* + * Cancel the listening form life cycle + */ + unsubscribe(id: number): void + /* + * Trigger form custom life cycle + */ + notify: (type: string, payload?: T) => void + /* + * Set the field value + */ + setFieldValue(path?: FormPathPattern, value?: any): void + /* + * Get the field value + */ + getFieldValue(path?: FormPathPattern): any + /* + * Set the initial value of the field + */ + setFieldInitialValue(path?: FormPathPattern, value?: any): void + /* + * Get the initial value of the field + */ + getFieldInitialValue(path?: FormPathPattern): any +} +``` +#### IMutators > The instance API created by crewikiutators is mainly used to operate field data. - - ```typescript interface IMutators { // Changing the field value and multi parameter condition will store all parameters in values @@ -845,18 +668,18 @@ interface IMutators { // Get focus, trigger active state change focus(): void // Lose focus, trigger active / visited status change - blur (): void + blur(): void // Trigger current field verifier validate(): Promise // Whether the value of the current field exists in the values property of form - exist (index?: number | string): Boolean - + exist(index?: number | string): Boolean + /**Array operation method**/ - + // Append data push(value?: any): any[] // Pop up tail data - pop (): any[] + pop(): any[] // Insert data insert(index: number, value: any): any[] // Delete data @@ -874,37 +697,35 @@ interface IMutators { } ``` - - -#### Validation - - +#### Validation > Here we mainly list the intermediate type signatures related to verification. - - ```typescript - type CustomValidator = ( - value: any, - rescription?: ValidateDescription - ) => ValidateResponse - type SyncValidateResponse = null | string | boolean | { - type?: 'error' | 'warning' - message: string - }; - type AsyncValidateResponse = Promise - type ValidateResponse = SyncValidateResponse | AsyncValidateResponse; - - interface IFormValidateResult { - errors: Array<{ - path: string; - messages: string[] - }> - warnings: Array<{ - path: string - messages: string[] - }> +type CustomValidator = ( + value: any, + rescription?: ValidateDescription +) => ValidateResponse +type SyncValidateResponse = + | null + | string + | boolean + | { + type?: 'error' | 'warning' + message: string + } +type AsyncValidateResponse = Promise +type ValidateResponse = SyncValidateResponse | AsyncValidateResponse + +interface IFormValidateResult { + errors: Array<{ + path: string + messages: string[] + }> + warnings: Array<{ + path: string + messages: string[] + }> } type InternalFormats = @@ -923,212 +744,126 @@ type InternalFormats = | string interface ValidateDescription { -     // Regular rule type -     format?: InternalFormats -     // Custom validator -     validator?: CustomValidator -     // Is it required? -     required?: boolean -     // Customize with regularity -     pattern?: RegExp | string -     // Maximum length rule -     max?: number; -     // Maximum numerical rule -     maximum?: number -     // Exclusive maximum numerical rule -     exclusiveMaximum?: number -     // Exclusive minimum numerical rules -     exclusiveMinimum?: number -     // Minimum value rule -     minimum?: number -     // Minimum length rule -     min?: number -     // Length rule -     len?: number -     // Whether to check the white space -     whitespace?: boolean -     // Enumeration check rules -     enum?: any[] -     // Custom error copy -     message?: string -     // Custom validation rules -     [key: string]: any + // Regular rule type + format?: InternalFormats + // Custom validator + validator?: CustomValidator + // Is it required? + required?: boolean + // Customize with regularity + pattern?: RegExp | string + // Maximum length rule + max?: number; + // Maximum numerical rule + maximum?: number + // Exclusive maximum numerical rule + exclusiveMaximum?: number + // Exclusive minimum numerical rules + exclusiveMinimum?: number + // Minimum value rule + minimum?: number + // Minimum length rule + min?: number + // Length rule + len?: number + // Whether to check the white space + whitespace?: boolean + // Enumeration check rules + enum?: any[] + // Custom error copy + message?: string + // Custom validation rules + [key: string]: any } ``` - - -#### IFormState - - +#### IFormState > Form the core state - - ```typescript interface IFormState { -    -   /**Read-only attribute**/ -    -  // Is it in the original state, pristine is true only when values === initialValues -  pristine: boolean -  // Is it legal, as long as the error length is greater than 0, the valid is false -  valid: boolean -  // Is it illegal, as long as the error length is greater than 0, the valid is true -  invalid: boolean -  // Is it in the check state, it will only be set when calling the validate API -  validating: boolean -  // Is it in the commit state, it will only be set when the submit API is called -  submitting: boolean -  //Error message list -  errors: string[] -  //Alarm message list -  warnings: string[] -   -  /** writable property**/ -   -  // Is it in the loaded state, writable state, as long as validating is true, the state will also be true, the same as false -  loading: boolean -  // Is it in the initial state? -  initialized: boolean -  // Is it editable? -  editable: boolean | ((name: string) => boolean) -  // form value -  values: {} -  // form initial value -  initialValues: {} -  // form mount, the life cycle hook mentioned earlier, must be triggered by setting the state, the default will not trigger -  mounted: boolean -  // Form unmount, the life cycle hook mentioned earlier, must be triggered by setting the state, the default will not trigger -  unmounted: boolean -  // Form extension properties -  props: FormProps + /**Read-only attribute**/ + // Is it in the original state, pristine is true only when values === initialValues + pristine: boolean // Is it legal, as long as the error length is greater than 0, the valid is false + valid: boolean // Is it illegal, as long as the error length is greater than 0, the valid is true + invalid: boolean // Is it in the check state, it will only be set when calling the validate API + validating: boolean // Is it in the commit state, it will only be set when the submit API is called + submitting: boolean //Error message list + errors: string[] //Alarm message list + warnings: string[] /** writable property**/ // Is it in the loaded state, writable state, as long as validating is true, the state will also be true, the same as false + loading: boolean // Is it in the initial state? + initialized: boolean // Is it editable? + editable: boolean | ((name: string) => boolean) // form value + values: {} // form initial value + initialValues: {} // form mount, the life cycle hook mentioned earlier, must be triggered by setting the state, the default will not trigger + mounted: boolean // Form unmount, the life cycle hook mentioned earlier, must be triggered by setting the state, the default will not trigger + unmounted: boolean // Form extension properties + props: FormProps } ``` - - -#### IFieldState - - +#### IFieldState > CORE Field status - - ```typescript interface IFieldState { -    -  /**Read-only attribute**/ -    -  // State name, FieldState -  displayName?: string -  // Data path -  name: string -  // Node path -  path: string -  // Has been initialized -  initialized: boolean -  // Is it in the original state, the state is true only when value===intialValues -  pristine: boolean -  // Is it in a legal state, as long as the error length is greater than 0, the valid is false -  valid: boolean -  // Is it illegal, as long as the error length is greater than 0, the valid is true -  invalid: boolean -  // Is it in check state? -  validating: boolean -  // Is it modified, if the value changes, the property is true, and will be true throughout the life of the field -  modified: boolean -  // Is it touched? -  touched: boolean -  // Is it activated, when the field triggers the onFocus event, it will be triggered to true, when onBlur is triggered, it is false -  active: boolean -  // Have you ever visited, when the field triggers the onBlur event, it will be triggered to true -  visited: boolean -   -  /** writable property**/ -   -  // Is it visible, note: if the state is false, then the value of the field will not be submitted, and the UI will not display -  visible: boolean -  // Whether to show, note: if the state is false, then the value of the field will be submitted, the UI will not display, similar to the form hidden field -  display: boolean -  // Is it editable? -  editable: boolean -  // Is it in the loading state, note: if the field is in asynchronous verification, loading is true -  loading: boolean -  // Field multi-parameter value, such as when the field onChange trigger, the event callback passed multi-parameter data, then the value of all parameters will be stored here -  values: any[] -  // Field error message -  errors: string[] -  // Field alert message -  warnings: string[] -  // Field value, is equal to values[0] -  value: any -  // Initial value -  initialValue: any -  // Check the rules, the specific type description refers to the following documents -  rules: ValidatePatternRules[] -  // Is it required? -  required: boolean -  // Whether to mount -  mounted: boolean -  // Whether to uninstall -  unmounted: boolean -  // field extension properties -  props: FieldProps + /**Read-only attribute**/ + // State name, FieldState + displayName?: string // Data path + name: string // Node path + path: string // Has been initialized + initialized: boolean // Is it in the original state, the state is true only when value===initialValues + pristine: boolean // Is it in a legal state, as long as the error length is greater than 0, the valid is false + valid: boolean // Is it illegal, as long as the error length is greater than 0, the valid is true + invalid: boolean // Is it in check state? + validating: boolean // Is it modified, if the value changes, the property is true, and will be true throughout the life of the field + modified: boolean // Is it touched? + touched: boolean // Is it activated, when the field triggers the onFocus event, it will be triggered to true, when onBlur is triggered, it is false + active: boolean // Have you ever visited, when the field triggers the onBlur event, it will be triggered to true + visited: boolean /** writable property**/ // Is it visible, note: if the state is false, then the value of the field will not be submitted, and the UI will not display + visible: boolean // Whether to show, note: if the state is false, then the value of the field will be submitted, the UI will not display, similar to the form hidden field + display: boolean // Is it editable? + editable: boolean // Is it in the loading state, note: if the field is in asynchronous verification, loading is true + loading: boolean // Field multi-parameter value, such as when the field onChange trigger, the event callback passed multi-parameter data, then the value of all parameters will be stored here + values: any[] // Field error message + errors: string[] // Field alert message + warnings: string[] // Field value, is equal to values[0] + value: any // Initial value + initialValue: any // Check the rules, the specific type description refers to the following documents + rules: ValidatePatternRules[] // Is it required? + required: boolean // Whether to mount + mounted: boolean // Whether to uninstall + unmounted: boolean // field extension properties + props: FieldProps } ``` - - -#### IVirtualFieldState - - +#### IVirtualFieldState > Virtual Field core status - - ```typescript interface IVirtualFieldState { -    -   /**Read-only status**/ -    -   // State name, VirtualFieldState -   displayName: string -   // Field data path -   name: string -   // Field node path -   path: string -   // Has been initialized -   initialized: boolean -   -   /** writable status**/ -   -   // Is it visible, note: if the state is false, the UI will not be displayed, the data will not be submitted (because it is a VirtualField) -   visible: boolean -   // Whether to show, note: if the state is false, the UI will not display, the data will not be submitted (because it is VirtualField) -   display: boolean -   // Is it mounted? -   mounted: boolean -   // Has been uninstalled -   unmounted: boolean -   // field extension properties -   props: FieldProps + /**Read-only status**/ + // State name, VirtualFieldState + displayName: string // Field data path + name: string // Field node path + path: string // Has been initialized + initialized: boolean /** writable status**/ // Is it visible, note: if the state is false, the UI will not be displayed, the data will not be submitted (because it is a VirtualField) + visible: boolean // Whether to show, note: if the state is false, the UI will not display, the data will not be submitted (because it is VirtualField) + display: boolean // Is it mounted? + mounted: boolean // Has been uninstalled + unmounted: boolean // field extension properties + props: FieldProps } ``` - - -#### IField/IVirtualField - - +#### IField/IVirtualField > The instance API created by using registerField/registerVirtualField - - ```typescript interface IField/IVirtualField {    // Batch update container @@ -1151,4 +886,4 @@ interface IField/IVirtualField {    // Get state dirty information    getDirtyInfo: () => StateDirtyMap } -``` \ No newline at end of file +``` diff --git a/packages/core/README.zh-cn.md b/packages/core/README.zh-cn.md index 2dccf58ef18..d5f118c85c8 100644 --- a/packages/core/README.zh-cn.md +++ b/packages/core/README.zh-cn.md @@ -37,7 +37,7 @@ npm install --save @uform/core - [Interfaces](#interfaces) - [IFormCreatorOptions](#iformcreatoroptions) - [IForm](#iform) - - [Imutators](#imutators) + - [IMutators](#imutators) - [Validator](#validator) - [IFormState](#iformstate) - [IFieldState](#ifieldstate) @@ -48,6 +48,7 @@ npm install --save @uform/core ### 背景 + 中后台领域,核心就是两种场景,**一个是数据录入,一个是数据查询+数据展现**,不管 是数据录入还是数据查询,都是借助表单来实现的,从实现复杂度来看,两者复杂度相差不 多,因为数据呈现层面难免会有极度复杂的呈现形式(比如 Tree Table 等等),但是,数据 @@ -61,6 +62,7 @@ npm install --save @uform/core ### 核心亮点 + - 时间旅行,借助首创 Observable Graph,可以记录任意时刻的全量状态,也可以将状态 回滚至任意时刻,这样的能力在,重事务型应用与本地调试上可以发挥出最大价值 - 高效更新,精确渲染,无需整树渲染 @@ -82,6 +84,7 @@ npm install --save @uform/core ### 术语解释 + **FormPath/FormPathPattern** 是一个抽象数据路径形式,FormPath 是路径类 ,FormPathPattern 是可以被 FormPath 解析的路径形式,在这里主要使用了 [cool-path](https://github.com/janrywang/cool-path) 路径解析匹配,求值取值能力 @@ -110,6 +113,7 @@ Graph 中作为一个独立节点而存在,但是这个节点类型就是一 - d 字段的 name 属性是 a.d,path 属性是 a.d - e 字段的 name 属性是 a.d.e,path 属性是 a.d.e + 这一来解释之后,我们就大概明白了,只要在某个节点路径中,存在 VirtualField,那么 它的数据路径就会略过 VirtualField,但是,对于 VirtualField 自身这个节点,它的 name 属性,是包含它自身的节点标识的,这就是为什么 b 字段的 name 属性是 a.b 的原 @@ -139,7 +143,7 @@ const form = createForm({ initialValues: {}, onChange: values => { console.log(values) - } + }, }) const aa = form.registerField({ @@ -170,7 +174,7 @@ registerValidationFormats(formats:{ import { createForm, registerValidationFormats } from '@uform/core' registerValidationFormats({ - number: /^[+-]?\d+(\.\d+)?$/ + number: /^[+-]?\d+(\.\d+)?$/, }) const form = createForm({ @@ -178,7 +182,7 @@ const form = createForm({ initialValues: {}, onChange: values => { console.log(values) - } + }, }) const aa = form.registerField({ @@ -198,9 +202,9 @@ form.validate() console.log(form.getFormState(state => state.errors)) /** -[{ +[{ path: 'aa', - messages: [ 'This field is not a number.' ] + messages: [ 'This field is not a number.' ] }] **/ ``` @@ -228,7 +232,7 @@ import { createForm, registerValidationRules } from '@uform/core' registerValidationRules({ custom: value => { return value === '123' ? 'This field can not be 123' : '' - } + }, }) const form = createForm({ @@ -236,16 +240,16 @@ const form = createForm({ initialValues: {}, onChange: values => { console.log(values) - } + }, }) const aa = form.registerField({ path: 'aa', rules: [ { - custom: true - } - ] + custom: true, + }, + ], }) aa.setState(state => { @@ -382,9 +386,9 @@ setValidationLocale({ ```typescript type FormLifeCycleHandler = (payload: T, context: any) => void -new FormLifeCyle(handler: FormLifeCycleHandler) -new FormLifeCyle(...type: LifeCycleTypes, handler: FormLifeCycleHandler...) -new FormLifeCyle(handlerMap: { [key: LifeCycleTypes]: FormLifeCycleHandler }) +new FormLifeCycle(handler: FormLifeCycleHandler) +new FormLifeCycle(...type: LifeCycleTypes, handler: FormLifeCycleHandler...) +new FormLifeCycle(handlerMap: { [key: LifeCycleTypes]: FormLifeCycleHandler }) ``` **用法** @@ -684,7 +688,7 @@ interface IForm { } ``` -#### Imutators +#### IMutators > 通过 createMutators 创建出来的实例 API,主要用于操作字段数据 From 7fe8a7848d45198d90b3818be263c4d848ba0f3b Mon Sep 17 00:00:00 2001 From: quirkyvar Date: Thu, 21 Nov 2019 16:42:24 +0800 Subject: [PATCH 49/99] docs(@uform/react): improve docs (#406) --- packages/react/README.md | 1184 ++++++++++++++++++++- packages/react/README.zh-cn.md | 1345 ++++++++++++++++++++++-- packages/react/src/hooks/useFormSpy.ts | 4 +- packages/react/src/types.ts | 6 + 4 files changed, 2421 insertions(+), 118 deletions(-) diff --git a/packages/react/README.md b/packages/react/README.md index 2d10c6ccd95..072157e1c97 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -1,5 +1,7 @@ # @uform/react +English | [简体中文](./README.zh-cn.md) + > @uform/react is based on `react` and @uform/core is already built in. It provide API to manuplate form state and components for rendering support. > it mainly includes: > @@ -30,25 +32,44 @@ npm install --save @uform/react - [`Object Field`](#Object-Field) - [`ArrayField`](#ArrayField) - [`ArrayField`](#ArrayField) + - [`display and visible`](#display-visible) + - [`Linkage`](#Linkage) + - [`Async Linkage`](#Async-Linkage) + - [`Linkage Validation`](#Linkage-Validation) + - [`Complex Linkage`](#Complex-Linkage) - [`Combo`](#Combo) - [`Provide and FormSpy`](#Provide-and-FormSpy) + - [`Deconstruction`](#Deconstruction) + - [`Complex Deconstruction`](#Complex-Deconstruction) - [Components](#components) - - [`Form`](#Form) - - [`Field`](#Field) - - [`VirtualField`](#VirtualField) - - [`FormSpy`](#FormSpy) - - [`FormProvider`](#FormProvider) - - [`FormConsumer(deprecated,pls using FormSpy)`](<#FormConsumer(deprecated,pls-using-FormSpy)>) + - [`
    `](#Form) + - [``](#Field) + - [``](#VirtualField) + - [``](#FormSpy) + - [``](#FormProvider) + - [`(deprecated,pls using )`](#formconsumerdeprecated,pls-using-formspy) +- [Hook](#Hook) + - [`useFormEffects`](#useFormEffects) + - [`useForm`](#useForm) + - [`useField`](#useField) + - [`useVirtualField`](#useVirtualField) + - [`useFormSpy`](#useFormSpy) - [API](#API) - [`createFormActions`](#createFormActions) - [`createAsyncFormActions`](#createAsyncFormActions) - [`FormEffectHooks`](#FormEffectHooks) - [`createEffectHook`](#createEffectHook) - [Interfaces](#Interfaces) + - [`IForm`](#IForm) + - [`Imutators`](#Imutators) - [`IFormActions`](#IFormActions) - [`IFormAsyncActions`](#IFormAsyncActions) - [`IFieldState`](#IFieldState) - [`IVirtualFieldState`](#IVirtualFieldState) + - [`IFormSpyProps`](#IFormSpyProps) + - [`IFieldHook`](#IFieldHook) + - [`IVirtualFieldHook`](#IVirtualFieldHook) + - [`ISpyHook`](#ISpyHook) - [`SyncValidateResponse`](#SyncValidateResponse) - [`AsyncValidateResponse`](#AsyncValidateResponse) - [`ValidateResponse`](#ValidateResponse) @@ -462,13 +483,492 @@ const App = () => { - + + + ) + })} + + + ) + }} + + + + + ) + })} + + + + ) + }} + +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + + +#### display visible + +Example: see how `display` 与 `visible` affect values + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, LifeCycleTypes, FormSpy } from './src' + +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + +const CheckedField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : { + mutators.change(!state.value) + }} checked={!!state.value} /> } + {state.errors} + {state.warnings} + + }} + +) + +const actions = createFormActions() +const App = () => { + return ( +
    { + $(LifeCycleTypes.ON_FORM_INIT).subscribe(() => { + setFieldState('displayTrigger', state => state.value = true) + setFieldState('visibleTrigger', state => state.value = true) + setFieldState('a', state => state.value = 1) + setFieldState('b', state => state.value = 2) + }) + + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'visibleTrigger').subscribe((fieldState) => { + setFieldState('a', state => { + state.visible = fieldState.value + }) + }) + + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'displayTrigger').subscribe((fieldState) => { + setFieldState('b', state => { + state.display = fieldState.value + }) + }) + }} + > +
    + + + +
    +
    + + +
    + + + {({ state, form }) => { + return (
    + {JSON.stringify(form.getFormState(state => state.values))} +
    ) + }} +
    + + + +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + + +#### Linkage + +Example:Show/hide field and modified props/value by using effects + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, LifeCycleTypes } from './src' + +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + +const CheckedField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : { + mutators.change(!state.value) + }} checked={!!state.value} /> } + {state.errors} + {state.warnings} + + }} + +) + +const actions = createFormActions() +const App = () => { + return ( +
    { + $(LifeCycleTypes.ON_FORM_INIT).subscribe(() => { + setFieldState('a~', state => state.visible = false) + }) + + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'trigger').subscribe((triggerState) => { + setFieldState('a~', state => { + state.visible = triggerState.value + }) + }) + + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'a').subscribe((fieldState) => { + setFieldState('a-copy', state => { + state.value = fieldState.value + }) + }) + }} + > + +
    + +
    +
    + +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### Async Linkage + +Example:Change dataSource in select asynchronously by effects + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, LifeCycleTypes } from './src' + +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) +const CheckedField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : { + mutators.change(!state.value) + }} checked={!!state.value} /> } + {state.errors} + {state.warnings} + + }} + +) + +const SelectField = props => ( + + {({ state, mutators }) => { + const { loading, dataSource = [] } = state.props + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + +const actions = createFormActions() +const App = () => { + return ( +
    { + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'trigger').subscribe((fieldState) => { + const dataSource = [{ label: 'aa', value: 'aa' }, { label: 'bb', value: 'bb' } ] + setFieldState('sync-source', state => { + state.props.dataSource = fieldState.value ? dataSource : [] + }) + setFieldState('async-source', state => { + state.props.loading = true + }) + + setTimeout(() => { + setFieldState('async-source', state => { + state.props.loading = false + state.props.dataSource = fieldState.value ? dataSource : [] + }) + }, 300) + }) + }} + > + +
    + +
    +
    + +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### Linkage Validation + +Example:validation when form mounted and re-trigger validation when field change + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, LifeCycleTypes } from './src' + +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + +const actions = createFormActions() +const App = () => { + return ( +
    { + $(LifeCycleTypes.ON_FORM_MOUNT).subscribe(() => { + validate() + }) + + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'a').subscribe((fieldState) => { + setFieldState('a-copy', state => { + state.value = fieldState.value + }) + }) + }} + > + +
    + +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### Complex Linkage + +Example:See how ArrayField communicate with other field by using effects + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, LifeCycleTypes } from './src' + +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + +const actions = createFormActions() +const App = () => { + return ( +
    { + $(LifeCycleTypes.ON_FORM_INIT).subscribe(() => { + setFieldState('userList.*.username', state => { + state.visible = false + }) + }) + + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'trigger').subscribe((fieldState) => { + setFieldState('userList.*.username', state => { + state.visible = fieldState.value + }) + }) + }} + > +
    + + {({ state, mutators }) => { + return + }} + +
    +
    + + {({ state, mutators }) => { + return ( + + {state.value.map((item, index) => { + return ( +
    + + {({ state: innerState, mutators: innerMutator }) => { + return ( + + {Object.keys(innerState.value).map(key => { + if (!innerMutator.exist(key)) return + return ( + + ) })} @@ -513,7 +1013,8 @@ const App = () => { ) }} - + +
    ) } @@ -642,11 +1143,143 @@ const App = () => { ReactDOM.render(, document.getElementById('root')) ``` +#### Deconstruction + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, FormSpy, FormPath } from './src' + +const actions = createFormActions() + +const App = () => { + return ( +
    + + + {({ state, mutators }) => { + + const [start, end] = state.value + return
    + + { + mutators.change([e.target.value, end]) + }} /> + + { + mutators.change([start, e.target.value]) + }} /> +
    + }} +
    + + + {({ state, form }) => { + return (
    + Form values: + +
    +                {JSON.stringify(form.getFormState(state => state.values), null, 2)}    
    +              
    +
    +
    ) + }} +
    +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### Complex Deconstruction + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, FormSpy } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + + +const App = () => { + return ( +
    + + {({ state, mutators }) => { + return
    + +
    Field value:
    + +
    {JSON.stringify(state.value, null, 2)}
    +
    +
    + }} +
    + + {({ state, form }) => { + return (
    + Form values: + +
    +                {JSON.stringify(form.getFormState(state => state.values), null, 2)}    
    +              
    +
    +
    ) + }} +
    +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + ### Components --- -#### Form +#### `
    ` > `` Props @@ -676,7 +1309,7 @@ interface IFormProps { } ``` -#### Field +#### `` > `` Props @@ -901,7 +1534,7 @@ const App = () => { ReactDOM.render(, document.getElementById('root')) ``` -#### VirtualField +#### `` > `` Props @@ -997,7 +1630,7 @@ const App = () => { ReactDOM.render(, document.getElementById('root')) ``` -#### FormSpy +#### `` > `` Props @@ -1119,7 +1752,7 @@ const App = () => { ReactDOM.render(, document.getElementById('root')) ``` -#### FormProvider +#### `` > Used with FormSpy, often used in Cross-file consumption form state @@ -1183,7 +1816,7 @@ const App = () => { ReactDOM.render(, document.getElementById('root')) ``` -#### FormConsumer(deprecated,pls using FormSpy) +#### `(deprecated,pls using )` > `` Props @@ -1197,8 +1830,219 @@ interface IFormConsumerProps { } ``` + +### Hook + +#### `useFormEffects` + +> Implement local effects by using useFormEffects. Same effect as the example of [Linkage](#Linkage) + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, useFormEffects, LifeCycleTypes } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + +const CheckedField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : { + mutators.change(!state.value) + }} checked={!!state.value} /> } + {state.errors} + {state.warnings} + + }} + +) + +const FormFragment = () => { + useFormEffects(($, { setFieldState }) => { + $(LifeCycleTypes.ON_FORM_INIT).subscribe(() => { + setFieldState('a~', state => state.visible = false) + }) + + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'trigger').subscribe((triggerState) => { + setFieldState('a~', state => { + state.visible = triggerState.value + }) + }) + + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'a').subscribe((fieldState) => { + setFieldState('a-copy', state => { + state.value = fieldState.value + }) + }) + }) + + return ( + + +
    + +
    +
    + +
    +
    + ) +} + +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### useForm + +> get [IForm](#IForm) instance + +**Signature** + +```typescript +type useForm = < + Value = any, + DefaultValue = any, + EffectPayload = any, + EffectAction = any +>( + props: IFormProps +) => IForm +``` + +**Usage** + +```typescript +import { useForm } from '@uform/react' + +const FormFragment = () => { + const form = useForm() + return
    {form.getFieldValue('username')}
    +} +``` + +#### useField + +> get [IFieldHook](#IFieldHook) instance + +**Signature** + +```typescript +type useField = (options: IFieldStateUIProps): IFieldHook +``` + +**Usage** + +```typescript +import { useField } from '@uform/react' + +const FormFragment = (props) => { + const { + form, + state, + props: fieldProps, + mutators + } = useField({ name: 'username' }) + + return +} +``` + +#### useVirtualField + +> get [IVirtualFieldHook](#IVirtualFieldHook) instance + +**Signature** + +```typescript +type UseVirtualField = (options: IVirtualFieldStateProps): IVirtualFieldHook +``` + +**Usage** + +```typescript +import { UseVirtualField } from '@uform/react' + +const FormFragment = (props) => { + const { + form, + state, + props: fieldProps, + } = UseVirtualField({ name: 'username' }) + + return
    + {props.children} +
    +} +``` + +#### useFormSpy + +> get [ISpyHook](#ISpyHook) instance. Same effect as the first example of [FormSpy](#FormSpy). + +**Signature** + +```typescript +type useFormSpy = (props: IFormSpyProps): ISpyHook +``` + +**Usage** + +```typescript +import { useFormSpy, LifeCycleTypes } from '@uform/react' +const FormFragment = (props) => { + const { + form, + state, + type, + } = useFormSpy({ + selector: LifeCycleTypes.ON_FORM_VALUES_CHANGE, + reducer: (state, action, form) => ({ + count: state.count ? state.count + 1 : 1 + }) + }) + + return
    +
    count: {state.count || 0}
    +
    +} +``` + ### API +> The API is fully inherited from @uform/core. The specific API of @uform/react is listed below. + --- #### `createFormActions` @@ -1363,8 +2207,252 @@ ReactDOM.render(, document.getElementById('root')) ### Interfaces +> The Interfaces is fully inherited from @uform/core. The specific Interfaces of @uform/react is listed below. + --- +#### IForm + + + +> Form instance object API created by using createForm + + + +```typescript +interface IForm { +  /* +   * Form submission, if the callback parameter returns Promise, +   * Then the entire submission process will hold and load is true. +   * Wait for Promise resolve to trigger the form onFormSubmitEnd event while loading is false +   */ +   submit( +      onSubmit?: (values: IFormState['values']) => any | Promise +    ): Promise<{ +       Validated: IFormValidateResult +       Payload: any //onSubmit callback function return value +   }> +    +   /* +    * Clear the error message, you can pass the FormPathPattern to batch or precise control of the field to be cleared. +    * For example, clearErrors("*(aa,bb,cc)") +    */ +   clearErrors: (pattern?: FormPathPattern) => void +    +   /* +    * Get status changes, mainly used to determine which states in the current life cycle have changed in the form lifecycle hook. +    * For example, hasChanged(state,'value.aa') +    */ +   hasChanged(target: IFormState | IFieldState | IVirtualFieldState, path: FormPathPattern): boolean +    +   /* +    * Reset form +    */ +   reset(options?: { +     // Forced to empty +     forceClear?: boolean +     // Forced check +     validate?: boolean +     // Reset range for batch or precise control of the field to be reset +     selector?: FormPathPattern +   }): Promise +    +   /* +    * Validation form +    */ +   validate(path?: FormPathPattern, options?: { +     // Is it pessimistic check, if the current field encounters the first verification error, stop the subsequent verification process +     first?:boolean +   }): Promise +    +   /* +    * Set the form status +    */ +   setFormState( +     // Operation callback +     callback?: (state: IFormState) => any, +     // No trigger the event +     silent?: boolean +   ): void +    +   /* +    * Get form status +    */ +   getFormState( +     //transformer +     callback?: (state: IFormState) => any +   ): any +    +   /* +    * Set the field status +    */ +   setFieldState( +     // Field path +     path: FormPathPattern, +     // Operation callback +     callback?: (state: IFieldState) => void, +     // No trigger the event +     silent?: boolean +   ): void +    +   /* +    * Get the field status +    */ +   getFieldState( +     // Field path +     path: FormPathPattern, +     // Transformer +     callback?: (state: IFieldState) => any +   ): any +    +   /* +    * Registration field +    */ +   registerField(props: { +    // Node path +    path?: FormPathPattern +    // Data path +    name?: string +    // Field value +    value?: any +    // Field multi-value +    values?: any[] +    // Field initial value +    initialValue?: any +    // Field extension properties +    props?: any +    // Field check rule +    rules?: ValidatePatternRules[] +    // Field is required +    required?: boolean +    // Is the field editable? +    editable?: boolean +    // Whether the field is dirty check +    useDirty?: boolean +    // Field state calculation container, mainly used to extend the core linkage rules +    computeState?: (draft: IFieldState, prevState: IFieldState) => void +  }): IField +   +  /* +   * Register virtual fields +   */ +  registerVirtualField(props: { +    // Node path +    path?: FormPathPattern +    // Data path +    name?: string +    // Field extension properties +    props?: any +    // Whether the field is dirty check +    useDirty?: boolean +    // Field state calculation container, mainly used to extend the core linkage rules +    computeState?: (draft: IFieldState, prevState: IFieldState) => void +  }): IVirtualField +   +  /* +   * Create a field data operator, which will explain the returned API in detail later. +   */ +  createMutators(field: IField): IMutators +   +  /* +   * Get the form observer tree +   */ +  getFormGraph(): IFormGraph +   +  /* +   * Set the form observer tree +   */ +  setFormGraph(graph: IFormGraph): void +   +  /* +   * Listen to the form life cycle +   */ +  subscribe(callback?: ({ +    type, +    payload +  }: { +    type: string +    payload: any +  }) => void): number +   +  /* +   * Cancel the listening form life cycle +   */ +  unsubscribe(id: number): void +   +  /* +   * Trigger form custom life cycle +   */ +  notify: (type: string, payload?: T) => void +   +  /* +   * Set the field value +   */ +  setFieldValue(path?: FormPathPattern, value?: any): void +   +  /* +   * Get the field value +   */ +  getFieldValue(path?: FormPathPattern): any +   +  /* +   * Set the initial value of the field +   */ +  setFieldInitialValue(path?: FormPathPattern, value?: any): void +   +  /* +   * Get the initial value of the field +   */ +  getFieldInitialValue(path?: FormPathPattern): any +} +``` + + +#### Imutators + + + +> The instance API created by crewikiutators is mainly used to operate field data. + + + +```typescript +interface IMutators { + // Changing the field value and multi parameter condition will store all parameters in values + change(...values: any[]): any + // Get focus, trigger active state change + focus(): void + // Lose focus, trigger active / visited status change + blur (): void + // Trigger current field verifier + validate(): Promise + // Whether the value of the current field exists in the values property of form + exist (index?: number | string): Boolean + + /**Array operation method**/ + + // Append data + push(value?: any): any[] + // Pop up tail data + pop (): any[] + // Insert data + insert(index: number, value: any): any[] + // Delete data + remove(index: number | string): any + // Head insertion + unshift(value: any): any[] + // Head ejection + shift(): any[] + // Move element + move($from: number, $to: number): any[] + // Move down + moveDown(index: number): any[] + // Move up + moveUp(index: number): any[] +} +``` + + #### IFormActions ```typescript @@ -1678,6 +2766,62 @@ interface IVirtualFieldState { } ``` +### IFormSpyProps + +```typescript + +interface IFormSpyProps { + selector?: string[] | string + reducer?: ( + state: any, + action: { type: string; payload: any }, + form: IForm + ) => any + children?: React.ReactElement | ((api: IFormSpyAPI) => React.ReactElement) +} + +``` + + +### IFieldHook + +```typescript + +interface IFieldHook { + form: IForm + state: IFieldState + props: {} + mutators: IMutators +} + +``` + + +### IVirtualFieldHook + +```typescript + +interface IVirtualFieldHook { + form: IForm + state: IFieldState + props: {} +} + +``` + +### ISpyHook + +```typescript + +interface ISpyHook { + form: IForm + state: IFieldState + props: {} + mutators: IMutators +} + +``` + #### SyncValidateResponse ```typescript diff --git a/packages/react/README.zh-cn.md b/packages/react/README.zh-cn.md index 2b554b058a0..6595e963f49 100644 --- a/packages/react/README.zh-cn.md +++ b/packages/react/README.zh-cn.md @@ -30,25 +30,44 @@ npm install --save @uform/react - [`对象类型字段`](#对象类型字段) - [`简单数组类型字段`](#简单数组类型字段) - [`对象数组类型字段`](#对象数组类型字段) + - [`display与visible`](#display-visible) + - [`简单联动`](#简单联动) + - [`异步联动`](#异步联动) + - [`联动校验`](#联动校验) + - [`复杂联动`](#复杂联动) - [`combo字段`](#combo字段) - [`跨文件消费表单数据`](#跨文件消费表单数据) + - [`简单解构`](#简单解构) + - [`复杂结构`](#复杂结构) - [Components](#components) - - [`Form`](#Form) - - [`Field`](#Field) - - [`VirtualField`](#VirtualField) - - [`FormSpy`](#FormSpy) - - [`FormProvider`](#FormProvider) - - [`FormConsumer(即将废弃,请使用FormSpy)`](<#FormConsumer(即将废弃,请使用FormSpy)>) + - [`
    `](#Form) + - [``](#Field) + - [``](#VirtualField) + - [``](#FormSpy) + - [``](#FormProvider) + - [`(即将废弃,请使用)`](<#FormConsumer(即将废弃,请使用FormSpy)>) +- [Hook](#Hook) + - [`useFormEffects`](#useFormEffects) + - [`useForm`](#useForm) + - [`useField`](#useField) + - [`useVirtualField`](#useVirtualField) + - [`useFormSpy`](#useFormSpy) - [API](#API) - [`createFormActions`](#createFormActions) - [`createAsyncFormActions`](#createAsyncFormActions) - [`FormEffectHooks`](#FormEffectHooks) - [`createEffectHook`](#createEffectHook) - [Interfaces](#Interfaces) - - [`IFormActions`](#IFormActions) + - [`IForm`](#IForm) + - [`Imutators`](#Imutators) + - [`IFormActions`](#IFormActions) - [`IFormAsyncActions`](#IFormAsyncActions) - [`IFieldState`](#IFieldState) - [`IVirtualFieldState`](#IVirtualFieldState) + - [`IFormSpyProps`](#IFormSpyProps) + - [`IFieldHook`](#IFieldHook) + - [`IVirtualFieldHook`](#IVirtualFieldHook) + - [`ISpyHook`](#ISpyHook) - [`SyncValidateResponse`](#SyncValidateResponse) - [`AsyncValidateResponse`](#AsyncValidateResponse) - [`ValidateResponse`](#ValidateResponse) @@ -155,19 +174,21 @@ import { Form, Field, createFormActions } from './src' const actions = createFormActions() const InputField = props => ( - {({ state, mutators }) => ( - - { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : + /> } {state.errors} {state.warnings} - )} + }} ) @@ -272,19 +293,21 @@ import { Form, Field, createFormActions } from './src' const actions = createFormActions() const InputField = props => ( - {({ state, mutators }) => ( - - { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : + /> } {state.errors} {state.warnings} - )} + }} ) @@ -362,19 +385,21 @@ import { Form, Field, createFormActions } from './src' const actions = createFormActions() const InputField = props => ( - {({ state, mutators }) => ( - - { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : + /> } {state.errors} {state.warnings} - )} + }} ) @@ -407,45 +432,531 @@ const App = () => { ReactDOM.render(, document.getElementById('root')) ``` -#### 对象数组类型字段 +#### 对象数组类型字段 + +示例:用户 id 列表,增删改查 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + +const App = () => { + return ( + + + {({ state, mutators }) => { + return ( + + {state.value.map((item, index) => { + return ( +
    + + {({ state: innerState, mutators: innerMutator }) => { + return ( + + {Object.keys(innerState.value).map(key => { + if (!innerMutator.exist(key)) return + return ( + + + + + ) + })} + + + ) + }} + + + +
    + ) + })} + + +
    + ) + }} +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### display visible + +示例: display 与 visible 对values的影响 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, LifeCycleTypes, FormSpy } from './src' + +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + +const CheckedField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : { + mutators.change(!state.value) + }} checked={!!state.value} /> } + {state.errors} + {state.warnings} + + }} + +) + +const actions = createFormActions() +const App = () => { + return ( +
    { + $(LifeCycleTypes.ON_FORM_INIT).subscribe(() => { + setFieldState('displayTrigger', state => state.value = true) + setFieldState('visibleTrigger', state => state.value = true) + setFieldState('a', state => state.value = 1) + setFieldState('b', state => state.value = 2) + }) + + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'visibleTrigger').subscribe((fieldState) => { + setFieldState('a', state => { + state.visible = fieldState.value + }) + }) + + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'displayTrigger').subscribe((fieldState) => { + setFieldState('b', state => { + state.display = fieldState.value + }) + }) + }} + > +
    + + + +
    +
    + + +
    + + + {({ state, form }) => { + return ( +
    +              {JSON.stringify(form.getFormState(state => state.values), null, 2)}    
    +            
    +
    ) + }} +
    + + + +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### 简单联动 + +示例:显示及隐藏,修改props和value + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, LifeCycleTypes } from './src' + +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + +const CheckedField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : { + mutators.change(!state.value) + }} checked={!!state.value} /> } + {state.errors} + {state.warnings} + + }} + +) + +const actions = createFormActions() +const App = () => { + return ( +
    { + $(LifeCycleTypes.ON_FORM_INIT).subscribe(() => { + setFieldState('a~', state => state.visible = false) + }) + + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'trigger').subscribe((triggerState) => { + setFieldState('a~', state => { + state.visible = triggerState.value + }) + }) + + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'a').subscribe((fieldState) => { + setFieldState('a-copy', state => { + state.value = fieldState.value + }) + }) + }} + > + +
    + +
    +
    + +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### 异步联动 + +示例:异步切换 select 的 dataSource + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, LifeCycleTypes } from './src' + +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) +const CheckedField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : { + mutators.change(!state.value) + }} checked={!!state.value} /> } + {state.errors} + {state.warnings} + + }} + +) + +const SelectField = props => ( + + {({ state, mutators }) => { + const { loading, dataSource = [] } = state.props + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + +const actions = createFormActions() +const App = () => { + return ( +
    { + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'trigger').subscribe((fieldState) => { + const dataSource = [{ label: 'aa', value: 'aa' }, { label: 'bb', value: 'bb' } ] + setFieldState('sync-source', state => { + state.props.dataSource = fieldState.value ? dataSource : [] + }) + setFieldState('async-source', state => { + state.props.loading = true + }) + + setTimeout(() => { + setFieldState('async-source', state => { + state.props.loading = false + state.props.dataSource = fieldState.value ? dataSource : [] + }) + }, 300) + }) + }} + > + +
    + +
    +
    + +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### 联动校验 + +示例:初始化校验,字段change时自动重新触发校验 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, LifeCycleTypes } from './src' + +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + +const actions = createFormActions() +const App = () => { + return ( +
    { + $(LifeCycleTypes.ON_FORM_MOUNT).subscribe(() => { + validate() + }) + + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'a').subscribe((fieldState) => { + setFieldState('a-copy', state => { + state.value = fieldState.value + }) + }) + }} + > + +
    + +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### 复杂联动 -示例:用户 id 列表,增删改查 +示例:ArrayField复杂联动 ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions } from './src' +import { Form, Field, createFormActions, LifeCycleTypes } from './src' -const actions = createFormActions() const InputField = props => ( - {({ state, mutators }) => ( - - { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : + /> } {state.errors} {state.warnings} - )} + }} ) +const actions = createFormActions() const App = () => { return ( -
    - + { + $(LifeCycleTypes.ON_FORM_INIT).subscribe(() => { + setFieldState('userList.*.username', state => { + state.visible = false + }) + }) + + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'trigger').subscribe((fieldState) => { + setFieldState('userList.*.username', state => { + state.visible = fieldState.value + }) + }) + }} + > + + {({ state, mutators }) => { + return + }} + +
    + + {({ state, mutators }) => { return ( {state.value.map((item, index) => { @@ -460,15 +971,9 @@ const App = () => { return ( - ) })} @@ -513,7 +1018,8 @@ const App = () => { ) }} - + +
    ) } @@ -533,19 +1039,21 @@ import { Form, Field, createFormActions, FormSpy } from './src' const actions = createFormActions() const InputField = props => ( - {({ state, mutators }) => ( - - { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : + /> } {state.errors} {state.warnings} - )} + }} ) @@ -593,19 +1101,21 @@ import { Form, Field, createFormActions, FormSpy, FormProvider } from './src' const actions = createFormActions() const InputField = props => ( - {({ state, mutators }) => ( - - { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : + /> } {state.errors} {state.warnings} - )} + }} ) @@ -642,11 +1152,144 @@ const App = () => { ReactDOM.render(, document.getElementById('root')) ``` +#### 简单解构 + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, FormSpy, FormPath } from './src' + +const actions = createFormActions() + +const App = () => { + return ( +
    + + + {({ state, mutators }) => { + + const [start, end] = state.value + return
    + + { + mutators.change([e.target.value, end]) + }} /> + + { + mutators.change([start, e.target.value]) + }} /> +
    + }} +
    + + + {({ state, form }) => { + return (
    + Form values: + +
    +                {JSON.stringify(form.getFormState(state => state.values), null, 2)}    
    +              
    +
    +
    ) + }} +
    +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### 复杂解构 + + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, FormSpy } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + + +const App = () => { + return ( +
    + + {({ state, mutators }) => { + return
    + +
    Field value:
    + +
    {JSON.stringify(state.value, null, 2)}
    +
    +
    + }} +
    + + {({ state, form }) => { + return (
    + Form values: + +
    +                {JSON.stringify(form.getFormState(state => state.values), null, 2)}    
    +              
    +
    +
    ) + }} +
    +
    + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + ### Components --- -#### Form +#### `
    ` > Form 组件属性定义 @@ -677,7 +1320,7 @@ interface IFormProps { } ``` -#### Field +#### `` > Field 组件属性定义 @@ -729,19 +1372,21 @@ import { Form, Field, createFormActions } from './src' const actions = createFormActions() const InputField = props => ( - {({ state, mutators }) => ( - - { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : + /> } {state.errors} {state.warnings} - )} + }} ) @@ -913,7 +1558,7 @@ const App = () => { ReactDOM.render(, document.getElementById('root')) ``` -#### VirtualField +#### `` > VirtualField 组件属性定义 @@ -949,19 +1594,21 @@ import { Form, Field, createFormActions, VirtualField } from './src' const actions = createFormActions() const InputField = props => ( - {({ state, mutators }) => ( - - { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : + /> } {state.errors} {state.warnings} - )} + }} ) @@ -1014,7 +1661,7 @@ const App = () => { ReactDOM.render(, document.getElementById('root')) ``` -#### FormSpy +#### `` > FormSpy 组件属性定义 @@ -1044,19 +1691,21 @@ import { Form, Field, createFormActions, FormSpy, LifeCycleTypes } from './src' const actions = createFormActions() const InputField = props => ( - {({ state, mutators }) => ( - - { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : + /> } {state.errors} {state.warnings} - )} + }} ) @@ -1095,19 +1744,21 @@ import { Form, Field, createFormActions, FormSpy } from './src' const actions = createFormActions() const InputField = props => ( - {({ state, mutators }) => ( - - { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : + /> } {state.errors} {state.warnings} - )} + }} ) @@ -1136,7 +1787,7 @@ const App = () => { ReactDOM.render(, document.getElementById('root')) ``` -#### FormProvider +#### `` > 与 FormSpy 搭配使用,常用与跨文件通信 @@ -1150,19 +1801,21 @@ import { Form, Field, createFormActions, FormSpy, FormProvider } from './src' const actions = createFormActions() const InputField = props => ( - {({ state, mutators }) => ( - - { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : + /> } {state.errors} {state.warnings} - )} + }} ) @@ -1196,25 +1849,235 @@ const App = () => { ) } -ReactDOM.render(, document.getElementById('root')) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `(即将废弃,请使用 )` + +> FormConsumer 组件属性定义 + +```typescript +interface IFormConsumerProps { + // 选择器, 如:[ LifeCycleTypes.ON_FORM_SUBMIT_START, LifeCycleTypes.ON_FORM_SUBMIT_END ] + selector?: string[] | string + children?: + | React.ReactElement + | ((api: IFormConsumerAPI) => React.ReactElement) +} +``` + +### Hook + +#### `useFormEffects` + +> 使用 useFormEffects 可以实现局部effect的表单组件,效果同:[简单联动](#简单联动) + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, useFormEffects, LifeCycleTypes } from './src' + +const actions = createFormActions() +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + +const CheckedField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : { + mutators.change(!state.value) + }} checked={!!state.value} /> } + {state.errors} + {state.warnings} + + }} + +) + +const FormFragment = () => { + useFormEffects(($, { setFieldState }) => { + $(LifeCycleTypes.ON_FORM_INIT).subscribe(() => { + setFieldState('a~', state => state.visible = false) + }) + + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'trigger').subscribe((triggerState) => { + setFieldState('a~', state => { + state.visible = triggerState.value + }) + }) + + $(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, 'a').subscribe((fieldState) => { + setFieldState('a-copy', state => { + state.value = fieldState.value + }) + }) + }) + + return ( + + +
    + +
    +
    + +
    +
    + ) +} + +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### useForm + +> 获取一个 [IForm](#IForm) 实例 + +**签名** + +```typescript +type useForm = < + Value = any, + DefaultValue = any, + EffectPayload = any, + EffectAction = any +>( + props: IFormProps +) => IForm +``` + +**用法** + +```typescript +import { useForm } from '@uform/react' + +const FormFragment = () => { + const form = useForm() + return
    {form.getFieldValue('username')}
    +} +``` + +#### useField + +> 获取一个 [IFieldHook](#IFieldHook) 实例 + +**签名** + +```typescript +type useField = (options: IFieldStateUIProps): IFieldHook +``` + +**用法** + +```typescript +import { useField } from '@uform/react' + +const FormFragment = (props) => { + const { + form, + state, + props: fieldProps, + mutators + } = useField({ name: 'username' }) + + return +} +``` + +#### useVirtualField + +> 获取一个 [IVirtualFieldHook](#IVirtualFieldHook) 实例 + +**签名** + +```typescript +type UseVirtualField = (options: IVirtualFieldStateProps): IVirtualFieldHook +``` + +**用法** + +```typescript +import { UseVirtualField } from '@uform/react' + +const FormFragment = (props) => { + const { + form, + state, + props: fieldProps, + } = UseVirtualField({ name: 'username' }) + + return
    + {props.children} +
    +} ``` -#### FormConsumer(即将废弃,请使用 FormSpy) +#### useFormSpy -> FormConsumer 组件属性定义 +> 获取一个 [ISpyHook](#ISpyHook) 实例, 实现[FormSpy](#FormSpy) 第一个例子 + +**签名** ```typescript -interface IFormConsumerProps { - // 选择器, 如:[ LifeCycleTypes.ON_FORM_SUBMIT_START, LifeCycleTypes.ON_FORM_SUBMIT_END ] - selector?: string[] | string - children?: - | React.ReactElement - | ((api: IFormConsumerAPI) => React.ReactElement) +type useFormSpy = (props: IFormSpyProps): ISpyHook +``` + +**用法** + +```typescript +import { useFormSpy, LifeCycleTypes } from '@uform/react' +const FormFragment = (props) => { + const { + form, + state, + type, + } = useFormSpy({ + selector: LifeCycleTypes.ON_FORM_VALUES_CHANGE, + reducer: (state, action, form) => ({ + count: state.count ? state.count + 1 : 1 + }) + }) + + return
    +
    count: {state.count || 0}
    +
    } ``` ### API +> 整体完全继承@uform/core, 下面只列举@uform/react的特有API + --- #### `createFormActions` @@ -1357,8 +2220,245 @@ ReactDOM.render(, document.getElementById('root')) ### Interfaces +> 整体完全继承@uform/core, 下面只列举@uform/react的特有的Interfaces + --- +#### IForm + +> 通过 createForm 创建出来的 Form 实例对象 API + +```typescript +interface IForm { + /* + * 表单提交,如果回调参数返回Promise, + * 那么整个提交流程会hold住,同时loading为true, + * 等待Promise resolve才触发表单onFormSubmitEnd事件,同时loading为false + */ + submit( + onSubmit?: (values: IFormState['values']) => any | Promise + ): Promise<{ + validated: IFormValidateResult + payload: any //onSubmit回调函数返回值 + }> + + /* + * 清空错误消息,可以通过传FormPathPattern来批量或精确控制要清空的字段, + * 比如clearErrors("*(aa,bb,cc)") + */ + clearErrors: (pattern?: FormPathPattern) => void + + /* + * 获取状态变化情况,主要用于在表单生命周期钩子内判断当前生命周期中有哪些状态发生了变化, + * 比如hasChanged(state,'value.aa') + */ + hasChanged( + target: IFormState | IFieldState | IVirtualFieldState, + path: FormPathPattern + ): boolean + + /* + * 重置表单 + */ + reset(options?: { + //强制清空 + forceClear?: boolean + //强制校验 + validate?: boolean + //重置范围,用于批量或者精确控制要重置的字段 + selector?: FormPathPattern + }): Promise + + /* + * 校验表单 + */ + validate( + path?: FormPathPattern, + options?: { + //是否悲观校验,如果当前字段遇到第一个校验错误则停止后续校验流程 + first?: boolean + } + ): Promise + + /* + * 设置表单状态 + */ + setFormState( + //操作回调 + callback?: (state: IFormState) => any, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取表单状态 + */ + getFormState( + //transformer + callback?: (state: IFormState) => any + ): any + + /* + * 设置字段状态 + */ + setFieldState( + //字段路径 + path: FormPathPattern, + //操作回调 + callback?: (state: IFieldState) => void, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取字段状态 + */ + getFieldState( + //字段路径 + path: FormPathPattern, + //transformer + callback?: (state: IFieldState) => any + ): any + + /* + * 注册字段 + */ + registerField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段值 + value?: any + //字段多参值 + values?: any[] + //字段初始值 + initialValue?: any + //字段扩展属性 + props?: any + //字段校验规则 + rules?: ValidatePatternRules[] + //字段是否必填 + required?: boolean + //字段是否可编辑 + editable?: boolean + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IField + + /* + * 注册虚拟字段 + */ + registerVirtualField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段扩展属性 + props?: any + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IVirtualField + + /* + * 创建字段数据操作器,后面会详细解释返回的API + */ + createMutators(field: IField): IMutators + + /* + * 获取表单观察者树 + */ + getFormGraph(): IFormGraph + + /* + * 设置表单观察者树 + */ + setFormGraph(graph: IFormGraph): void + + /* + * 监听表单生命周期 + */ + subscribe( + callback?: ({ type, payload }: { type: string; payload: any }) => void + ): number + + /* + * 取消监听表单生命周期 + */ + unsubscribe(id: number): void + + /* + * 触发表单自定义生命周期 + */ + notify: (type: string, payload?: T) => void + + /* + * 设置字段值 + */ + setFieldValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段值 + */ + getFieldValue(path?: FormPathPattern): any + + /* + * 设置字段初始值 + */ + setFieldInitialValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段初始值 + */ + getFieldInitialValue(path?: FormPathPattern): any +} +``` + + +#### Imutators + +> 通过 createMutators 创建出来的实例 API,主要用于操作字段数据 + +```typescript +interface IMutators { + //改变字段值,多参情况,会将所有参数存在values中 + change(...values: any[]): any + //获取焦点,触发active状态改变 + focus(): void + //失去焦点,触发active/visited状态改变 + blur(): void + //触发当前字段校验器 + validate(): Promise + //当前字段的值是否在Form的values属性中存在 + exist(index?: number | string): boolean + + /**数组操作方法**/ + + //追加数据 + push(value?: any): any[] + //弹出尾部数据 + pop(): any[] + //插入数据 + insert(index: number, value: any): any[] + //删除数据 + remove(index: number | string): any + //头部插入 + unshift(value: any): any[] + //头部弹出 + shift(): any[] + //移动元素 + move($from: number, $to: number): any[] + //下移 + moveDown(index: number): any[] + //上移 + moveUp(index: number): any[] +} +``` + #### IFormActions ```typescript @@ -1732,6 +2832,59 @@ interface IVirtualFieldState { } ``` +### IFormSpyProps + +```typescript + +interface IFormSpyProps { + selector?: string[] | string + reducer?: ( + state: any, + action: { type: string; payload: any }, + form: IForm + ) => any + children?: React.ReactElement | ((api: IFormSpyAPI) => React.ReactElement) +} + +``` + +### IFieldHook + +```typescript + +interface IFieldHook { + form: IForm + state: IFieldState + props: {} + mutators: IMutators +} + +``` + +### IVirtualFieldHook + +```typescript + +interface IVirtualFieldHook { + form: IForm + state: IFieldState + props: {} +} + +``` + +### ISpyHook + +```typescript + +interface ISpyHook { + form: IForm + state: any + type: string +} + +``` + #### SyncValidateResponse ```typescript diff --git a/packages/react/src/hooks/useFormSpy.ts b/packages/react/src/hooks/useFormSpy.ts index 1c2000f194f..7d194487ae6 100644 --- a/packages/react/src/hooks/useFormSpy.ts +++ b/packages/react/src/hooks/useFormSpy.ts @@ -9,10 +9,10 @@ import { } from 'react' import { FormHeartSubscriber, LifeCycleTypes } from '@uform/core' import { isStr, FormPath, isArr } from '@uform/shared' -import { IFormSpyProps } from '../types' +import { IFormSpyProps, ISpyHook } from '../types' import FormContext, { BroadcastContext } from '../context' -export const useFormSpy = (props: IFormSpyProps) => { +export const useFormSpy = (props: IFormSpyProps): ISpyHook => { const broadcast = useContext(BroadcastContext) const form = useContext(FormContext) const initializedRef = useRef(false) diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 95f243d7ca9..44c5a8166ca 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -123,6 +123,12 @@ export interface IVirtualFieldHook { props: {} } +export interface ISpyHook { + form: IForm + state: any + type: string +} + export interface IFormActions { submit( onSubmit?: (values: IFormState['values']) => void | Promise From 193b3e3597e62fbd13582a43be1202ef9537609b Mon Sep 17 00:00:00 2001 From: quirkyvar Date: Fri, 22 Nov 2019 10:01:53 +0800 Subject: [PATCH 50/99] docs: fix typo and improve docs (#408) --- packages/react/README.md | 24 ++++++++++++++++++------ packages/react/README.zh-cn.md | 30 +++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/packages/react/README.md b/packages/react/README.md index 072157e1c97..40778b7af3d 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -1162,11 +1162,11 @@ const App = () => { const [start, end] = state.value return
    - { + { mutators.change([e.target.value, end]) }} /> - { + { mutators.change([start, e.target.value]) }} />
    @@ -1176,9 +1176,6 @@ const App = () => { actions.setFormState(state => { state.values = { start: 'x', end: 'y' } }) - // actions.setFieldState(FormPath.match('\\[start,end\\]'), state => { - // state.value = ['x', 'y'] - // }) }}>set value {({ state, form }) => { @@ -1204,7 +1201,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React, { useState } from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, FormSpy } from './src' +import { Form, Field, createFormActions, FormSpy, FormPath } from './src' const actions = createFormActions() const InputField = props => ( @@ -1256,6 +1253,21 @@ const App = () => {
    }} + {({ state, form }) => { return (
    diff --git a/packages/react/README.zh-cn.md b/packages/react/README.zh-cn.md index 6595e963f49..ab539700359 100644 --- a/packages/react/README.zh-cn.md +++ b/packages/react/README.zh-cn.md @@ -205,7 +205,7 @@ const App = () => { name="age" rules={[ val => - val === undefiend + val === undefined ? { type: 'error', message: 'age is required' } : undefined ]} @@ -217,7 +217,7 @@ const App = () => { name="gender" rules={[ val => - val === undefiend + val === undefined ? { type: 'warning', message: 'gender is required' } : undefined ]} @@ -242,7 +242,7 @@ const App = () => { rules={[ { validator(value) { - return value === undefiend + return value === undefined ? 'This field can not be empty, please enter {{scope.outerVariable}}' : undefined }, @@ -1171,11 +1171,11 @@ const App = () => { const [start, end] = state.value return
    - { + { mutators.change([e.target.value, end]) }} /> - { + { mutators.change([start, e.target.value]) }} />
    @@ -1185,9 +1185,6 @@ const App = () => { actions.setFormState(state => { state.values = { start: 'x', end: 'y' } }) - // actions.setFieldState(FormPath.match('\\[start,end\\]'), state => { - // state.value = ['x', 'y'] - // }) }}>set value {({ state, form }) => { @@ -1214,7 +1211,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React, { useState } from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, FormSpy } from './src' +import { Form, Field, createFormActions, FormSpy, FormPath } from './src' const actions = createFormActions() const InputField = props => ( @@ -1266,6 +1263,21 @@ const App = () => {
    }} + {({ state, form }) => { return (
    From e17ebaeded812294c9100844b641a6f775ef9333 Mon Sep 17 00:00:00 2001 From: dahuang Date: Fri, 22 Nov 2019 15:26:25 +0800 Subject: [PATCH 51/99] Fix isFn can't test async function (#411) * Fix isFn * Fix(@uform/shared): add isFn unittest --- packages/shared/src/__tests__/index.spec.ts | 51 ++++++++++++++------- packages/shared/src/types.ts | 16 +++++-- 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/packages/shared/src/__tests__/index.spec.ts b/packages/shared/src/__tests__/index.spec.ts index cd1795db081..2c4b4081ab2 100644 --- a/packages/shared/src/__tests__/index.spec.ts +++ b/packages/shared/src/__tests__/index.spec.ts @@ -8,6 +8,7 @@ import { deprecate } from '../deprecate' import { isValid, isEmpty } from '../isEmpty' import { stringLength } from '../string' import { Subscribable } from '../subscribable' +import { isFn } from '../types' describe('array', () => { test('toArr', () => { @@ -99,14 +100,14 @@ describe('compare', () => { expect(isEqual([{ k1: 'v1' }, { k2: 'v2' }], [{ k1: 'v1' }, { k2: 'v2' }])).toBeTruthy() expect(isEqual([{ k1: 'v1' }, { k2: 'v2' }], [{ k1: 'v1' }])).toBeFalsy() expect(isEqual([{ k1: 'v1' }, { k2: 'v2' }], [{ k1: 'v1' }], (_, key) => key !== 'k1')).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' }) @@ -124,8 +125,8 @@ describe('compare', () => { 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() + expect(isEqual(regexpA, new RegExp(/test/))).toBeTruthy() + expect(isEqual(regexpA, regexpB)).toBeFalsy() // URL const urlA = new URL('https://uformjs.org/') const urlB = new URL('https://www.taobao.com') @@ -151,7 +152,7 @@ describe('clone and compare', () => { // ff.add({}) // var gg = new Set() // gg.add(3) - + var a = { aa: 123123, bb: [{ bb: 111 }, { bb: 222 }], @@ -175,7 +176,7 @@ describe('clone and compare', () => { // expect(a.ff === cloned.ff).toBeTruthy() // expect(a.gg === cloned.gg).toBeTruthy() }) - + test('filter equal', () => { var a = { aa: { @@ -187,11 +188,11 @@ describe('clone and compare', () => { bb: 123 } } - + expect(isEqual(a, b)).toBeTruthy() expect(isEqual(a, b, (_, key) => key !== 'aa')).toBeTruthy() }) - + test('filter clone', () => { var a = { aa: { @@ -201,9 +202,9 @@ describe('clone and compare', () => { dd: [1, 3, 4, 5] } } - + var b = clone(a, (_, key) => key !== 'aa') - + expect(a.aa === b.aa).toBeTruthy() expect(a.cc === b.cc).toBeFalsy() expect(isEqual(a.cc, b.cc)).toBeTruthy() @@ -258,8 +259,8 @@ describe('isEmpty', () => { expect(isEmpty('')).toBeTruthy() // val - function - const emptyFunc = function() {} - const nonEmptyFunc = function(payload) { + const emptyFunc = function () { } + const nonEmptyFunc = function (payload) { console.log(payload) } expect(isEmpty(emptyFunc)).toBeTruthy() @@ -280,7 +281,7 @@ describe('isEmpty', () => { 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({ key: 'val' })).toBeFalsy() expect(isEmpty({})).toBeTruthy() expect(isEmpty(Symbol())) @@ -301,12 +302,12 @@ describe('shared Subscribable', () => { const obj = new Subscribable() const handlerIdx = obj.subscribe(cb) expect(handlerIdx).toEqual(1) - obj.notify({ key: 'val'}) + obj.notify({ key: 'val' }) expect(cb).toHaveBeenCalledTimes(1) expect(cb).toBeCalledWith({ key: 'val' }) obj.unsubscribe(handlerIdx) - obj.notify({ key: 'val'}) + obj.notify({ key: 'val' }) expect(cb).toHaveBeenCalledTimes(1) // subscribable with custom filter @@ -321,7 +322,7 @@ describe('shared Subscribable', () => { objWithCustomFilter.subscribe(cb) const handlerIdx2 = objWithCustomFilter.subscribe(cb) expect(handlerIdx2).toEqual(2) - objWithCustomFilter.notify({ key4: 'val4'}) + objWithCustomFilter.notify({ key4: 'val4' }) expect(cb).toHaveBeenCalledTimes(3) expect(cb).toBeCalledWith({ key4: 'val4', key2: 'val2' }) @@ -335,7 +336,23 @@ describe('shared Subscribable', () => { notify: customNotify } objWithCustomNotify.subscribe(cb) - objWithCustomNotify.notify({ key3: 'val3'}) + objWithCustomNotify.notify({ key3: 'val3' }) expect(customNotify).toBeCalledTimes(1) }) }) + + +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() + }) +}) diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 31a19f4d322..9b2f4cbbec3 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -1,8 +1,14 @@ -const isType = (type: string) => (obj: unknown): obj is T => - obj != null && Object.prototype.toString.call(obj) === `[object ${type}]` -// FIXME: isFn, isArr is incorrect -export const isFn = isType<(...args: any[]) => any>('Function') -export const isArr = Array.isArray || isType('Array') +const isType = (type: string | string[]) => (obj: unknown): obj is T => + obj != null && + (Array.isArray(type) ? type : [type]).some( + t => Object.prototype.toString.call(obj) === `[object ${t}]` + ) +export const isFn = isType<(...args: any[]) => any>([ + 'Function', + 'AsyncFunction', + 'GeneratorFunction' +]) +export const isArr = Array.isArray export const isPlainObj = isType('Object') export const isStr = isType('String') export const isBool = isType('Boolean') From b5400eff1a7232e575774d0855dcb97d7d05312b Mon Sep 17 00:00:00 2001 From: Janry Date: Fri, 22 Nov 2019 15:37:57 +0800 Subject: [PATCH 52/99] Improve docs (#407) --- .../react-schema-renderer/README.zh-cn.md | 1757 +++++++++++++++-- .../src/hooks/useSchemaForm.ts | 3 +- packages/react-schema-renderer/src/index.tsx | 1 - .../src/shared/registry.ts | 16 +- .../src/shared/schema.ts | 25 +- packages/react-schema-renderer/src/types.ts | 14 +- packages/react/src/types.ts | 10 +- 7 files changed, 1616 insertions(+), 210 deletions(-) diff --git a/packages/react-schema-renderer/README.zh-cn.md b/packages/react-schema-renderer/README.zh-cn.md index 1e5d905f91b..243228508e8 100644 --- a/packages/react-schema-renderer/README.zh-cn.md +++ b/packages/react-schema-renderer/README.zh-cn.md @@ -1,7 +1,8 @@ # @uform/react-schema-renderer -> Schema渲染引擎,该包主要依赖了@uform/react,它的职责很简单,核心就做了两件事情: + +> Schema 渲染引擎,该包主要依赖了@uform/react,它的职责很简单,核心就做了两件事情: > -> - 解析Form Schema协议,递归渲染 +> - 解析 Form Schema 协议,递归渲染 > - 管理自定义组件 ### 安装 @@ -14,162 +15,255 @@ npm install --save @uform/react-schema-renderer +- [使用方式](#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F) + * [快速开始](#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B) + * [JSON Schema 驱动表单渲染](#json-schema-%E9%A9%B1%E5%8A%A8%E8%A1%A8%E5%8D%95%E6%B8%B2%E6%9F%93) + * [JSchema 驱动表单渲染](#jschema-%E9%A9%B1%E5%8A%A8%E8%A1%A8%E5%8D%95%E6%B8%B2%E6%9F%93) + * [非单例注册组件](#%E9%9D%9E%E5%8D%95%E4%BE%8B%E6%B3%A8%E5%86%8C%E7%BB%84%E4%BB%B6) +- [高级教程](#%E9%AB%98%E7%BA%A7%E6%95%99%E7%A8%8B) + * [如何接入第三方组件库?](#%E5%A6%82%E4%BD%95%E6%8E%A5%E5%85%A5%E7%AC%AC%E4%B8%89%E6%96%B9%E7%BB%84%E4%BB%B6%E5%BA%93) + * [如何接入 Form/FormItem 组件?](#%E5%A6%82%E4%BD%95%E6%8E%A5%E5%85%A5-formformitem-%E7%BB%84%E4%BB%B6) + * [如何接入表单组件?](#%E5%A6%82%E4%BD%95%E6%8E%A5%E5%85%A5%E8%A1%A8%E5%8D%95%E7%BB%84%E4%BB%B6) + * [如何处理表单布局?](#%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86%E8%A1%A8%E5%8D%95%E5%B8%83%E5%B1%80) + * [如何实现递归渲染组件?](#%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E9%80%92%E5%BD%92%E6%B8%B2%E6%9F%93%E7%BB%84%E4%BB%B6) + * [如何实现自增列表组件?](#%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%A2%9E%E5%88%97%E8%A1%A8%E7%BB%84%E4%BB%B6) + * [如何实现超复杂自定义组件?](#%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E8%B6%85%E5%A4%8D%E6%9D%82%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%84%E4%BB%B6) +- [API](#api) + * [connect](#connect) + * [registerFormField](#registerformfield) + * [registerFormFields](#registerformfields) + * [registerFormComponent](#registerformcomponent) + * [registerFormItemComponent](#registerformitemcomponent) + * [registerVirtualBox](#registervirtualbox) + * [createVirtualBox](#createvirtualbox) + * [createControllerBox](#createcontrollerbox) + * [getRegistry](#getregistry) + * [cleanRegistry](#cleanregistry) +- [Classes](#classes) + * [new Schema(json : ISchema)](#new-schemajson--ischema) + + [`get`](#get) + + [`merge `](#merge-) + + [`getEmptyValue`](#getemptyvalue) + + [`getSelfProps`](#getselfprops) + + [`getExtendsRules`](#getextendsrules) + + [`getExtendsRequired`](#getextendsrequired) + + [`getExtendsEditable`](#getextendseditable) + + [`getExtendsTriggerType`](#getextendstriggertype) + + [`getExtendsProps`](#getextendsprops) + + [`getExtendsComponent`](#getextendscomponent) + + [`getExtendsComponentProps`](#getextendscomponentprops) + + [`getExtendsRenderer`](#getextendsrenderer) + + [`getExtendsEffect`](#getextendseffect) + + [`setProperty`](#setproperty) + + [`setProperties`](#setproperties) + + [`setArrayItems`](#setarrayitems) + + [`getOrderProperties`](#getorderproperties) + + [`mapProperties`](#mapproperties) + + [`toJSON`](#tojson) + + [`fromJSON`](#fromjson) + + [`isObject`](#isobject) + + [`isArray()`](#isarray) +- [Components](#components) + - [``](#SchemaForm) + - [``](#SchemaField) + - [``](#SchemaMarkupForm) + - [``](#SchemaMarkupField) + - [``](#InternalForm) + - [``](#InternalField) +- [Interfaces](#interfaces) + * [ISchemaFieldComponentProps](#ischemafieldcomponentprops) + * [ISchemaVirtualFieldComponentProps](#ischemavirtualfieldcomponentprops) + * [ISchemaFormRegistry](#ischemaformregistry) + * [ISchema](#ischema) + * [ISchemaFormActions](#ischemaformactions) + * [ISchemaFormAsyncActions](#ischemaformasyncactions) + * [IFormValidateResult](#iformvalidateresult) + * [InternalFormats](#internalformats) + * [ValidateDescription](#validatedescription) + * [ValidateResponse](#validateresponse) + * [CustomValidator](#customvalidator) + * [ValidatePatternRules](#validatepatternrules) + + + ### 使用方式 + --- #### 快速开始 -如果您是直接基于@uform/react-schema-renderer做开发的,那么您必须在开发前将自定义组件注册到渲染器里去,否则,我们的JSON-Schema协议是不能渲染表单的。所以: +如果您是直接基于@uform/react-schema-renderer 做开发的,那么您必须在开发前将自定义组件注册到渲染器里去,否则,我们的 JSON-Schema 协议是不能渲染表单的。所以: ```jsx -import { SchemaForm, registerFormField,connect } from '@uform/react-schema-renderer' +import React from 'react' +import { + SchemaForm, + registerFormField, + connect +} from '@uform/react-schema-renderer' -registerFormField('string',connect()(({value,onChange})=>{ - return -})) +registerFormField( + 'string', + connect()(({ value, onChange }) => { + return + }) +) -export default ()=>{ +export default () => { return ( - ) } ``` -大工告成,这个就是最简单的用法,核心就是注册组件,然后使用协议渲染。**需要注意一点是,我们在注册组件的时候使用了connect函数,这个connect函数的功能就是,让任意一个组件,只要支持value/onChange API的,都可以快速注册到SchemaForm里面去,同时,connect函数也屏蔽了Field API,所以使用了connect函数的组件,是不能做更加强大的扩展的,详细的connect API后面会有介绍**。同时,还有一个要注意的就是,如果我们要接入一套组件库的,业内大多数组件库其实都是有自己的Form和FormItem组件的,他们核心是用于控制样式,FormItem控制表单局部样式,Form控制全局表单样式,所以在生产环境下,其实我们还需要注册Form和FormItem组件,这样才能做到样式与原有解决方案的一致性,具体如何注册,我们会在后面有详细介绍。 +大工告成,这个就是最简单的用法,核心就是注册组件,然后使用协议渲染。**需要注意一点是,我们在注册组件的时候使用了 connect 函数,这个 connect 函数的功能就是,让任意一个组件,只要支持 value/onChange API 的,都可以快速注册到 SchemaForm 里面去,同时,connect 函数也屏蔽了 Field API,所以使用了 connect 函数的组件,是不能做更加强大的扩展的,详细的 connect API 后面会有介绍**。同时,还有一个要注意的就是,如果我们要接入一套组件库的,业内大多数组件库其实都是有自己的 Form 和 FormItem 组件的,他们核心是用于控制样式,FormItem 控制表单局部样式,Form 控制全局表单样式,所以在生产环境下,其实我们还需要注册 Form 和 FormItem 组件,这样才能做到样式与原有解决方案的一致性,具体如何注册,我们会在后面有详细介绍。 -#### JSON Schema驱动表单渲染 +#### JSON Schema 驱动表单渲染 -说到JSON Schema,上面一个例子其实已经涉及了,当然,它并不够复杂,我们看一个较为复杂的例子: +说到 JSON Schema,上面一个例子其实已经涉及了,当然,它并不够复杂,我们看一个较为复杂的例子: ```tsx +import React from 'react' import { SchemaForm } from '@uform/react-schema-renderer' -registerFormField('string',connect()(({value,onChange})=>{ - return -})) +registerFormField( + 'string', + connect()(({ value, onChange }) => { + return + }) +) -registerFormField('array',()=>{ +registerFormField('array', () => { return //... }) -export default ()=>{ +export default () => { return ( - + /> ) } ``` -上面的代码是一段伪代码,因为我们并没有注册array类型的自定义组件,这里先暂时不讲如何注册array类型的自定义组件,我们核心是分析JSON Schema是如何驱动表单渲染的。在这份JSON Schema中,我们主要使用了properties和items属性用来描述复杂数据结构,这就是JSON Schema最核心的特性,注意:**在SchemaForm中,内置了object的properties的递归渲染,但是并没有内置array的items递归渲染**,主要原因是,array的递归渲染会涉及很多样式需求,并不方便内置,所以最好还是留给开发者自己实现,所以,后面我们会详细介绍如何实现自增列表的递归渲染需求。 +上面的代码是一段伪代码,因为我们并没有注册 array 类型的自定义组件,这里先暂时不讲如何注册 array 类型的自定义组件,我们核心是分析 JSON Schema 是如何驱动表单渲染的。在这份 JSON Schema 中,我们主要使用了 properties 和 items 属性用来描述复杂数据结构,这就是 JSON Schema 最核心的特性,注意:**在 SchemaForm 中,内置了 object 的 properties 的递归渲染,但是并没有内置 array 的 items 递归渲染**,主要原因是,array 的递归渲染会涉及很多样式需求,并不方便内置,所以最好还是留给开发者自己实现,所以,后面我们会详细介绍如何实现自增列表的递归渲染需求。 -#### JSchema驱动表单渲染 +#### JSchema 驱动表单渲染 -JSchema就是在jsx中以一种更优雅的写法来描述JSON Schema,我们可以针对以上例子用JSchema实现一版: +JSchema 就是在 jsx 中以一种更优雅的写法来描述 JSON Schema,我们可以针对以上例子用 JSchema 实现一版: ```tsx -import { SchemaForm,Field } from '@uform/react-schema-renderer' +import React from 'react' +import { + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field +} from '@uform/react-schema-renderer' -export default ()=>{ +export default () => { return ( - - - - - - + > + + + + + ) } ``` -可以看到,使用JSchema在代码中描述JSON Schema比起JSON而言变得更加优雅了,大大的提高了代码可维护性。 +可以看到,使用 JSchema 在代码中描述 JSON Schema 比起 JSON 而言变得更加优雅了,大大的提高了代码可维护性。 #### 非单例注册组件 -在前面的例子中,我们使用了registerFormField API来注册了自定义组件,这种方式是单例注册的方式,它的主要优点就是方便,但是也会存在一些问题,就是单例容易受污染,特别是在SPA页面中,如果不同页面的开发者是不一样的,因为共享同一个内存环境,那么A开发者可能会注册B开发者同名的自定义组件,这样就很容易导致线上故障,所以,我们更加推荐用户使用非单例注册方式: +在前面的例子中,我们使用了 registerFormField API 来注册了自定义组件,这种方式是单例注册的方式,它的主要优点就是方便,但是也会存在一些问题,就是单例容易受污染,特别是在 SPA 页面中,如果不同页面的开发者是不一样的,因为共享同一个内存环境,那么 A 开发者可能会注册 B 开发者同名的自定义组件,这样就很容易导致线上故障,所以,我们更加推荐用户使用非单例注册方式: ```jsx -import React,{ useMemo } from 'react' -import { SchemaForm, registerFormField,connect } from '@uform/react-schema-renderer' +import React, { useMemo } from 'react' +import { + SchemaForm, + registerFormField, + connect +} from '@uform/react-schema-renderer' -const StringField = connect()(({value,onChange})=>{ +const StringField = connect()(({ value, onChange }) => { return }) -const useFields = ()=>useMemo(()=>{ - string:StringField -}) +const useFields = () => + useMemo(() => { + string: StringField + }) -export default ()=>{ +export default () => { return ( - ) } ``` -在上面的例子中,我们主要是在SchemaForm的props维度来传递自定义组件,这样就能保证是实例级注册了,这样的形式对SPA非常友好,同时,**需要注意的是我们抽象了一个useFields的React Hook,它主要用于解决组件多次渲染的时候不会影响React Virtual DOM重新计算,从而避免表单组件重复渲染**。 +在上面的例子中,我们主要是在 SchemaForm 的 props 维度来传递自定义组件,这样就能保证是实例级注册了,这样的形式对 SPA 非常友好,同时,**需要注意的是我们抽象了一个 useFields 的 React Hook,它主要用于解决组件多次渲染的时候不会影响 React Virtual DOM 重新计算,从而避免表单组件重复渲染**。 ### 高级教程 + --- #### 如何接入第三方组件库? -因为@uform/react-schema-renderer是一个基础库,默认不会集成任何组件库的,所以我们在实际业务开发中,如果要基于它来定制,那么就必须得面对接入第三方组件库的问题。如何接入第三方组件库,我们分为以下几步: +因为@uform/react-schema-renderer 是一个基础库,默认不会集成任何组件库的,所以我们在实际业务开发中,如果要基于它来定制,那么就必须得面对接入第三方组件库的问题。如何接入第三方组件库,我们分为以下几步: -- 接入Form/FormItem组件 +- 接入 Form/FormItem 组件 - 接入组件库表单组件 - 实现表单布局组件 - 实现自增列表组件 -下面就让我们一步步的来接入第三方组件库吧!这里我们主要以antd组件库为例子。 +下面就让我们一步步的来接入第三方组件库吧!这里我们主要以 antd 组件库为例子。 -#### 如何接入Form/FormItem组件? +#### 如何接入 Form/FormItem 组件? -接入方式目前提供了全局注册机制与单例注册机制,全局注册主要使用registerFormComponent和registerFormItemComponent两个API来注册,单例注册则是直接在SchemaForm属性上传formComponent和formItemComponent。如果是SPA场景,推荐使用单例注册的方式,下面看看例子: +接入方式目前提供了全局注册机制与单例注册机制,全局注册主要使用 registerFormComponent 和 registerFormItemComponent 两个 API 来注册,单例注册则是直接在 SchemaForm 属性上传 formComponent 和 formItemComponent。如果是 SPA 场景,推荐使用单例注册的方式,下面看看例子: ```tsx +import React from 'react' import { SchemaForm, registerFormComponent, @@ -194,14 +288,14 @@ export const CompatFormItemComponent = ({children,...props})=>{ status = 'warning' } return ( - {children} - + ) } @@ -214,7 +308,7 @@ registerFormItemComponent(CompatFormItemComponent) //单例注册方式 export default ()=>{ return ( - @@ -222,19 +316,20 @@ export default ()=>{ ``` -我们可以看到,扩展表单整体或局部的样式,仅仅只需要通过扩展Form/FormItem组件就可以轻松解决了,这里需要注意的是,FormItem组件接收到的props有点复杂,不用担心,后面会列出详细props API,现在我们只需要知道大概是如何注册的就行了。 +我们可以看到,扩展表单整体或局部的样式,仅仅只需要通过扩展 Form/FormItem 组件就可以轻松解决了,这里需要注意的是,FormItem 组件接收到的 props 有点复杂,不用担心,后面会列出详细 props API,现在我们只需要知道大概是如何注册的就行了。 #### 如何接入表单组件? -因为组件库的所有组件都是原子型组件,同时大部分都兼容了value/onChange规范,所以我们可以借助connect函数快速接入组件库的组件,通常,我们接入组件库组件,大概要做3件事情: +因为组件库的所有组件都是原子型组件,同时大部分都兼容了 value/onChange 规范,所以我们可以借助 connect 函数快速接入组件库的组件,通常,我们接入组件库组件,大概要做 3 件事情: -- 处理状态映射,将uform内部的loading/error状态映射到该组件属性上,当然,**前提是要求组件必须支持loading或error这类的样式** -- 处理详情态样式,将uform内部的editable状态,映射到一个PreviewText组件上去,用于更友好更干净的展示数据 -- 处理组件枚举态,我们想一下,**JSON Schema,每一个节点都应该支持enum属性的**,如果配了enum属性,我们最好都以Select形式来展现,所以我们需要处理一下组件枚举态 +- 处理状态映射,将 uform 内部的 loading/error 状态映射到该组件属性上,当然,**前提是要求组件必须支持 loading 或 error 这类的样式** +- 处理详情态样式,将 uform 内部的 editable 状态,映射到一个 PreviewText 组件上去,用于更友好更干净的展示数据 +- 处理组件枚举态,我们想一下,**JSON Schema,每一个节点都应该支持 enum 属性的**,如果配了 enum 属性,我们最好都以 Select 形式来展现,所以我们需要处理一下组件枚举态 -咱们以InputNumber为例演示一下: +咱们以 InputNumber 为例演示一下: ```tsx +import React from 'react' import { connect, registerFormField } from '@uform/react-schema-renderer' import { InputNumber } from 'antd' @@ -280,90 +375,94 @@ const acceptEnum = (component: React.JSXElementConstructor) => { registerFormField( 'number', connect({ - getProps: mapStyledProps,//处理状态映射 - getComponent: mapTextComponent//处理详情态 - })(acceptEnum(InputNumber))//处理枚举态 + getProps: mapStyledProps, //处理状态映射 + getComponent: mapTextComponent //处理详情态 + })(acceptEnum(InputNumber)) //处理枚举态 ) - ``` -在这个例子中,我们深度使用了connect函数,其实connect就是一个HOC,在渲染阶段,它可以在组件渲染过程中加入一些中间处理逻辑,帮助动态分发。当然,connect还有很多API,后面会详细介绍。 +在这个例子中,我们深度使用了 connect 函数,其实 connect 就是一个 HOC,在渲染阶段,它可以在组件渲染过程中加入一些中间处理逻辑,帮助动态分发。当然,connect 还有很多 API,后面会详细介绍。 #### 如何处理表单布局? -JSON Schema描述表单数据结构,其实是天然支持的,但是表单最终还是落在UI层面的,可惜在UI层面上我们有很多组件其实并不能作为JSON Schema的一个具体数据节点,它仅仅只是一个UI节点。所以,想要在JSON Schema中描述复杂布局,怎么做? +JSON Schema 描述表单数据结构,其实是天然支持的,但是表单最终还是落在 UI 层面的,可惜在 UI 层面上我们有很多组件其实并不能作为 JSON Schema 的一个具体数据节点,它仅仅只是一个 UI 节点。所以,想要在 JSON Schema 中描述复杂布局,怎么做? -现在uform的做法是,抽象了一个叫**虚拟节点**的概念,用户在代码层面上指定某个JSON Schema x-component为虚拟节点之后,后面不管是在渲染,还是在数据处理,还是最终数据提交,只要解析到这个节点是虚拟节点,都不会将它当做一个正常的数据节点。所以,有了这个虚拟节点的概念,我们就可以在JSON Schema中描述各种复杂布局,下面让我们试着写一个布局组件: +现在 uform 的做法是,抽象了一个叫**虚拟节点**的概念,用户在代码层面上指定某个 JSON Schema x-component 为虚拟节点之后,后面不管是在渲染,还是在数据处理,还是最终数据提交,只要解析到这个节点是虚拟节点,都不会将它当做一个正常的数据节点。所以,有了这个虚拟节点的概念,我们就可以在 JSON Schema 中描述各种复杂布局,下面让我们试着写一个布局组件: ```tsx -import { SchemaForm,registerVirtualBox } from '@uform/react-schema-renderer' +import React from 'react' +import { SchemaForm, registerVirtualBox } from '@uform/react-schema-renderer' import { Card } from 'antd' -registerVirtualBox('card',({children,...props})=>{ - return {children} +registerVirtualBox('card', ({ children, ...props }) => { + return {children} }) -export default ()=>{ +export default () => { return ( - ) } ``` -从这段伪代码中我们可以看到card就是一个正常的Object Schema节点,只是需要指定一个x-component为card,这样就能和registerVirtualBox注册的card匹配上,就达到了虚拟节点的效果,所以,不管你把JSON Schema中的属性名改为什么,都不会影响最终的提交的数据结构。**这里需要注意的是x-component-props是直接透传到registerVirtualBox的回调函数参数上的。** 这是JSON Schema形式的使用,我们还有JSchema的使用方式: +从这段伪代码中我们可以看到 card 就是一个正常的 Object Schema 节点,只是需要指定一个 x-component 为 card,这样就能和 registerVirtualBox 注册的 card 匹配上,就达到了虚拟节点的效果,所以,不管你把 JSON Schema 中的属性名改为什么,都不会影响最终的提交的数据结构。**这里需要注意的是 x-component-props 是直接透传到 registerVirtualBox 的回调函数参数上的。** 这是 JSON Schema 形式的使用,我们还有 JSchema 的使用方式: ```tsx -import { SchemaForm,createVirtualBox } from '@uform/react-schema-renderer' +import React from 'react' +import { SchemaForm, createVirtualBox } from '@uform/react-schema-renderer' import { Card } from 'antd' -const Card = createVirtualBox('card',({children,...props})=>{ - return {children} +const Card = createVirtualBox('card', ({ children, ...props }) => { + return {children} }) -export default ()=>{ +export default () => { return ( - - - - - - - + + + + + + + ) } ``` -从这个例子中我们可以看到,借助createVirtualBox API可以快速创建一个布局组件,同时在JSchema中直接使用。**其实createVirtualBox的内部实现很简单,还是使用了registerVitualBox和Field**: +从这个例子中我们可以看到,借助 createVirtualBox API 可以快速创建一个布局组件,同时在 JSchema 中直接使用。**其实 createVirtualBox 的内部实现很简单,还是使用了 registerVitualBox 和 SchemaMarkupField**: ```tsx +import React from 'react' +import { SchemaMarkupField as Field } from '@uform/react-schema-renderer' export function createVirtualBox( key: string, component?: React.JSXElementConstructor> @@ -371,7 +470,7 @@ export function createVirtualBox( registerVirtualBox( key, component - ? ({ props,schema, children }) => { + ? ({ props, schema, children }) => { return React.createElement(component, { ...schema.getExtendsComponentProps(), children @@ -403,14 +502,15 @@ export function createVirtualBox( 前面介绍的注册布局组件的方式,其实都是单例注册,如果我们需要实例形式的注册,还是与前面说的方式类似 ```tsx - +import React from 'react' +import { Card as AntdCard } from 'antd' const Card = ({children,...props})=>{ - return {children} + return {children} } export default ()=>{ return ( - { } ``` - - #### 如何实现递归渲染组件? -什么叫递归渲染组件?其实就是**实现了JSON Schema中properties和items的组件**,像`type:"string"` 这种节点就属于原子节点,不属于递归渲染组件。其实像前面说的布局组件,其实它也是属于递归渲染组件,只是它固定了渲染模式,所以可以很简单的注册。所以,我们大部分想要实现递归渲染的场景,可能实际业务场景中,更多的是在`type:"array"`这种场景才会去实现递归渲染,下面我们会详细介绍自增列表组件的实现方式。 +什么叫递归渲染组件?其实就是**实现了 JSON Schema 中 properties 和 items 的组件**,像`type:"string"` 这种节点就属于原子节点,不属于递归渲染组件。其实像前面说的布局组件,其实它也是属于递归渲染组件,只是它固定了渲染模式,所以可以很简单的注册。所以,我们大部分想要实现递归渲染的场景,可能更多的是在`type:"array"`这种场景才会去实现递归渲染,下面我们会详细介绍自增列表组件的实现方式。 #### 如何实现自增列表组件? @@ -457,7 +555,7 @@ export default ()=>{ - 有独立样式 - 支持递归渲染子组件 - 支持数组项的新增,删除,上移,下移 -- 不能使用connect函数包装,因为必须调用Field API +- 不能使用 connect 函数包装,因为必须调用 Field API 为了帮助大家更好的理解如何实现自增列表组件,我们就不实现具体样式了,更多的是教大家如何实现递归渲染和,数组项的操作。下面我们看伪代码: @@ -470,81 +568,1394 @@ import { } from '@uform/react-schema-renderer' //不用connect包装 -registerFormField('array',({value,path,mutators})=>{ - - const emptyUI = - - const listUI = value.map((item,index)=>{ + }} + > + Add Element + + ) + + const listUI = value.map((item, index) => { return ( -
    - - - + - + + }} + > + move up +
    ) }) - - return ( - value.length == 0 ? emptyUI : listUI - ) + + return value.length == 0 ? emptyUI : listUI }) ``` -看到了没,要实现一个带递归渲染的自增列表组件,超级简单,反而如果要实现相关的样式就会有点麻烦,总之核心就是使用了SchemaField这个组件和mutators API,具体API会在后面详细介绍。 +看到了没,要实现一个带递归渲染的自增列表组件,超级简单,反而如果要实现相关的样式就会有点麻烦,总之核心就是使用了 SchemaField 这个组件和 mutators API,具体 API 会在后面详细介绍。 #### 如何实现超复杂自定义组件? -这个问题,在老版UForm中基本无解,恰好也是因为我们这边的业务复杂度高到一定程度之后,我们自己被这个问题给受限制了,所以必须得想办法解决这个问题,下面我们可以定义一下,什么才是超复杂自定义组件: +这个问题,在老版 UForm 中基本无解,恰好也是因为我们这边的业务复杂度高到一定程度之后,我们自己被这个问题给受限制了,所以必须得想办法解决这个问题,下面我们可以定义一下,什么才是超复杂自定义组件: - 组件内部存在大量表单组件,同时内部也存在大量联动关系 - 组件内部存在私有的服务端动态渲染方案 - 组件内部有复杂布局结构 -就这3点,基本上满足超复杂自定义组件的特征了,对于这种场景,为什么我们通过正常的封装自定义组件的形式不能解决问题呢?其实主要是受限于校验,没法整体校验,所以,我们需要一个能聚合大量字段处理逻辑的能力,下面我们来看看具体方案: +就这 3 点,基本上满足超复杂自定义组件的特征了,对于这种场景,为什么我们通过正常的封装自定义组件的形式不能解决问题呢?其实主要是受限于校验,没法整体校验,所以,我们需要一个能聚合大量字段处理逻辑的能力,下面我们来看看具体方案: ```tsx +import React, { Fragment } from 'react' +import { + registerFormField, + SchemaField, + FormPath, + InternalField, + useFormEffects, + FormEffectHooks +} from '@uform/react-schema-renderer' +import { Input, Form } from 'antd' + +const FormItem = ({ component, ...props }) => { + return ( + + {({ state, mutators }) => { + const messages = [].concat(state.errors || [], state.warnings || []) + let status = '' + if (state.loading) { + status = 'validating' + } + if (state.invalid) { + status = 'error' + } + if (state.warnings && state.warnings.length) { + status = 'warning' + } + return ( + + {React.createElement(component, { + ...state.props, + value: state.value, + onChange: mutators.change, + onBlur: mutators.blur, + onFocus: mutators.focus + })} + + ) + }} + + ) +} + +//不用connect包装 +registerFormField('complex', ({ path }) => { + useFormEffects(({ setFieldState }) => { + FormEffectHooks.onFieldValueChange$('ccc').subscribe(({ value }) => { + if (value === '123') { + setFieldState('ddd', state => { + state.value = 'this is linkage relationship' + }) + } + }) + }) + return ( + <> + + + + + + ) +}) ``` +在这段伪代码中,我们主要使用了两个核心 API,主要是 useFormEffects 和 InternalField,useFormEffects 给开发者提供了局部写 effects 逻辑的地方这样就能很方便的复用 effects 逻辑,InternalField 则就是@uform/react 的 Field 组件,这个可以具体看看@uform/react 的文档,因为 SchemaForm 内部也是使用的@uform/react,所以可以共享同一个 Context,所以我们就能很方便的在自定义组件内使用 InternalField,同时需要注意一点,**直接使用 InternalField 的时候,我们注册的 name 是根级别的 name,如果想要复用当前自定义组件的路径,可以借助 FormPath 解析路径,然后再 concat 即可。** +### API -### 协议 --- -#### 理念 +> 整体 API 完全继承@uform/core 与@uform/react,下面只列举@uform/react-schema-renderer 的特有 API -#### 架构 +#### connect -#### 数据结构协议 +> 自定义组件注册桥接器,这是一个高阶组件函数(HOC),主要用于快速接入大多数组件库组件(实现了 value/onChange API 的组件) -#### 布局协议 +**签名** + +```typescript +connect(options?: IConnectOptions): (component : React.JSXElementConstructor)=>(fieldProps:ISchemaFieldComponentProps)=>JSX.Element +``` -#### 顺序协议 +**用法** -#### 校验协议 +```typescript +import { registerFormField, connect } from '@uform/react-schema-renderer' +import { Select } from 'antd' +registerFormField('select', connect()(Select)) +``` -#### UI协议 -#### 逻辑协议 -### API ---- +#### registerFormField -### Components ---- +> 注册自定义组件函数 +**签名** -### Interfaces ---- +```typescript +registerFormField( + name:string, + component: React.JSXElementConstructor +) +``` + + + +#### registerFormFields + +> 批量注册自定义组件 + +**签名** + +```typescript +registerFormFields( + fieldsMap: { + [key : string]: React.JSXElementConstructor + } +) +``` + + + +#### registerFormComponent + +> 注册 Form 样式组件 + +**签名** +```typescript +registerFormComponent( + component:React.JSXElementConstructor +) +``` + + + +#### registerFormItemComponent + +> 注册 FormItem 样式组件 + +**签名** + +```typescript +registerFormItemComponent( + component:React.JSXElementConstructor +) +``` + + + +#### registerVirtualBox + +> 注册虚拟盒子组件,主要用于在 JSON Schema 中描述布局 + +**签名** + +```typescript +registerVirtualBox( + name:string, + component:React.JSXElementConstructor +) +``` + + + +#### createVirtualBox + +> 创建一个虚拟盒子组件,返回的组件可以在 SchemaMarkupForm 中直接使用 + +**签名** + +```typescript +createVirtualBox( + name:string, + component:React.JSXElementConstructor +) : React.FC +``` + +**用法** + +```tsx +import React from 'react' +import { + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, + createVirtualBox +} from '@uform/react-schema-renderer' +import { Card } from 'antd' + +const FormCard = createVirtualBox('card', props => { + return +}) + +export default () => ( + + + + + +) +``` + + + +#### createControllerBox + +> 创建一个虚拟盒子组件,返回的组件可以在 SchemaMarkupForm 中直接使用,它与 createVirtualBox 的不同主要是组件接收的 props +> createVirtualBox 接收的 props 就是最简单的组件 props +> createControllerBox 接收的是`ISchemaVirtualFieldComponentProps` + +**签名** + +```typescript +createControllerBox( + name:string, + component:React.JSXElementConstructor +) : React.FC +``` + +**用法** + +```tsx +import React from 'react' +import { + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, + createControllerBox +} from '@uform/react-schema-renderer' +import { Card } from 'antd' + +const FormCard = createControllerBox('card', ({ schema, children }) => { + return {children} +}) + +export default () => ( + + + + + +) +``` + + + +#### getRegistry + +> 获取注册中心,所有通过registerFormXXX API 注册的组件都统一在registry中管理 + +**签名** + +```typescript +getRegistry(): ISchemaFormRegistry +``` + + + +#### cleanRegistry + +> 清空注册中心,清除所有通过registerFormXXX API注册的组件 + +**签名** + +```typescript +cleanRegistry(): void +``` + + + +### Classes + +> 整体 Class 完全继承@uform/core,比如 FormPath 与 FormLifeCyle,下面只列举@uform/react-schema-renderer 特有的 Class + +#### new Schema(json : ISchema) + +> Schema解析引擎,给定一份满足JSON Schema的数据,我们会将其解析成对应的Schema实例,可以借助一些工具方法快速处理一些事情,同时该Schema Class提供了统一的协议差异抹平能力,保证协议升级的时候可以无缝平滑升级 + +**属性** + +| 属性名 | 描述 | 类型 | +| -------------------- | ------------------------------------ | ------------------------------------------------------------ | +| title | 字段标题 | `React.ReactNode` | +| description | 字段描述 | `React.ReactNode` | +| default | 字段默认值 | `any` | +| readOnly | 是否只读与editable一致 | `boolean` | +| type | 字段类型 | `'string' | 'object' | 'array' | 'number' | string` | +| enum | 枚举数据 | `Array` | +| const | 校验字段值是否与const的值相等 | `any` | +| multipleOf | 校验字段值是否可被multipleOf的值整除 | `number` | +| maximum | 校验最大值(大于) | `number` | +| exclusiveMaximum | 校验最大值(大于等于) | `number` | +| minimum | 校验最小值(小于) | `number` | +| exclusiveMinimum | 最小值(小于等于) | `number` | +| maxLength | 校验最大长度 | `number` | +| minLength | 校验最小长度 | `number` | +| pattern | 正则校验规则 | `string | RegExp` | +| maxItems | 最大条目数 | `number` | +| minItems | 最小条目数 | `number` | +| uniqueItems | 是否校验重复 | `boolean` | +| maxProperties | 最大属性数量 | `number` | +| minProperties | 最小属性数量 | `number` | +| required | 必填 | `boolean` | +| format | 正则规则类型 | `InternalFormats` | +| properties | 对象属性 | `{[key : string]:Schema}` | +| items | 数组描述 | `Schema | Schema[]` | +| additionalItems | 额外数组元素描述 | `Schema` | +| patternProperties | 动态匹配对象的某个属性的Schema | `{[key : string]:Schema}` | +| additionalProperties | 匹配对象额外属性的Schema | `Schema` | +| editable | 字段是否可编辑 | `boolean` | +| x-props | 字段扩展属性 | `{ [name: string]: any }` | +| x-index | 字段顺序 | `number` | +| x-rules | 字段校验规则 | `ValidatePatternRules` | +| x-component | 字段UI组件 | `string` | +| x-component-props | 字段UI组件属性 | `{}` | +| x-render | 字段扩展渲染函数 | `(props: T & { renderComponent: () => React.ReactElement}) => React.ReactElement` | +| x-effect | 字段副作用触发器 | `(dispatch: (type: string, payload: any) => void,option?:object) => { [key: string]: any }` | + + + +**方法** + +##### `get` + +> 根据数据路径获取Schema子节点 + +签名 + +```typescript +get(path?: FormPathPattern): Schema +``` + +用法 + +```typescript +const schema = new Schema({ + type:"object", + properties:{ + array:{ + type:'array', + items:{ + type:'object', + properties:{ + input:{ + type:'string' + } + } + } + } + } +}) + +schema.get('array[0].input') //{type:"string"} +``` + + + +##### `merge ` + +> 合并Schema + +签名 + +```typescript +merge(spec:ISchema): Schema +``` + +用法 + +```typescript +const schema = new Schema({ + type:"object", + properties:{ + array:{ + type:'array', + items:{ + type:'object', + properties:{ + input:{ + type:'string' + } + } + } + } + } +}) + +schema.merge({ + title:'root object' +}) +/** +{ + type:"object", + title:"root object", + properties:{ + array:{ + type:'array', + items:{ + type:'object', + properties:{ + input:{ + type:'string' + } + } + } + } + } +} +**/ +``` + + + +##### `getEmptyValue` + +> 基于Schema的type获取当前Schema的空值 + +签名 + +```typescript +getEmptyValue() : '' | [] | {} | 0 +``` + +用法 + +```typescript +const schema = new Schema({ + type:"object", + properties:{ + array:{ + type:'array', + items:{ + type:'object', + properties:{ + input:{ + type:'string' + } + } + } + } + } +}) + +schema.get('array.0.input').getEmptyValue() // '' +schema.get('array.0').getEmptyValue() // {} +schema.get('array').getEmptyValue() // [] +schema.getEmptyValue() // {} + +``` + + + +##### `getSelfProps` + +> 获取无嵌套Schema属性(不会包含properties/items这类嵌套属性) + +签名 + +```typescript +getSelfProps() : ISchema +``` + +用法 + +```typescript +const schema = new Schema({ + type:"object", + properties:{ + array:{ + type:'array', + items:{ + type:'object', + properties:{ + input:{ + type:'string' + } + } + } + } + } +}) + +schema.getSelfProps() // { type:"object" } + +``` + + + +##### `getExtendsRules` + +> 获取扩展校验规则,该方法比较复杂,会解析当前Schema的所有校验类型的属性与x-rules属性,最终合并为一个统一的rules结构 + +签名 + +```typescript +getExtendsRules() : ValidateArrayRules +``` + +用法 + +```typescript +const schema = new Schema({ + type:"string", + required:true, + maxLength:10 + "x-rules":{ + pattern:/^\d+$/ + } +}) + +schema.getExtendsRules() // [{required:true},{max:10},{pattern:/^\d+$/}] +``` + + + +##### `getExtendsRequired` + +> 获取是否必填,其实就是读取Schema的required属性,为什么封装成方法,是为了保证协议升级的时候对用户无感知,我们只需要保证方法的向后兼容即可 + +签名 + +```typescript +getExtendsRequired(): void | boolean +``` + +用法 + +```typescript +const schema = new Schema({ + type:"string", + required:true, + maxLength:10 + "x-rules":{ + pattern:/^\d+$/ + } +}) + +schema.getExtendsRequired() // true +``` + + + +##### `getExtendsEditable` + +> 获取Schema的editable状态,与getExtendsEditable能力一致,也是为了抹平协议差异 + +签名 + +```typescript +getExtendsEditable() : void | boolean +``` + +用法 + +```typescript +const schema1 = new Schema({ + type:"string", + editable:false +}) + +schema1.getExtendsEditable() // false + +const schema2 = new Schema({ + type:"string", + readOnly:true +}) + +schema2.getExtendsEditable() // false + +const schema3 = new Schema({ + type:"string", + "x-props":{ + editable:false + } +}) + +schema3.getExtendsEditable() // false + +const schema4 = new Schema({ + type:"string", + "x-component-props":{ + editable:false + } +}) + +schema4.getExtendsEditable() // false +``` + + + +##### `getExtendsTriggerType` + +> 获取triggerType,与getExtendsTriggerType能力一致,都是提供协议差异抹平的能力 + +签名 + +```typescript +getExtendsTriggerType() : 'onBlur' | 'onChange' | string +``` + +用法 + +```typescript +const schema1 = new Schema({ + type:"string", + "x-props":{ + triggerType:"onBlur" + } +}) + +schema1.getExtendsTriggerType() // onBlur + +const schema2 = new Schema({ + type:"string", + "x-component-props":{ + triggerType:"onBlur" + } +}) + +schema2.getExtendsTriggerType() // onBlur + +const schema3 = new Schema({ + type:"string", + "x-item-props":{ + triggerType:"onBlur" + } +}) + +schema3.getExtendsTriggerType() // onBlur +``` + +##### `getExtendsProps` + +> 获取x-props属性 + +签名 + +```typescript +getExtendsProps() : {} +``` + + + +##### `getExtendsComponent` + +> 获取x-component属性 + +签名 + +```typescript +getExtendsComponent() : string +``` + + + +##### `getExtendsComponentProps` + +> 获取x-component-props属性,也就是x-component的组件属性 + +签名 + +```typescript +getExtendsComponentProps() : {} +``` + + + +##### `getExtendsRenderer` + +> 获取x-render属性 + +签名 + +```typescript +getExtendsRenderer() : ( + props: T & { + renderComponent: () => React.ReactElement + } + ) => React.ReactElement +``` + + + +##### `getExtendsEffect` + +> 获取x-effect属性 + +签名 + +```typescript +getExtendsEffect() : ( + dispatch: (type: string, payload: any) => void, + option?: object +) => { [key: string]: any } +``` + + + +##### `setProperty` + +> 给当前Schema设置properties + +签名 + +```typescript +setProperty(key: string, schema: ISchema): Schema +``` + + + +##### `setProperties` + +> 给当前Schema批量设置properties + +签名 + +```typescript +setProperties(properties: {[key : string]:ISchema}) : {[key : string]:Schema} +``` + + + +##### `setArrayItems` + +> 给当前Schema设置items属性 + +签名 + +```typescript +setArrayItems(schema:Ischema) : Schema +``` + + + +##### `getOrderProperties` + +> 按照x-index顺序给出所有properties + +签名 + +```typescript +getOrderProperties() : {schema:Schema,key:string}[] +``` + + + +##### `mapProperties` + +> 按顺序(x-index)遍历Schema的properties属性 + +签名 + +```typescript +mapProperties(callback?: (schema: Schema, key: string) => any):any[] +``` + + + +##### `toJSON` + +> 输出无循环依赖json数据结构 + +签名 + +``` +toJSON() : ISchema +``` + + + +##### `fromJSON` + +> 基于一份json解析生成Schema对象 + +签名 + +```typescript +fromJSON(json : ISchema) : Schema +``` + + + +##### `isObject` + +> 判断当前Schema是否是object类型 + +签名 + +``` +isObject() : boolean +``` + + + +##### `isArray()` + +> 判断当前Schema是否是array类型 + +签名 + +``` +isArray() : boolean +``` + + + +### Components + +--- + +> 整体组件完全继承@uform/react,下面只列举@uform/react-schema-renderer 特有的组件 + +#### `` + +> 最核心的JSON Schema渲染组件 + +**属性** + +```typescript +interface ISchemaFormProps< + Value = any, + DefaultValue = any, + FormEffectPayload = any, + FormActions = ISchemaFormActions | ISchemaFormAsyncActions +> { + //表单值 + value?: Value; + //表单默认值 + defaultValue?: DefaultValue; + //表单默认值,弱受控 + initialValues?: DefaultValue; + //表单actions + actions?: FormActions; + //表单effects + effects?: IFormEffect; + //form实例 + form?: IForm; + //表单变化回调 + onChange?: (values: Value) => void; + //表单提交回调 + onSubmit?: (values: Value) => void | Promise; + //表单重置回调 + onReset?: () => void; + //表单校验失败回调 + onValidateFailed?: (valideted: IFormValidateResult) => void; + //表单子节点 + children?: React.ReactElement; + //是否开启脏检查 + useDirty?: boolean; + //是否可编辑 + editable?: boolean | ((name: string) => boolean); + //是否开启悲观校验,遇到第一个校验失败,则停止剩余校验 + validateFirst?: boolean; + //Form Schema对象 + schema?: ISchema + //实例级注册自定义组件 + fields?: ISchemaFormRegistry['fields'] + //实例级注册虚拟盒子组件 + virtualFields?: ISchemaFormRegistry['virtualFields'] + //实例级注册Form样式组件 + formComponent?: ISchemaFormRegistry['formComponent'] + //实例级注册FormItem样式组件 + formItemComponent?: ISchemaFormRegistry['formItemComponent'] +} +``` + + + +#### `` + +> 基于一个Data Path,自动寻找Schema节点并渲染的内部组件,主要用于在自定义组件内实现递归渲染 + +**属性** + +```typescript +interface ISchemaFieldProps { + //数据路径 + path?: FormPathPattern +} +``` + + + +#### `` + +> 让SchemaForm支持jsx标签式写法的Form组件,需要配合SchemaMarkupField一起使用 + +**属性** + +```typescript +interface ISchemaFormProps< + Value = any, + DefaultValue = any, + FormEffectPayload = any, + FormActions = ISchemaFormActions | ISchemaFormAsyncActions +> { + //表单值 + value?: Value; + //表单默认值 + defaultValue?: DefaultValue; + //表单默认值,弱受控 + initialValues?: DefaultValue; + //表单actions + actions?: FormActions; + //表单effects + effects?: IFormEffect; + //form实例 + form?: IForm; + //表单变化回调 + onChange?: (values: Value) => void; + //表单提交回调 + onSubmit?: (values: Value) => void | Promise; + //表单重置回调 + onReset?: () => void; + //表单校验失败回调 + onValidateFailed?: (valideted: IFormValidateResult) => void; + //表单子节点 + children?: React.ReactElement; + //是否开启脏检查 + useDirty?: boolean; + //是否可编辑 + editable?: boolean | ((name: string) => boolean); + //是否开启悲观校验,遇到第一个校验失败,则停止剩余校验 + validateFirst?: boolean; + //Form Schema对象 + schema?: ISchema + //实例级注册自定义组件 + fields?: ISchemaFormRegistry['fields'] + //实例级注册虚拟盒子组件 + virtualFields?: ISchemaFormRegistry['virtualFields'] + //实例级注册Form样式组件 + formComponent?: ISchemaFormRegistry['formComponent'] + //实例级注册FormItem样式组件 + formItemComponent?: ISchemaFormRegistry['formItemComponent'] +} +``` + +**用法** + +```tsx +import { + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field +} from '@uform/react-schema-renderer' + +export default ()=>{ + return ( + + + + ) +} +``` + +#### `` + +> 让SchemaForm支持jsx标签式写法的Field组件,需要配合SchemaMarkupForm一起使用 + +**属性** + +```typescript +type IMarkupSchemaFieldProps = ISchema +``` + + + +#### `` + +> 核心Form,与@uform/react中的Form组件一样 + + + +#### `` + +> 核心Field,与@uform/react中的Field组件一样,主要用于复杂自定义组件内使用 + +### Interfaces + +--- + +> 整体继承@uform/react 和@uform/core 的 Interfaces,下面只列举@uform/react-schema-renderer 特有 Interfaces + +#### ISchemaFieldComponentProps + +> 自定义组件所接收的属性,非常重要,只要涉及开发自定义组件,都需要了解该协议 + +```typescript +interface ISchemaFieldComponentProps { + //状态名称,FieldState + displayName?: string + //数据路径 + name: string + //节点路径 + path: string + //是否已经初始化 + initialized: boolean + //是否处于原始态,只有value===intialValues时的时候该状态为true + pristine: boolean + //是否处于合法态,只要errors长度大于0的时候valid为false + valid: boolean + //是否处于非法态,只要errors长度大于0的时候valid为true + invalid: boolean + //是否处于校验态 + validating: boolean + //是否被修改,如果值发生变化,该属性为true,同时在整个字段的生命周期内都会为true + modified: boolean + //是否被触碰 + touched: boolean + //是否被激活,字段触发onFocus事件的时候,它会被触发为true,触发onBlur时,为false + active: boolean + //是否访问过,字段触发onBlur事件的时候,它会被触发为true + visited: boolean + //是否可见,注意:该状态如果为false,那么字段的值不会被提交,同时UI不会显示 + visible: boolean + //是否展示,注意:该状态如果为false,那么字段的值会提交,UI不会展示,类似于表单隐藏域 + display: boolean + //是否可编辑 + editable: boolean + //是否处于loading状态,注意:如果字段处于异步校验时,loading为true + loading: boolean + //字段多参值,比如字段onChange触发时,给事件回调传了多参数据,那么这里会存储所有参数的值 + values: any[] + //字段错误消息 + errors: string[] + //字段告警消息 + warnings: string[] + //字段值,与values[0]是恒定相等 + value: any + //初始值 + initialValue: any + //校验规则,具体类型描述参考后面文档 + rules: ValidatePatternRules[] + //是否必填 + required: boolean + //是否挂载 + mounted: boolean + //是否卸载 + unmounted: boolean + //字段扩展属性,在SchemaForm下就是ISchema结构 + props: ISchema + //当前字段的schema对象 + schema: Schema + //当前字段的数据操作函数集 + mutators: IMutators + //form实例 + form: IForm + //递归渲染函数 + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + + + +#### ISchemaVirtualFieldComponentProps + +> 虚拟字段组件所接收的属性,只要涉及注册虚拟字段的,都需要了解该协议 + +```typescript +interface ISchemaVirtualFieldComponentProps { + //状态名称,VirtualFieldState + displayName: string + //字段数据路径 + name: string + //字段节点路径 + path: string + //是否已经初始化 + initialized: boolean + //是否可见,注意:该状态如果为false,UI不会显示,数据也不会提交(因为它是VirtualField) + visible: boolean + //是否展示,注意:该状态如果为false,UI不会显示,数据也不会提交(因为它是VirtualField) + display: boolean + //是否已挂载 + mounted: boolean + //是否已卸载 + unmounted: boolean + //字段扩展属性 + props: ISchema + //当前字段的schema对象 + schema: Schema + //form实例 + form: IForm + //子元素 + children: React.ReactElement[] + //递归渲染函数 + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + + + +#### ISchemaFormRegistry + +> 组件注册中心,不管是普通字段,还是虚拟字段,还是Form/FormItem都会注册在这里 + +```typescript +interface ISchemaFormRegistry { + fields: { + [key: string]: React.JSXElementConstructor + }, + virtualFields: { + [key: string]: React.JSXElementConstructor + }, + formItemComponent: React.JSXElementConstructor + formComponent: string | React.JSXElementConstructor +} +``` + + + +#### ISchema + +> Schema协议对象,主要用于约束一份json结构满足Schema协议 + +```typescript +interface ISchema { + /** base json schema spec**/ + title?: React.ReactNode + description?: React.ReactNode + default?: any + readOnly?: boolean + writeOnly?: boolean + type?: 'string' | 'object' | 'array' | 'number' | string + enum?: Array + const?: any + multipleOf?: number + maximum?: number + exclusiveMaximum?: number + minimum?: number + exclusiveMinimum?: number + maxLength?: number + minLength?: number + pattern?: string | RegExp + maxItems?: number + minItems?: number + uniqueItems?: boolean + maxProperties?: number + minProperties?: number + required?: string[] | boolean + format?: string + /** nested json schema spec **/ + properties?: { + [key: string]: ISchema + } + items?: ISchema | ISchema[] + additionalItems?: ISchema + patternProperties?: { + [key: string]: ISchema + } + additionalProperties?: ISchema + /** extend json schema specs */ + editable?: boolean + ['x-props']?: { [name: string]: any } + ['x-index']?: number + ['x-rules']?: ValidatePatternRules + ['x-component']?: string + ['x-component-props']?: { [name: string]: any } + ['x-render']?: ( + props: T & { + renderComponent: () => React.ReactElement + } + ) => React.ReactElement + ['x-effect']?: ( + dispatch: (type: string, payload: any) => void, + option?: object + ) => { [key: string]: any } +} +``` + + + +#### ISchemaFormActions + +> 核心actions继承@uform/react的IFormActions,主要增加了getSchema API + +```typescript +interface ISchemaFormActions extends IFormActions { + getSchema(): Schema + getFormSchema(): Schema +} +``` + +#### ISchemaFormAsyncActions + +> 核心actions继承@uform/react的IFormAsyncActions,主要增加了getSchema API + +```typescript +interface ISchemaFormAsyncActions extends IFormAsyncActions { + getSchema(): Promise + getFormSchema(): Promise +} +``` + + + +#### IFormValidateResult + +> 校验结果 + +```typescript +interface IFormValidateResult { + errors: Array<{ + path: string + messages: string[] + }> + warnings: Array<{ + path: string + messages: string[] + }> +} +``` + + + +#### InternalFormats + +> 内置格式校验集 + +```typescript +type InternalFormats = + | 'url' + | 'email' + | 'ipv6' + | 'ipv4' + | 'idcard' + | 'taodomain' + | 'qq' + | 'phone' + | 'money' + | 'zh' + | 'date' + | 'zip' + | string +``` + + + +#### ValidateDescription + +> 原始校验描述 + +```typescript +interface ValidateDescription { + //正则规则类型 + format?: InternalFormats + //自定义校验规则 + validator?: CustomValidator + //是否必填 + required?: boolean + //自定以正则 + pattern?: RegExp | string + //最大长度规则 + max?: number + //最大数值规则 + maximum?: number + //封顶数值规则 + exclusiveMaximum?: number + //封底数值规则 + exclusiveMinimum?: number + //最小数值规则 + minimum?: number + //最小长度规则 + min?: number + //长度规则 + len?: number + //是否校验空白符 + whitespace?: boolean + //枚举校验规则 + enum?: any[] + //自定义错误文案 + message?: string + //自定义校验规则 + [key: string]: any +} +``` + + + +#### ValidateResponse + +```typescript +type SyncValidateResponse = + | null + | string + | boolean + | { + type?: 'error' | 'warning' + message: string + } +type AsyncValidateResponse = Promise +type ValidateResponse = SyncValidateResponse | AsyncValidateResponse +``` + + + +#### CustomValidator + +> 自定义校验函数 + +```typescript +type CustomValidator = ( + value: any, + rescription?: ValidateDescription +) => ValidateResponse +``` + + + + + +#### ValidatePatternRules + +> 校验规则集 + +```typescript + type ValidatePatternRules = InternalFormats + | CustomValidator + | ValidateDescription + | Array< + InternalFormats | CustomValidator | ValidateDescription +> +``` diff --git a/packages/react-schema-renderer/src/hooks/useSchemaForm.ts b/packages/react-schema-renderer/src/hooks/useSchemaForm.ts index 1bd1c998523..4c0a419c9e4 100644 --- a/packages/react-schema-renderer/src/hooks/useSchemaForm.ts +++ b/packages/react-schema-renderer/src/hooks/useSchemaForm.ts @@ -13,8 +13,8 @@ const useInternalSchemaForm = (props: ISchemaFormProps) => { virtualFields, formComponent, formItemComponent, - component, schema, + defaultValue, value, initialValues, actions, @@ -25,6 +25,7 @@ const useInternalSchemaForm = (props: ISchemaFormProps) => { onValidateFailed, useDirty, children, + form, editable, validateFirst, ...formComponentProps diff --git a/packages/react-schema-renderer/src/index.tsx b/packages/react-schema-renderer/src/index.tsx index 8b2cc7cd7e6..9aa8faff73c 100644 --- a/packages/react-schema-renderer/src/index.tsx +++ b/packages/react-schema-renderer/src/index.tsx @@ -7,7 +7,6 @@ export * from '@uform/react' export * from './components/SchemaField' export * from './components/SchemaForm' export * from './components/SchemaMarkup' -export * from './hooks/useSchemaForm' export * from './shared/connect' export * from './shared/registry' export * from './shared/schema' diff --git a/packages/react-schema-renderer/src/shared/registry.ts b/packages/react-schema-renderer/src/shared/registry.ts index 3c60566729d..a8a16828644 100644 --- a/packages/react-schema-renderer/src/shared/registry.ts +++ b/packages/react-schema-renderer/src/shared/registry.ts @@ -32,8 +32,8 @@ export const cleanRegistry = () => { registry.wrappers = [] } -export function registerFormComponent( - component: React.JSXElementConstructor +export function registerFormComponent( + component: React.JSXElementConstructor ) { if (isFn(component)) { registry.formComponent = component @@ -65,11 +65,7 @@ export function registerFormField( registry.fields[name] = component registry.fields[name].__WRAPPERS__ = [] } else { - registry.fields[name] = compose( - component, - registry.wrappers, - true - ) + registry.fields[name] = compose(component, registry.wrappers, true) registry.fields[name].__WRAPPERS__ = registry.wrappers } registry.fields[name].displayName = pascalCase(name) @@ -123,11 +119,7 @@ export const registerFieldMiddleware = deprecate< if ( !component.__WRAPPERS__.some(wrapper => wrappers.indexOf(wrapper) > -1) ) { - registry.fields[key] = compose( - registry.fields[key], - wrappers, - true - ) + registry.fields[key] = compose(registry.fields[key], wrappers, true) registry.fields[key].__WRAPPERS__ = wrappers } } diff --git a/packages/react-schema-renderer/src/shared/schema.ts b/packages/react-schema-renderer/src/shared/schema.ts index 3716c7d996d..576a4dd4dff 100644 --- a/packages/react-schema-renderer/src/shared/schema.ts +++ b/packages/react-schema-renderer/src/shared/schema.ts @@ -1,8 +1,7 @@ import React from 'react' import { ValidatePatternRules, - ValidateDescription, - CustomValidator, + ValidateArrayRules, getMessage } from '@uform/validator' import { @@ -109,8 +108,12 @@ export class Schema implements ISchema { return res } - merge(props: {}) { - Object.assign(this, props) + merge(spec: any) { + if (spec instanceof Schema) { + Object.assign(this, spec.getSelfProps()) + } else { + Object.assign(this, spec) + } return this } @@ -143,7 +146,7 @@ export class Schema implements ISchema { return props } getExtendsRules() { - let rules: Array = [] + let rules: ValidateArrayRules= [] if (this.format) { rules.push({ format: this.format }) } @@ -233,9 +236,11 @@ export class Schema implements ISchema { getExtendsEditable(): boolean { if (isValid(this.editable)) { return this.editable - } else if (isValid(this['x-props'] && this['x-props'].editable)) { + } else if (isValid(this['x-props']) && isValid(this['x-props'].editable)) { return this['x-props'].editable - } else if (isValid(this.readOnly)) { + } else if(isValid(this['x-component-props']) && isValid(this['x-component-props'].editable)){ + return this['x-component-props'].editable + }else if (isValid(this.readOnly)) { return !this.readOnly } } @@ -365,12 +370,12 @@ export class Schema implements ISchema { return Schema.getOrderProperties(this) } - getOrderPatternProperties() { + unrelease_getOrderPatternProperties() { return Schema.getOrderProperties(this, 'patternProperties') } - mapPatternProperties(callback?: (schema: Schema, key: string) => any) { - return this.getOrderPatternProperties().map(({ schema, key }) => { + unrelease_mapPatternProperties(callback?: (schema: Schema, key: string) => any) { + return this.unrelease_getOrderPatternProperties().map(({ schema, key }) => { return callback(schema, key) }) } diff --git a/packages/react-schema-renderer/src/types.ts b/packages/react-schema-renderer/src/types.ts index 5d36dccae96..1c81a529519 100644 --- a/packages/react-schema-renderer/src/types.ts +++ b/packages/react-schema-renderer/src/types.ts @@ -126,15 +126,13 @@ export interface ISchema { ) => { [key: string]: any } } -export interface ISchemaFormProps - extends IFormProps< - any, - any, - any, - ISchemaFormActions | ISchemaFormAsyncActions - > { +export interface ISchemaFormProps< + Value = any, + DefaultValue = any, + FormEffectPayload = any, + FormActions = ISchemaFormActions | ISchemaFormAsyncActions +> extends IFormProps { schema?: ISchema - component?: string | React.JSXElementConstructor fields?: ISchemaFormRegistry['fields'] virtualFields?: ISchemaFormRegistry['virtualFields'] formComponent?: ISchemaFormRegistry['formComponent'] diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 44c5a8166ca..a69d2c106e6 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -36,14 +36,14 @@ export type IFormExtendsEffectSelector< export interface IFormProps< Value = {}, DefaultValue = {}, - EffectPayload = any, - EffectActions = any + FormEffectPayload = any, + FormActions = any > { value?: Value defaultValue?: DefaultValue initialValues?: DefaultValue - actions?: EffectActions - effects?: IFormEffect + actions?: FormActions + effects?: IFormEffect form?: IForm onChange?: (values: Value) => void onSubmit?: (values: Value) => void | Promise @@ -51,7 +51,7 @@ export interface IFormProps< onValidateFailed?: (valideted: IFormValidateResult) => void children?: React.ReactElement | ((form: IForm) => React.ReactElement) useDirty?: boolean - editable?: boolean + editable?: boolean | ((name: string) => boolean) validateFirst?: boolean } From 8dbffb896a6b7a20d5d2b5bccfdc46db00939445 Mon Sep 17 00:00:00 2001 From: Janry Date: Fri, 22 Nov 2019 22:04:15 +0800 Subject: [PATCH 53/99] Improve docs (#412) --- README.md | 9 +- README.zh-cn.md | 9 +- packages/react-schema-renderer/README.md | 1962 ++++++++++++++++++++++ 3 files changed, 1977 insertions(+), 3 deletions(-) create mode 100644 packages/react-schema-renderer/README.md diff --git a/README.md b/README.md index 198796cc813..144edbba111 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ English | [简体中文](./README.zh-cn.md)

    UForm - a high-performance React form solution for handling complicated enterprise requirements.

    - --- ## Background @@ -39,10 +38,16 @@ Use Fusion Design: npm install --save @alifd/next @uform/next ``` -## Documents +## WebSite https://uformjs.org +## Documents + +[@uform/react-schema-renderer](./packages/react-schema-renderder/README.md) +[@uform/react](./packages/react/README.md) +[@uform/core](./packages/core/README.md) + ## Demo diff --git a/README.zh-cn.md b/README.zh-cn.md index 28c21dc2eb9..204221a10ce 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -41,10 +41,17 @@ npm install --save antd @uform/antd npm install --save @alifd/next @uform/next ``` -## 文档 +## 官网 https://uformjs.org +## 文档 + +[@uform/react-schema-renderer](./packages/react-schema-renderder/README.md) +[@uform/react](./packages/react/README.md) +[@uform/core](./packages/core/README.md) + + ## 入门案例 [codesandbox](https://codesandbox.io/s/o5up7) diff --git a/packages/react-schema-renderer/README.md b/packages/react-schema-renderer/README.md new file mode 100644 index 00000000000..a1552fb5e32 --- /dev/null +++ b/packages/react-schema-renderer/README.md @@ -0,0 +1,1962 @@ +# @uform/react-schema-renderer + +English | [简体中文](./README.zh-cn.md) + +> Schema rendering engine, the package mainly relies on @uform/react, its responsibilities are very simple, the core does two things: +> +> - Parse the Form Schema protocol, recursive rendering +> - Manage custom components + +### Installation + +```bash +npm install --save @uform/react-schema-renderer +``` + +### Contents + + + +- [Usage](#usage) + * [Quick start](#quick-start) + * [JSON Schema driver form rendering](#json-schema-driver-form-rendering) + * [JSchema driver form rendering](#jschema-driver-form-rendering) + * [Non-single instance registration component](#non-single-instance-registration-component) +- [Advanced tutorial](#advanced-tutorial) + * [How to access third-party component libraries?](#how-to-access-third-party-component-libraries) + * [How to access the Form/FormItem component?](#how-to-access-the-formformitem-component) + * [How to access form components?](#how-to-access-form-components) + * [How to handle form layout?](#how-to-handle-form-layout) + * [How to implement recursive rendering components?](#how-to-implement-recursive-rendering-components) + * [How to implement the auto-increment list component?](#how-to-implement-the-auto-increment-list-component) + * [How to implement ultra-complex custom components?](#how-to-implement-ultra-complex-custom-components) +- [API](#api) + * [connect](#connect) + * [registerFormField](#registerformfield) + * [registerFormFields](#registerformfields) + * [registerFormComponent](#registerformcomponent) + * [registerFormItemComponent](#registerformitemcomponent) + * [registerVirtualBox](#registervirtualbox) + * [createVirtualBox](#createvirtualbox) + * [createControllerBox](#createcontrollerbox) + * [getRegistry](#getregistry) + * [cleanRegistry](#cleanregistry) +- [Classes](#classes) + * [new Schema(json : ISchema)](#new-schemajson--ischema) + + [`get`](#get) + + [`merge `](#merge-) + + [`getEmptyValue`](#getemptyvalue) + + [`getSelfProps`](#getselfprops) + + [`getExtendsRules`](#getextendsrules) + + [`getExtendsRequired`](#getextendsrequired) + + [`getExtendsEditable`](#getextendseditable) + + [`getExtendsTriggerType`](#getextendstriggertype) + + [`getExtendsProps`](#getextendsprops) + + [`getExtendsComponent`](#getextendscomponent) + + [`getExtendsComponentProps`](#getextendscomponentprops) + + [`getExtendsRenderer`](#getextendsrenderer) + + [`getExtendsEffect`](#getextendseffect) + + [`setProperty`](#setproperty) + + [`setProperties`](#setproperties) + + [`setArrayItems`](#setarrayitems) + + [`getOrderProperties`](#getorderproperties) + + [`mapProperties`](#mapproperties) + + [`toJSON`](#tojson) + + [`fromJSON`](#fromjson) + + [`isObject`](#isobject) + + [`isArray()`](#isarray) +- [Components](#components) + * [``](#SchemaForm) + * [``](#SchemaField) + * [``](#SchemaMarkupForm) + * [``](#SchemaMarkupField) + * [``](#InternalForm) + * [``](#InternalField) +- [Interfaces](#interfaces) + * [ISchemaFieldComponentProps](#ischemafieldcomponentprops) + * [ISchemaVirtualFieldComponentProps](#ischemavirtualfieldcomponentprops) + * [ISchemaFormRegistry](#ischemaformregistry) + * [ISchema](#ischema) + * [ISchemaFormActions](#ischemaformactions) + * [ISchemaFormAsyncActions](#ischemaformasyncactions) + * [IFormValidateResult](#iformvalidateresult) + * [InternalFormats](#internalformats) + * [ValidateDescription](#validatedescription) + * [ValidateResponse](#validateresponse) + * [CustomValidator](#customvalidator) + * [ValidatePatternRules](#validatepatternrules) + + + +### Usage + +--- + +#### Quick start + +If you are developing directly based on @uform/react-schema-renderer, then you must register your custom component before development. Go to the renderer, otherwise our JSON-Schema protocol can't render the form. And so: + +```jsx +import React from 'react' +import { + SchemaForm, + registerFormField, + connect +} from '@uform/react-schema-renderer' + +registerFormField( + 'string', + connect()(({ value, onChange }) => { + return + }) +) + +export default () => { + return ( + + ) +} +``` + +Great workmanship, this is the easiest way to use, the core is to register components, and then use the protocol to render.**One thing to note is that we used the connect function when registering components,The function of this connect function is to allow any component to be registered in the SchemaForm as long as it supports the value/onChange API.At the same time, the connect function also shields the Field API, so components that use the connect function cannot be more powerfully extended. The detailed connect API will be introduced later. ** At the same time, one thing to note is that if we want to access a set of component libraries, most of the component libraries in the industry have their Form and FormItem components.Their core is used to control styles, FormItem controls form local styles, and Form controls global form styles,So in a production environment, we need to register the Form and FormItem components so that the style is consistent with the original solution.How to register specifically, we will introduce in detail later. + +#### JSON Schema driver form rendering + +Speaking of JSON Schema, the above example has been covered. Of course, it is not complicated enough. Let's look at a more complicated example: + +```tsx +import React from 'react' +import { SchemaForm } from '@uform/react-schema-renderer' + +registerFormField( + 'string', + connect()(({ value, onChange }) => { + return + }) +) + +registerFormField('array', () => { + return //... +}) + +export default () => { + return ( + + ) +} +``` + +The above code is a pseudo-code because we didn't register a custom component of type array. Here we don't talk about how to register custom components of the array type. Our core is to analyze how JSON Schema drives form rendering.Note:**In SchemaForm, recursive rendering of properties of an object is built-in, but there is no recursive rendering of items with a built-in array**.The main reason is that the recursive rendering of an array involves many style requirements, which is not convenient for built-in. So it’s best to leave it to the developer to implement it yourself. So, we will detail how to implement the recursive rendering requirements of the auto-increment list. + +#### JSchema driver form rendering + +JSchema describes JSON Schema in a more elegant way in jsx. We can implement a version of JSchema for the above example: + +```tsx +import React from 'react' +import { + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field +} from '@uform/react-schema-renderer' + +export default () => { + return ( + + + + + + + + ) +} +``` + +As you can see, using JSchema to describe JSON Schema in your code is more elegant than JSON, greatly improving code maintainability. + +#### Non-single instance registration component + +In the previous example, we used the registerFormField API to register a custom component, which is the way to register a singleton,Its main advantage is the convenience, but there are also some problems, that is, the single case is easily contaminated, especially in the SPA page.If the developers of different pages are different, because sharing the same memory environment, then A developers may register custom components with the same name as B developers, which can easily lead to online failures.Therefore, we recommend users to use non-single registration methods: + +```jsx +import React, { useMemo } from 'react' +import { + SchemaForm, + registerFormField, + connect +} from '@uform/react-schema-renderer' + +const StringField = connect()(({ value, onChange }) => { + return +}) + +const useFields = () => + useMemo(() => { + string: StringField + }) + +export default () => { + return ( + + ) +} +``` + +In the above example, we mainly pass the custom component in the props dimension of SchemaForm, so that we can guarantee the instance-level registration. This form is very friendly to the SPA.At the same time,**It should be noted that we abstracted a React Hook of useFields, which is mainly used to solve the React Virtual DOM recalculation when the component is rendered multiple times, thus avoiding the repeated rendering of the form component.** + +### Advanced tutorial + +--- + +#### How to access third-party component libraries? + +Because @uform/react-schema-renderer is a base library, no component library is integrated by default. so in actual business development, If you want to customize it based on it, you have to face the problem of accessing third-party component libraries. How to access the third-party component library, let's divide it into the following steps: + +- Access the Form/FormItem component +- Access component library form component +- Implement form layout components +- Implement an auto-increment list component + +Let's take a step by step access to third-party component libraries! Here we mainly use the antd component library as an example. + +#### How to access the Form/FormItem component? + +The access method currently provides a global registration mechanism and a singleton registration mechanism. Global registration is mainly registered using two APIs, registerFormComponent and registerFormItemComponent.Singleton registration is to upload formComponent and formItemComponent directly in the SchemaForm property. If it is a SPA scene, it is recommended to use the singleton registration method. Let's look at the example below: + +```tsx +import React from 'react' +import { + SchemaForm, + registerFormComponent, + registerFormItemComponent +} from '@uform/react-schema-renderer' +import { Form } from 'antd' + +export const CompatFormComponent = ({children,...props})=>{ + return
    {children}
    //Very simple to use the Form component, props is the props of the SchemaForm component, here will be directly transparent +}) + +export const CompatFormItemComponent = ({children,...props})=>{ + const messages = [].concat(props.errors || [], props.warnings || []) + let status = '' + if (props.loading) { + status = 'validating' + } + if (props.invalid) { + status = 'error' + } + if (props.warnings && props.warnings.length) { + status = 'warning' + } + return ( + + {children} + + ) +} + +/*** +Global registration method +registerFormComponent(CompatFormComponent) +registerFormItemComponent(CompatFormItemComponent) +***/ + +//Single case registration method +export default ()=>{ + return ( + +} + +``` + +We can see that extending the overall or partial style of the form can be easily solved by simply extending the Form/FormItem component. It should be noted here that the props received by the FormItem component are a bit complicated. Don't worry, the detailed props API will be listed later. Now we just need to know how it is probably registered. + +#### How to access form components? + +all components of the component library are atomic components, most of them are compatible with the value/onChange specification, so we can quickly access the components of the component library with the connect function. Usually, if we need access to the component library component, we probably do three things: + +- Handle state maps, map the loading/error state inside the uform to the component properties, of course, **the premise is that the component must support styles such as loading or error.** +- Handle detail styles, map the editable state inside the uform to a PreviewText component for more friendly and cleaner presentation data. +- Handle component enumeration, let's think about it, **JSON Schema, each node should support the enum attribute**, If we have the enum attribute, we'd better show it in the form of Select, so we need to handle the component enumeration state. + +Let's take an example of InputNumber: + +```tsx +import React from 'react' +import { connect, registerFormField } from '@uform/react-schema-renderer' +import { InputNumber } from 'antd' + +const mapTextComponent = ( + Target: React.JSXElementConstructor, + props: any = {}, + fieldProps: any = {} +): React.JSXElementConstructor => { + const { editable } = fieldProps + if (editable !== undefined) { + if (editable === false) { + return PreviewText + } + } + if (Array.isArray(props.dataSource)) { + return Select + } + return Target +} + +const mapStyledProps = ( + props: IConnectProps, + fieldProps: MergedFieldComponentProps +) => { + const { loading, errors } = fieldProps + if (loading) { + props.state = props.state || 'loading' + } else if (errors && errors.length) { + props.state = 'error' + } +} + +const acceptEnum = (component: React.JSXElementConstructor) => { + return ({ dataSource, ...others }) => { + if (dataSource) { + return React.createElement(Select, { dataSource, ...others }) + } else { + return React.createElement(component, others) + } + } +} + +registerFormField( + 'number', + connect({ + getProps: mapStyledProps, //Process state map + getComponent: mapTextComponent //Process details + })(acceptEnum(InputNumber)) //Process enumeration states +) +``` + +In this example, we use the connect function in depth. Connect is a HOC. During the rendering phase, it can add some intermediate processing logic to the component rendering process to help dynamic distribution. Of course, connect has a lot of APIs, which will be covered in detail later. + +#### How to handle form layout? + +JSON Schema describes the form of data structure, which is supported by nature. But the form ends up at the UI level. Unfortunately, at the UI level, we have a lot of components that are not a specific data node of JSON Schema. It's just a UI node. So, how to do to describe complex layouts in JSON Schema? + +Now uform's approach is to abstract a concept called **virtual node**After the user specifies a JSON Schema x-component as a virtual node at the code level, whether it is rendering, data processing, or final data submission, as long as the node is virtual, it will not be treated as a normal data node. So, with the concept of this virtual node, we can describe various complex layouts in JSON Schema. Let's try to write a layout component: + +```tsx +import React from 'react' +import { SchemaForm, registerVirtualBox } from '@uform/react-schema-renderer' +import { Card } from 'antd' + +registerVirtualBox('card', ({ children, ...props }) => { + return {children} +}) + +export default () => { + return ( + + ) +} +``` + +From this pseudo-code, we can see that the card is a normal Object Schema node, just need to specify an x-component as card, so that it can match the card registered by registerVirtualBox, it will achieve the effect of virtual node. So, no matter what you change the name of the property in JSON Schema, it will not affect the final submitted data structure.**It should be noted here that x-component-props is directly passed to the callback function parameters of registerVirtualBox.** This is the use of the JSON Schema form, and we also use JSchema: + +```tsx +import React from 'react' +import { SchemaForm, createVirtualBox } from '@uform/react-schema-renderer' +import { Card } from 'antd' + +const Card = createVirtualBox('card', ({ children, ...props }) => { + return {children} +}) + +export default () => { + return ( + + + + + + + + + + ) +} +``` + +From this example we can see that with the createVirtualBox API you can quickly create a layout component that you can use directly in JSchema.**In fact, the internal implementation of createVirtualBox is very simple, just use registerVitualBox and SchemaMarkupField**: + +```tsx +import React from 'react' +import { SchemaMarkupField as Field } from '@uform/react-schema-renderer' +export function createVirtualBox( + key: string, + component?: React.JSXElementConstructor> +) { + registerVirtualBox( + key, + component + ? ({ props, schema, children }) => { + return React.createElement(component, { + ...schema.getExtendsComponentProps(), + children + }) + } + : () => + ) + const VirtualBox: React.FC = ({ + children, + name, + ...props + }) => { + return ( + + {children} + + ) + } + return VirtualBox +} +``` + +The way to register the layout components described above is a singleton registration. If we need registration in the form of an instance, it is still similar to the way we said earlier. + +```tsx +import React from 'react' +import { Card as AntdCard } from 'antd' +const Card = ({children,...props})=>{ + return {children}
    +} + +export default ()=>{ + return ( + + ) +} +``` + +#### How to implement recursive rendering components? + +What is a recursive rendering component? In fact, **implement the components of properties and items in JSON Schema**,sucn as`type:"string"` This kind of node belongs to the atomic node and is not a recursive rendering component. The layout component mentioned above, it is also a recursive rendering component, but it fixes the rendering mode, so it can be easily registered. So, most of us want to implement recursive rendering of the scene,Perhaps more of a scene in`type:"array"will implement recursive rendering. Below we will detail the implementation of the auto-increment list component. + +#### How to implement the auto-increment list component? + +The self-increment list has several main features: + +- Independent style +- Support for recursive rendering of subcomponents +- Support for adding, deleting, moving up, down moving array items +- Cannot be wrapped with the connect function because the Field API must be called + +In order to help you better understand how to implement the auto-increment list component, we will not implement the specific style, and more is to teach you how to implement recursive rendering and array item operations. Let us look at the pseudo-code: + +```tsx +import React, { Fragment } from 'react' +import { + registerFormField, + SchemaField, + FormPath +} from '@uform/react-schema-renderer' + +//No need to pack with connet +registerFormField('array', ({ value, path, mutators }) => { + const emptyUI = ( + + ) + + const listUI = value.map((item, index) => { + return ( +
    + + + + +
    + ) + }) + + return value.length == 0 ? emptyUI : listUI +}) +``` + + To implement a self-incrementing list component with recursive rendering is super simple. On the contrary, it would be a bit cumbersome to implement related styles. In short, the core is to use the SchemaField component and the mutators API, the specific API will be described in detail later. + +#### How to implement ultra-complex custom components? + +This problem is no solution in the old UForm.It happens that because our business complexity is high to a certain extent, we are limited by this problem, so we must find a way to solve this problem. We can define what is a super complex custom component: + +- There are a large number of form components inside the component, and there are also a large number of linkage relationships inside. +- There is a private server dynamic rendering scheme inside the component. + +- There is a complex layout structure inside the component + +In these three points, meet the characteristics of ultra-complex custom components, For this scenario, why can't we solve the problem by properly encapsulating the form of a custom component? In fact, it is mainly limited to verification, there is no way to check the whole, so we need an ability to aggregate a large amount of field processing logic. Let's take a look at the specific solution: + +```tsx +import React, { Fragment } from 'react' +import { + registerFormField, + SchemaField, + FormPath, + InternalField, + useFormEffects, + FormEffectHooks +} from '@uform/react-schema-renderer' +import { Input, Form } from 'antd' + +const FormItem = ({ component, ...props }) => { + return ( + + {({ state, mutators }) => { + const messages = [].concat(state.errors || [], state.warnings || []) + let status = '' + if (state.loading) { + status = 'validating' + } + if (state.invalid) { + status = 'error' + } + if (state.warnings && state.warnings.length) { + status = 'warning' + } + return ( + + {React.createElement(component, { + ...state.props, + value: state.value, + onChange: mutators.change, + onBlur: mutators.blur, + onFocus: mutators.focus + })} + + ) + }} + + ) +} + +//No need to pack with connet +registerFormField('complex', ({ path }) => { + useFormEffects(({ setFieldState }) => { + FormEffectHooks.onFieldValueChange$('ccc').subscribe(({ value }) => { + if (value === '123') { + setFieldState('ddd', state => { + state.value = 'this is linkage relationship' + }) + } + }) + }) + + return ( + <> + + + + + + ) +}) +``` + +In this pseudo-code, we mainly use two core APIs, mainly useFormEffects and InternalField,useFormEffects gives developers a place to write the effects logic locally, so that the effects logic can be easily reused. InternalField is the @uform/react Field component. You can take a look at the @uform/react documentation. SchemaForm also uses @uform/react internally, it can share the same Context, so we can easily use InternalField inside a custom component. Also note that **when using InternalField directly, the name we registered is the root level name, if you want to reuse the path of the current custom component, you can use FormPath to resolve the path, and then concat.** + +### API + +--- + +> The overall API fully inherits @uform/core and @uform/react, and only the specific APIs of @uform/react-schema-renderer are listed below. + +#### connect + +> Custom component registration bridge, a high-level component function (HOC) that is used primarily to quickly access most component library components (components that implement the value/onChange API) + +**Signature** + +```typescript +connect(options?: IConnectOptions): (component : React.JSXElementConstructor)=>(fieldProps:ISchemaFieldComponentProps)=>JSX.Element +``` + +**Usage** + +```typescript +import { registerFormField, connect } from '@uform/react-schema-renderer' +import { Select } from 'antd' +registerFormField('select', connect()(Select)) +``` + + + +#### registerFormField + +> Register custom component functions + +**Signature** + +```typescript +registerFormField( + name:string, + component: React.JSXElementConstructor +) +``` + + + +#### registerFormFields + +> Bulk registration of custom components + +**Signature** + +```typescript +registerFormFields( + fieldsMap: { + [key : string]: React.JSXElementConstructor + } +) +``` + + + +#### registerFormComponent + +> Register Form Style Component + +**Signature** + +```typescript +registerFormComponent( + component:React.JSXElementConstructor +) +``` + + + +#### registerFormItemComponent + +> Register the FormItem style component + +**Signature** + +```typescript +registerFormItemComponent( + component:React.JSXElementConstructor +) +``` + + + +#### registerVirtualBox + +> Register the virtual box component, mainly used to describe the layout in JSON Schema + +**Signature** + +```typescript +registerVirtualBox( + name:string, + component:React.JSXElementConstructor +) +``` + + + +#### createVirtualBox + +> Create a virtual box component, the returned component can be used directly in SchemaMarkupForm + +**Signature** + +```typescript +createVirtualBox( + name:string, + component:React.JSXElementConstructor +) : React.FC +``` + +**Usage** + +```tsx +import React from 'react' +import { + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, + createVirtualBox +} from '@uform/react-schema-renderer' +import { Card } from 'antd' + +const FormCard = createVirtualBox('card', props => { + return +}) + +export default () => ( + + + + + +) +``` + + + +#### createControllerBox + +> Creating a virtual box component, the returned component can be used directly in SchemaMarkupForm, it is different from createVirtualBox mainly because the component receives the props +> The props received by createVirtualBox is the simplest component props +> createControllerBox receives`ISchemaVirtualFieldComponentProps` + +**Signature** + +```typescript +createControllerBox( + name:string, + component:React.JSXElementConstructor +) : React.FC +``` + +**Usage** + +```tsx +import React from 'react' +import { + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, + createControllerBox +} from '@uform/react-schema-renderer' +import { Card } from 'antd' + +const FormCard = createControllerBox('card', ({ schema, children }) => { + return {children} +}) + +export default () => ( + + + + + +) +``` + + + +#### getRegistry + +> Get the registry, all components registered through the registerFormXXX API are managed in the registry + +**Signature** + +```typescript +getRegistry(): ISchemaFormRegistry +``` + + + +#### cleanRegistry + +> Clear the registry and clear all components registered via the registerFormXXX API + +**Signature** + +```typescript +cleanRegistry(): void +``` + + + +### Classes + +> The whole Class inherits @uform/core completely, such as FormPath and FormLifeCyle. Only the classes specific to @uform/react-schema-renderer are listed below. + +#### new Schema(json : ISchema) + +> The Schema parsing engine, given a data that satisfies the JSON Schema, we will parse it into the corresponding Schema instance, and we can quickly process some things with some tool methods. At the same time, the Schema Class provides a unified protocol difference smoothing capability to ensure Seamless and smooth upgrade when the protocol is upgraded + +**Attributes** + +| Attribute name | Description | Type | +| -------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| title | Field title | `React.ReactNode` | +| description | Field description | `React.ReactNode` | +| default | Field default | `any` | +| readOnly | Whether read-only and editable | `boolean` | +| type | Field Type | `'string' | 'object' | 'array' | 'number' | string` | +| enum | Enumerate data | `Array` | +| const | Check if the field value is equal to the value of const | `any` | +| multipleOf | Check if the field value can be divisible by the value of multipleOf | `number` | +| maximum | Check the maximum value (greater than) | `number` | +| exclusiveMaximum | Check the maximum value (greater than or equal to) | `number` | +| minimum | Check minimum value (less than) | `number` | +| exclusiveMinimum | Minimum value (less than or equal to) | `number` | +| maxLength | Check maximum length | `number` | +| minLength | Check minimum length | `number` | +| pattern | Regular check rule | `string | RegExp` | +| maxItems | Maximum number of entries | `number` | +| minItems | Minimum number of entries | `number` | +| uniqueItems | Whether to check for duplicates | `boolean` | +| maxProperties | Maximum number of attributes | `number` | +| minProperties | Minimum number of attributes | `number` | +| required | Required | `boolean` | +| format | Regular rule type | `InternalFormats` | +| properties | Object property | `{[key : string]:Schema}` | +| items | Array description | `Schema | Schema[]` | +| additionalItems | Extra array element description | `Schema` | +| patternProperties | Dynamically match the schema of an attribute of an object | `{[key : string]:Schema}` | +| additionalProperties | Schema matching the extra attributes of the object | `Schema` | +| editable | Whether the field is editable | `boolean` | +| x-props | Field extension attribute | `{ [name: string]: any }` | +| x-index | Field order | `number` | +| x-rules | Field check rule | `ValidatePatternRules` | +| x-component | Field UI component | `string` | +| x-component-props | Field UI component properties | `{}` | +| x-render | Field extension rendering function | `(props: T & { renderComponent: () => React.ReactElement}) => React.ReactElement` | +| x-effect | Field side effect trigger | `(dispatch: (type: string, payload: any) => void,option?:object) => { [key: string]: any }` | + + + +**Method** + +##### `get` + +> Get the Schema child node according to the data path + +Signature + +```typescript +get(path?: FormPathPattern): Schema +``` + +Usage + +```typescript +const schema = new Schema({ + type:"object", + properties:{ + array:{ + type:'array', + items:{ + type:'object', + properties:{ + input:{ + type:'string' + } + } + } + } + } +}) + +schema.get('array[0].input') //{type:"string"} +``` + + + +##### `merge ` + +> Merge Schema + +Signature + +```typescript +merge(spec:ISchema): Schema +``` + +Usage + +```typescript +const schema = new Schema({ + type:"object", + properties:{ + array:{ + type:'array', + items:{ + type:'object', + properties:{ + input:{ + type:'string' + } + } + } + } + } +}) + +schema.merge({ + title:'root object' +}) +/** +{ + type:"object", + title:"root object", + properties:{ + array:{ + type:'array', + items:{ + type:'object', + properties:{ + input:{ + type:'string' + } + } + } + } + } +} +**/ +``` + + + +##### `getEmptyValue` + +> Get the null value of the current Schema based on the type of Schema + +Signature + +```typescript +getEmptyValue() : '' | [] | {} | 0 +``` + +Usage + +```typescript +const schema = new Schema({ + type:"object", + properties:{ + array:{ + type:'array', + items:{ + type:'object', + properties:{ + input:{ + type:'string' + } + } + } + } + } +}) + +schema.get('array.0.input').getEmptyValue() // '' +schema.get('array.0').getEmptyValue() // {} +schema.get('array').getEmptyValue() // [] +schema.getEmptyValue() // {} + +``` + + + +##### `getSelfProps` + +> Get no nested Schema properties (does not include nested properties like properties/items) + +Signature + +```typescript +getSelfProps() : ISchema +``` + +Usage + +```typescript +const schema = new Schema({ + type:"object", + properties:{ + array:{ + type:'array', + items:{ + type:'object', + properties:{ + input:{ + type:'string' + } + } + } + } + } +}) + +schema.getSelfProps() // { type:"object" } + +``` + + + +##### `getExtendsRules` + +> Get the extended check rule. This method is more complicated. It will parse the attributes and x-rules attributes of all the check types of the current Schema, and finally merge them into a unified rules structure. + +Signature + +```typescript +getExtendsRules() : ValidateArrayRules +``` + +Usage + +```typescript +const schema = new Schema({ + type:"string", + required:true, + maxLength:10 + "x-rules":{ + pattern:/^\d+$/ + } +}) + +schema.getExtendsRules() // [{required:true},{max:10},{pattern:/^\d+$/}] +``` + + + +##### `getExtendsRequired` + +> Obtaining whether it is required or not is actually reading the required attribute of the Schema. Why is it encapsulated into a method to ensure that the user is not aware of the protocol upgrade? We only need to ensure backward compatibility of the method. + +Signature + +```typescript +getExtendsRequired(): void | boolean +``` + +Usage + +```typescript +const schema = new Schema({ + type:"string", + required:true, + maxLength:10 + "x-rules":{ + pattern:/^\d+$/ + } +}) + +schema.getExtendsRequired() // true +``` + + + +##### `getExtendsEditable` + +> Get the editable state of the Schema, consistent with the getExtendsEditable ability, also to smooth out the protocol differences + +Signature + +```typescript +getExtendsEditable() : void | boolean +``` + +Usage + +```typescript +const schema1 = new Schema({ + type:"string", + editable:false +}) + +schema1.getExtendsEditable() // false + +const schema2 = new Schema({ + type:"string", + readOnly:true +}) + +schema2.getExtendsEditable() // false + +const schema3 = new Schema({ + type:"string", + "x-props":{ + editable:false + } +}) + +schema3.getExtendsEditable() // false + +const schema4 = new Schema({ + type:"string", + "x-component-props":{ + editable:false + } +}) + +schema4.getExtendsEditable() // false +``` + + + +##### `getExtendsTriggerType` + +> Get the triggerType, which is consistent with the getExtendsTriggerType capability, and is capable of providing protocol differences. + +Signature + +```typescript +getExtendsTriggerType() : 'onBlur' | 'onChange' | string +``` + +Usage + +```typescript +const schema1 = new Schema({ + type:"string", + "x-props":{ + triggerType:"onBlur" + } +}) + +schema1.getExtendsTriggerType() // onBlur + +const schema2 = new Schema({ + type:"string", + "x-component-props":{ + triggerType:"onBlur" + } +}) + +schema2.getExtendsTriggerType() // onBlur + +const schema3 = new Schema({ + type:"string", + "x-item-props":{ + triggerType:"onBlur" + } +}) + +schema3.getExtendsTriggerType() // onBlur +``` + +##### `getExtendsProps` + +> Get the x-props attribute + +Signature + +```typescript +getExtendsProps() : {} +``` + + + +##### `getExtendsComponent` + +> Get the x-props attribute + +Signature + +```typescript +getExtendsComponent() : string +``` + + + +##### `getExtendsComponentProps` + +> Get the x-component-props property, which is the component property of the x-component + +Signature + +```typescript +getExtendsComponentProps() : {} +``` + + + +##### `getExtendsRenderer` + +> Get the x-render attribute + +Signature + +```typescript +getExtendsRenderer() : ( + props: T & { + renderComponent: () => React.ReactElement + } + ) => React.ReactElement +``` + + + +##### `getExtendsEffect` + +> Get the x-effect attribute + +Signature + +```typescript +getExtendsEffect() : ( + dispatch: (type: string, payload: any) => void, + option?: object +) => { [key: string]: any } +``` + + + +##### `setProperty` + +> Set properties for the current schema + +Signature + +```typescript +setProperty(key: string, schema: ISchema): Schema +``` + + + +##### `setProperties` + +> Set properties for the current Schema batch + +Signature + +```typescript +setProperties(properties: {[key : string]:ISchema}) : {[key : string]:Schema} +``` + + + +##### `setArrayItems` + +> Set the items property to the current schema + +Signature + +```typescript +setArrayItems(schema:Ischema) : Schema +``` + + + +##### `getOrderProperties` + +> Give all properties in x-index order + +Signature + +```typescript +getOrderProperties() : {schema:Schema,key:string}[] +``` + + + +##### `mapProperties` + +> Traverse the properties of the schema in order (x-index) + +Signature + +```typescript +mapProperties(callback?: (schema: Schema, key: string) => any):any[] +``` + + + +##### `toJSON` + +> Output no loop dependent json data structure + +Signature + +``` +toJSON() : ISchema +``` + + + +##### `fromJSON` + +> Generate a Schema object based on a json parsing + +Signature + +```typescript +fromJSON(json : ISchema) : Schema +``` + + + +##### `isObject` + +> Determine whether the current schema is an object type + +Signature + +``` +isObject() : boolean +``` + + + +##### `isArray()` + +> Determine if the current schema is an array type + +Signature + +``` +isArray() : boolean +``` + + + +### Components + +--- + +> The whole component is completely inherited @uform/react, only the components specific to @uform/react-schema-renderer are listed below. + +#### `` + +> The core JSON Schema rendering component + +**Attributes** + +```typescript +interface ISchemaFormProps< + Value = any, + DefaultValue = any, + FormEffectPayload = any, + FormActions = ISchemaFormActions | ISchemaFormAsyncActions +> { + //Form value + value?: Value; + //Form default value + defaultValue?: DefaultValue; + //Form default value, weakly controlled + initialValues?: DefaultValue; + //Form actions + actions?: FormActions; + //Form effects + effects?: IFormEffect; + //Form instance + form?: IForm; + //Form change callback + onChange?: (values: Value) => void; + //Form submission callback + onSubmit?: (values: Value) => void | Promise; + //Form reset callback + onReset?: () => void; + //Form validation failure callback + onValidateFailed?: (valideted: IFormValidateResult) => void; + //Form child node + children?: React.ReactElement; + //Whether to open the dirty check + useDirty?: boolean; + //Is it editable + editable?: boolean | ((name: string) => boolean); + //Whether to enable pessimistic verification, if the first verification fails, stop the remaining verification + validateFirst?: boolean; + //Form Schema object + schema?: ISchema + //Instance level registration custom component + fields?: ISchemaFormRegistry['fields'] + //Instance level registration virtual box component + virtualFields?: ISchemaFormRegistry['virtualFields'] + //Instance level registration Form style component + formComponent?: ISchemaFormRegistry['formComponent'] + //Instance level registration FormItem style component + formItemComponent?: ISchemaFormRegistry['formItemComponent'] +} +``` + + + +#### `` + +> Based on a Data Path, it automatically finds and renders the internal components of the Schema node, mainly used to implement recursive rendering within the custom component. + +**Attributes** + +```typescript +interface ISchemaFieldProps { + //Data path + path?: FormPathPattern +} +``` + + + +#### `` + +> Let SchemaForm support the Form component of jsx tabbed notation, which needs to be used together with SchemaMarkupField + +**Attributes** + +```typescript +interface ISchemaFormProps< + Value = any, + DefaultValue = any, + FormEffectPayload = any, + FormActions = ISchemaFormActions | ISchemaFormAsyncActions +> { + //Form value + value?: Value; + //Form default value + defaultValue?: DefaultValue; + //Form default value, weakly controlled + initialValues?: DefaultValue; + //Form actions + actions?: FormActions; + //Form effects + effects?: IFormEffect; + //Form instance + form?: IForm; + //Form change callback + onChange?: (values: Value) => void; + //Form submission callback + onSubmit?: (values: Value) => void | Promise; + //Form reset callback + onReset?: () => void; + //Form validation failure callback + onValidateFailed?: (valideted: IFormValidateResult) => void; + //Form child node + children?: React.ReactElement; + //Whether to open the dirty check + useDirty?: boolean; + //Is it editable + editable?: boolean | ((name: string) => boolean); + //Whether to enable pessimistic verification, if the first verification fails, stop the remaining verification + validateFirst?: boolean; + //Form Schema object + schema?: ISchema + //Instance level registration custom component + fields?: ISchemaFormRegistry['fields'] + //Instance level registration virtual box component + virtualFields?: ISchemaFormRegistry['virtualFields'] + //Instance level registration Form style component + formComponent?: ISchemaFormRegistry['formComponent'] + //Instance level registration FormItem style component + formItemComponent?: ISchemaFormRegistry['formItemComponent'] +} +``` + +**Usage** + +```tsx +import { + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field +} from '@uform/react-schema-renderer' + +export default ()=>{ + return ( + + + + ) +} +``` + +#### `` + +> The SchemaForm supports the Field component of jsx tabbed notation, which needs to be used together with SchemaMarkupForm + +**Attributes** + +```typescript +type IMarkupSchemaFieldProps = ISchema +``` + + + +#### `` + +> The core Form is the same as the Form component in @uform/react + + + +#### `` + +> The core Field, like the Field component in @uform/react, is mainly used in complex custom components. + +### Interfaces + +--- + +> Inheriting @uform/react and @uform/core's Interfaces as a whole, only @uform/react-schema-renderer are listed below. + +#### ISchemaFieldComponentProps + +> It is very important to customize the properties received by the component. As long as it involves developing custom components, you need to understand the protocol. + +```typescript +interface ISchemaFieldComponentProps { + //State name, FieldState + displayName?: string + //Data path + name: string + //Node path + path: string + //Whether it has been initialized + initialized: boolean + //Whether it is in the original state, the state is true only when value===intialValues + pristine: boolean + //Whether it is in a legal state, as long as the error length is greater than 0, the valid is false. + valid: boolean + //Whether it is in an illegal state, as long as the error length is greater than 0, the valid is true. + invalid: boolean + //Is it in check state? + validating: boolean + //Whether it is modified, if the value changes, the property is true and will be true for the entire lifetime of the field. + modified: boolean + //Whether it is touched + touched: boolean + //Whether it is activated, it will be triggered to true when the field triggers the onFocus event, and false when onBlur is triggered. + active: boolean + //Whether it has been accessed, when the field triggers the onBlur event, it will be triggered to true + visited: boolean + //Is it visible, note: if the status is false, the value of the field will not be submitted, and the UI will not display + visible: boolean + //Whether to show, note: if the status is false, then the value of the field will be submitted, the UI will not display, similar to the form hidden field + display: boolean + //Is it editable + editable: boolean + //Whether it is in the loading state, note: if the field is in asynchronous check, loading is true + loading: boolean + //The field has multiple parameters. For example, when the field onChange is triggered, multi-parameter data is passed to the event callback, then the values of all parameters are stored here. + values: any[] + //Field error message + errors: string[] + //Field alert message + warnings: string[] + //Field value, which is constant equal to values[0] + value: any + //Initial value + initialValue: any + //Verification rules, specific type descriptions refer to the following documents + rules: ValidatePatternRules[] + //Required or not + required: boolean + //Has it been mounted? + mounted: boolean + //Has it been unmounted? + unmounted: boolean + //Field extension property, under the SchemaForm ISchema structure + props: ISchema + //The schema object of the current field + schema: Schema + //Current operation data set of the field + mutators: IMutators + //Form instance + form: IForm + //Recursive rendering function + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + + + +#### ISchemaVirtualFieldComponentProps + +> The attributes received by the virtual field component, as long as it involves registering the virtual field, you need to understand the protocol. + +```typescript +interface ISchemaVirtualFieldComponentProps { + //State name, VirtualFieldState + displayName: string + //Field data path + name: string + //Field node path + path: string + //Whether it has been initialized + initialized: boolean + //Is it visible, note: if the status is false, the UI will not be displayed, and the data will not be submitted (because it is a VirtualField) + visible: boolean + //Whether to show, note: if the state is false, the UI will not be displayed, and the data will not be submitted (because it is a VirtualField) + display: boolean + //Has it been mounted? + mounted: boolean + //Has it been unmounted? + unmounted: boolean + //Field extension attribute + props: ISchema + //The schema object of the current field + schema: Schema + //Form instance + form: IForm + //Child element + children: React.ReactElement[] + //Recursive rendering function + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + + + +#### ISchemaFormRegistry + +> The component registry, whether it is a normal field, a virtual field, or a Form/FormItem will be registered here. + +```typescript +interface ISchemaFormRegistry { + fields: { + [key: string]: React.JSXElementConstructor + }, + virtualFields: { + [key: string]: React.JSXElementConstructor + }, + formItemComponent: React.JSXElementConstructor + formComponent: string | React.JSXElementConstructor +} +``` + + + +#### ISchema + +> Schema protocol object, mainly used to constrain a json structure to satisfy the Schema protocol + +```typescript +interface ISchema { + /** base json schema spec**/ + title?: React.ReactNode + description?: React.ReactNode + default?: any + readOnly?: boolean + writeOnly?: boolean + type?: 'string' | 'object' | 'array' | 'number' | string + enum?: Array + const?: any + multipleOf?: number + maximum?: number + exclusiveMaximum?: number + minimum?: number + exclusiveMinimum?: number + maxLength?: number + minLength?: number + pattern?: string | RegExp + maxItems?: number + minItems?: number + uniqueItems?: boolean + maxProperties?: number + minProperties?: number + required?: string[] | boolean + format?: string + /** nested json schema spec **/ + properties?: { + [key: string]: ISchema + } + items?: ISchema | ISchema[] + additionalItems?: ISchema + patternProperties?: { + [key: string]: ISchema + } + additionalProperties?: ISchema + /** extend json schema specs */ + editable?: boolean + ['x-props']?: { [name: string]: any } + ['x-index']?: number + ['x-rules']?: ValidatePatternRules + ['x-component']?: string + ['x-component-props']?: { [name: string]: any } + ['x-render']?: ( + props: T & { + renderComponent: () => React.ReactElement + } + ) => React.ReactElement + ['x-effect']?: ( + dispatch: (type: string, payload: any) => void, + option?: object + ) => { [key: string]: any } +} +``` + + + +#### ISchemaFormActions + +> The core actions inherit @uform/react's IFormActions, mainly adding the getSchema API + +```typescript +interface ISchemaFormActions extends IFormActions { + getSchema(): Schema + getFormSchema(): Schema +} +``` + +#### ISchemaFormAsyncActions + +> Core actions inherit @uform/react's IFormAsyncActions, mainly adding the getSchema API + +```typescript +interface ISchemaFormAsyncActions extends IFormAsyncActions { + getSchema(): Promise + getFormSchema(): Promise +} +``` + + + +#### IFormValidateResult + +> Calibration result + +```typescript +interface IFormValidateResult { + errors: Array<{ + path: string + messages: string[] + }> + warnings: Array<{ + path: string + messages: string[] + }> +} +``` + + + +#### InternalFormats + +> Built-in format checksum + +```typescript +type InternalFormats = + | 'url' + | 'email' + | 'ipv6' + | 'ipv4' + | 'idcard' + | 'taodomain' + | 'qq' + | 'phone' + | 'money' + | 'zh' + | 'date' + | 'zip' + | string +``` + + + +#### ValidateDescription + +> Original check description + +```typescript +interface ValidateDescription { + // built-in rules,ref: string rules + format?: InternalFormats + // custom validation + validator?: CustomValidator + // required + required?: boolean + // pattern + pattern?: RegExp | string + // max length + max?: number + // maximum + maximum?: number + // exclusiveMaximum + exclusiveMaximum?: number + // exclusiveMinimum + exclusiveMinimum?: number + // minimum + minimum?: number + // min + min?: number + // length + len?: number + // whitespace + whitespace?: boolean + // enum + enum?: any[] + // error message + message?: string + [key: string]: any +} +``` + + + +#### ValidateResponse + +```typescript +type SyncValidateResponse = + | null + | string + | boolean + | { + type?: 'error' | 'warning' + message: string + } +type AsyncValidateResponse = Promise +type ValidateResponse = SyncValidateResponse | AsyncValidateResponse +``` + + + +#### CustomValidator + +> Custom check function + +```typescript +type CustomValidator = ( + value: any, + rescription?: ValidateDescription +) => ValidateResponse +``` + + + + + +#### ValidatePatternRules + +> Verification rule set + +```typescript + type ValidatePatternRules = InternalFormats + | CustomValidator + | ValidateDescription + | Array< + InternalFormats | CustomValidator | ValidateDescription +> +``` + From 66dff2c0955d65b8e4a0c9bc7f29c09d844af2a3 Mon Sep 17 00:00:00 2001 From: Janry Date: Fri, 22 Nov 2019 23:05:16 +0800 Subject: [PATCH 54/99] Improve docs (#413) * test(@uform/react-schema-renderer): improve v0 test cases * feat(@uform/next/antd): support formStep * docs(@uform/react-schema-renderer): add english doc * docs(@uform/react-schema-renderer): improve readme * docs(@uform/react-schema-renderer): improve readme * docs(@uform/react-schema-renderer): improve readme --- README.md | 38 +- README.zh-cn.md | 39 +- docs/README.md | 74 +++ docs/SUMMARY.md | 42 +- packages/antd/README.zh-cn.md | 2 + packages/next/README.md | 75 --- packages/next/README.zh-cn.md | 2 + packages/react-schema-renderer/README.md | 496 +++++++--------- .../react-schema-renderer/README.zh-cn.md | 552 ++++++++---------- packages/react/README.md | 42 +- packages/react/README.zh-cn.md | 42 +- 11 files changed, 602 insertions(+), 802 deletions(-) create mode 100644 docs/README.md create mode 100644 packages/antd/README.zh-cn.md create mode 100644 packages/next/README.zh-cn.md diff --git a/README.md b/README.md index 144edbba111..d7383808302 100644 --- a/README.md +++ b/README.md @@ -38,15 +38,36 @@ Use Fusion Design: npm install --save @alifd/next @uform/next ``` +Use uform react render engine pacakge: + +```bash +npm install --save @uform/react-schema-renderer +``` + +Use uform react package: + +```bash +npm install --save @uform/react +``` + +Use UForm core package: + +```bash +npm install --save @uform/core +``` + + ## WebSite https://uformjs.org ## Documents -[@uform/react-schema-renderer](./packages/react-schema-renderder/README.md) -[@uform/react](./packages/react/README.md) -[@uform/core](./packages/core/README.md) +- [@uform/antd](./packages/antd/README.md) +- [@uform/next](./packages/next/README.md) +- [@uform/react-schema-renderer](./packages/react-schema-renderder/README.md) +- [@uform/react](./packages/react/README.md) +- [@uform/core](./packages/core/README.md) ## Demo @@ -77,17 +98,6 @@ Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/uform#sponsor)] - - - - - - - - - - - ## LICENSE diff --git a/README.zh-cn.md b/README.zh-cn.md index 204221a10ce..73221375f05 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -41,15 +41,35 @@ npm install --save antd @uform/antd npm install --save @alifd/next @uform/next ``` +使用UForm React渲染引擎包: + +```bash +npm install --save @uform/react-schema-renderer +``` + +使用 UForm React包: + +```bash +npm install --save @uform/react +``` + +使用 UForm 核心包: + +```bash +npm install --save @uform/core +``` + ## 官网 https://uformjs.org ## 文档 -[@uform/react-schema-renderer](./packages/react-schema-renderder/README.md) -[@uform/react](./packages/react/README.md) -[@uform/core](./packages/core/README.md) +- [@uform/antd](./packages/antd/README.md) +- [@uform/next](./packages/next/README.md) +- [@uform/react-schema-renderer](./packages/react-schema-renderder/README.md) +- [@uform/react](./packages/react/README.md) +- [@uform/core](./packages/core/README.md) ## 入门案例 @@ -80,19 +100,6 @@ Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/uform#sponsor)] - - - - - - - - - - - - - ## LICENSE diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000000..de72618f030 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,74 @@ + +## 背景 + +在React中,在受控模式下,表单的整树渲染问题非常明显。特别是对于数据联动的场景,很容易导致页面卡顿,为了解决这个问题,我们将每个表单字段的状态做了分布式管理,从而大大提升了表单操作性能。同时,我们深度整合了JSON Schema协议,可以帮助您快速解决后端驱动表单渲染的问题。 + +## 特性 + +- 🚀 高性能,字段分布式渲染,大大减轻 React 渲染压力 +- 💡 支持 Ant Design/Fusion Next 组件体系 +- 🎨 JSX 标签化写法/JSON Schema 数据驱动方案无缝迁移过渡 +- 🏅 副作用逻辑独立管理,涵盖各种复杂联动校验逻辑 +- 🌯 支持各种表单复杂布局方案 + +## 安装 + +使用 Ant Design: + +```bash +npm install --save antd @uform/antd +``` + +使用 Fusion Design: + +```bash +npm install --save @alifd/next @uform/next +``` + +使用UForm React渲染引擎包: + +```bash +npm install --save @uform/react-schema-renderer +``` + +使用 UForm React包: + +```bash +npm install --save @uform/react +``` + +使用 UForm 核心包: + +```bash +npm install --save @uform/core +``` + +## 社区 + + +| Online Chat Room | 微信 | 钉钉 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ---- | +| [gitter.im](https://gitter.im/alibaba-uform/community?source=orgpage) | | | + +## 贡献者 + +This project exists thanks to all the people who contribute. + + + +## 点个赞 + +Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/uform#backer)] + + + + +## 捐献我们 + +Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/uform#sponsor)] + + +## LICENSE + +UForm is open source software licensed as +[MIT.](https://github.com/alibaba/uform/blob/master/LICENSE.md) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 7fe1c8f2b7d..32e84cd15f4 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,39 +1,11 @@ - 开发指南 - - [UForm 是什么?](./Tutorials/UForm是什么.md) - - [UForm 原理](./Tutorials/UForm原理.md) - - [快速入门](./Tutorials/快速入门.md) - - [Form Schema 扩展规范](./Tutorials/FormSchema扩展规范.md) - - [常见问题](./Tutorials/Questions.md) -- API 文档 - - @uform/next or antd - - [](./API/SchemaForm.md) - - [](./API/FormButtonGroup.md) - - [](./API/Submit.md) - - [](./API/Reset.md) - - [](./API/FormLayout.md) - - [](./API/FormCard.md) - - [](./API/FormBlock.md) - - [](./API/FormItemGrid.md) - - [](./API/FormSlot.md) - - [](./API/FormTextBox.md) - - @uform/react - - [](./API/SchemaForm_React.md) - - [](./API/Field_React.md) - - [](./API/FormProvider.md) - - [](./API/FormConsumer.md) - - [registerFormField](./API/registerFormField.md) - - [registerFormFields](./API/registerFormFields.md) - - [registerFormWrapper](./API/registerFormWrapper.md) - - [registerFieldMiddleware](./API/registerFieldMiddleware.md) - - [createVirtualBox](./API/createVirtualBox.md) - - [connect](./API/connect.md) - - [createFormActions](./API/createFormActions.md) - - [createAsyncFormActions](./API/createAsyncFormActions.md) - - @uform/core - - [createForm](./API/createForm.md) - - [setValidationLocale](./API/setValidationLocale.md) - - [setValidationLanguage](./API/setValidationLanguage.md) - - [FormPath](./API/FormPath.md) + - [UForm 是什么?](./README.md) + - API 文档 + - [Antd扩展库](../packages/antd/README.zh-cn.md) + - [Fusion Next扩展库](../packages/next/README.zh-cn.md) + - [Schema渲染库](../packages/react-schema-renderer/README.zh-cn.md) + - [React核心库](../packages/react/README.zh-cn.md) + - [核心库](../packages/core/README.zh-cn.md) - 场景案例 - Fusion Next - [简单场景](./Examples/next/Sample.md) diff --git a/packages/antd/README.zh-cn.md b/packages/antd/README.zh-cn.md new file mode 100644 index 00000000000..c76e7635a8d --- /dev/null +++ b/packages/antd/README.zh-cn.md @@ -0,0 +1,2 @@ +# @uform/antd +> UForm Ant Design组件插件包 \ No newline at end of file diff --git a/packages/next/README.md b/packages/next/README.md index dabde78f31b..35f5c209246 100644 --- a/packages/next/README.md +++ b/packages/next/README.md @@ -1,77 +1,2 @@ # @uform/next - > UForm Fusion Next 组件插件包 - -```jsx -import { - SchemaForm, - Field, - FormButtonGroup, - Submit, - FormEffectHooks, - createFormActions, - FormGridRow, - FormItemGrid, - FormGridCol, - FormPath, - FormLayout, - FormBlock, - FormCard, - FormTextBox, - FormStep -} from './src/index' -import { Button } from '@alifd/next' -import '@alifd/next/dist/next.css' - -const { onFormInit$ } = FormEffectHooks - -const actions = createFormActions() - -export default () => ( - { - console.log('提交') - console.log(values) - }} - actions={actions} - labelCol={{ span: 8 }} - wrapperCol={{ span: 6 }} - validateFirst - effects={({ setFieldState, getFormGraph }) => { - onFormInit$().subscribe(() => { - setFieldState('col1', state => { - state.visible = false - }) - }) - FormStep.useEffects(['step-1', 'step-2', 'step-3']) - }} - > - - - - - - - - - - - - 提交 - - - - -) -``` diff --git a/packages/next/README.zh-cn.md b/packages/next/README.zh-cn.md new file mode 100644 index 00000000000..35f5c209246 --- /dev/null +++ b/packages/next/README.zh-cn.md @@ -0,0 +1,2 @@ +# @uform/next +> UForm Fusion Next 组件插件包 diff --git a/packages/react-schema-renderer/README.md b/packages/react-schema-renderer/README.md index a1552fb5e32..5696d051485 100644 --- a/packages/react-schema-renderer/README.md +++ b/packages/react-schema-renderer/README.md @@ -18,73 +18,73 @@ npm install --save @uform/react-schema-renderer - [Usage](#usage) - * [Quick start](#quick-start) - * [JSON Schema driver form rendering](#json-schema-driver-form-rendering) - * [JSchema driver form rendering](#jschema-driver-form-rendering) - * [Non-single instance registration component](#non-single-instance-registration-component) + - [`Quick start`](#quick-start) + - [`JSON Schema driver form rendering`](#json-schema-driver-form-rendering) + - [`JSchema driver form rendering`](#jschema-driver-form-rendering) + - [`Non-single instance registration component`](#non-single-instance-registration-component) - [Advanced tutorial](#advanced-tutorial) - * [How to access third-party component libraries?](#how-to-access-third-party-component-libraries) - * [How to access the Form/FormItem component?](#how-to-access-the-formformitem-component) - * [How to access form components?](#how-to-access-form-components) - * [How to handle form layout?](#how-to-handle-form-layout) - * [How to implement recursive rendering components?](#how-to-implement-recursive-rendering-components) - * [How to implement the auto-increment list component?](#how-to-implement-the-auto-increment-list-component) - * [How to implement ultra-complex custom components?](#how-to-implement-ultra-complex-custom-components) + - [`How to access third-party component libraries?`](#how-to-access-third-party-component-libraries) + - [`How to access the Form/FormItem component?`](#how-to-access-the-formformitem-component) + - [`How to access form components?`](#how-to-access-form-components) + - [`How to handle form layout?`](#how-to-handle-form-layout) + - [`How to implement recursive rendering components?`](#how-to-implement-recursive-rendering-components) + - [`How to implement the auto-increment list component?`](#how-to-implement-the-auto-increment-list-component) + - [`How to implement ultra-complex custom components?`](#how-to-implement-ultra-complex-custom-components) - [API](#api) - * [connect](#connect) - * [registerFormField](#registerformfield) - * [registerFormFields](#registerformfields) - * [registerFormComponent](#registerformcomponent) - * [registerFormItemComponent](#registerformitemcomponent) - * [registerVirtualBox](#registervirtualbox) - * [createVirtualBox](#createvirtualbox) - * [createControllerBox](#createcontrollerbox) - * [getRegistry](#getregistry) - * [cleanRegistry](#cleanregistry) + - [`connect`](#connect) + - [`registerFormField`](#registerformfield) + - [`registerFormFields`](#registerformfields) + - [`registerFormComponent`](#registerformcomponent) + - [`registerFormItemComponent`](#registerformitemcomponent) + - [`registerVirtualBox`](#registervirtualbox) + - [`createVirtualBox`](#createvirtualbox) + - [`createControllerBox`](#createcontrollerbox) + - [`getRegistry`](#getregistry) + - [`cleanRegistry`](#cleanregistry) - [Classes](#classes) - * [new Schema(json : ISchema)](#new-schemajson--ischema) - + [`get`](#get) - + [`merge `](#merge-) - + [`getEmptyValue`](#getemptyvalue) - + [`getSelfProps`](#getselfprops) - + [`getExtendsRules`](#getextendsrules) - + [`getExtendsRequired`](#getextendsrequired) - + [`getExtendsEditable`](#getextendseditable) - + [`getExtendsTriggerType`](#getextendstriggertype) - + [`getExtendsProps`](#getextendsprops) - + [`getExtendsComponent`](#getextendscomponent) - + [`getExtendsComponentProps`](#getextendscomponentprops) - + [`getExtendsRenderer`](#getextendsrenderer) - + [`getExtendsEffect`](#getextendseffect) - + [`setProperty`](#setproperty) - + [`setProperties`](#setproperties) - + [`setArrayItems`](#setarrayitems) - + [`getOrderProperties`](#getorderproperties) - + [`mapProperties`](#mapproperties) - + [`toJSON`](#tojson) - + [`fromJSON`](#fromjson) - + [`isObject`](#isobject) - + [`isArray()`](#isarray) + - [new Schema(json : ISchema)](#new-schemajson--ischema) + - [`get`](#get) + - [`merge`](#merge-) + - [`getEmptyValue`](#getemptyvalue) + - [`getSelfProps`](#getselfprops) + - [`getExtendsRules`](#getextendsrules) + - [`getExtendsRequired`](#getextendsrequired) + - [`getExtendsEditable`](#getextendseditable) + - [`getExtendsTriggerType`](#getextendstriggertype) + - [`getExtendsProps`](#getextendsprops) + - [`getExtendsComponent`](#getextendscomponent) + - [`getExtendsComponentProps`](#getextendscomponentprops) + - [`getExtendsRenderer`](#getextendsrenderer) + - [`getExtendsEffect`](#getextendseffect) + - [`setProperty`](#setproperty) + - [`setProperties`](#setproperties) + - [`setArrayItems`](#setarrayitems) + - [`getOrderProperties`](#getorderproperties) + - [`mapProperties`](#mapproperties) + - [`toJSON`](#tojson) + - [`fromJSON`](#fromjson) + - [`isObject`](#isobject) + - [`isArray()`](#isarray) - [Components](#components) - * [``](#SchemaForm) - * [``](#SchemaField) - * [``](#SchemaMarkupForm) - * [``](#SchemaMarkupField) - * [``](#InternalForm) - * [``](#InternalField) + - [``](#SchemaForm) + - [``](#SchemaField) + - [``](#SchemaMarkupForm) + - [``](#SchemaMarkupField) + - [``](#InternalForm) + - [``](#InternalField) - [Interfaces](#interfaces) - * [ISchemaFieldComponentProps](#ischemafieldcomponentprops) - * [ISchemaVirtualFieldComponentProps](#ischemavirtualfieldcomponentprops) - * [ISchemaFormRegistry](#ischemaformregistry) - * [ISchema](#ischema) - * [ISchemaFormActions](#ischemaformactions) - * [ISchemaFormAsyncActions](#ischemaformasyncactions) - * [IFormValidateResult](#iformvalidateresult) - * [InternalFormats](#internalformats) - * [ValidateDescription](#validatedescription) - * [ValidateResponse](#validateresponse) - * [CustomValidator](#customvalidator) - * [ValidatePatternRules](#validatepatternrules) + - [ISchemaFieldComponentProps](#ischemafieldcomponentprops) + - [ISchemaVirtualFieldComponentProps](#ischemavirtualfieldcomponentprops) + - [ISchemaFormRegistry](#ischemaformregistry) + - [ISchema](#ischema) + - [ISchemaFormActions](#ischemaformactions) + - [ISchemaFormAsyncActions](#ischemaformasyncactions) + - [IFormValidateResult](#iformvalidateresult) + - [InternalFormats](#internalformats) + - [ValidateDescription](#validatedescription) + - [ValidateResponse](#validateresponse) + - [CustomValidator](#customvalidator) + - [ValidatePatternRules](#validatepatternrules) @@ -96,7 +96,7 @@ npm install --save @uform/react-schema-renderer If you are developing directly based on @uform/react-schema-renderer, then you must register your custom component before development. Go to the renderer, otherwise our JSON-Schema protocol can't render the form. And so: -```jsx +```tsx import React from 'react' import { SchemaForm, @@ -208,7 +208,7 @@ As you can see, using JSchema to describe JSON Schema in your code is more elega In the previous example, we used the registerFormField API to register a custom component, which is the way to register a singleton,Its main advantage is the convenience, but there are also some problems, that is, the single case is easily contaminated, especially in the SPA page.If the developers of different pages are different, because sharing the same memory environment, then A developers may register custom components with the same name as B developers, which can easily lead to online failures.Therefore, we recommend users to use non-single registration methods: -```jsx +```tsx import React, { useMemo } from 'react' import { SchemaForm, @@ -614,7 +614,7 @@ registerFormField('array', ({ value, path, mutators }) => { }) ``` - To implement a self-incrementing list component with recursive rendering is super simple. On the contrary, it would be a bit cumbersome to implement related styles. In short, the core is to use the SchemaField component and the mutators API, the specific API will be described in detail later. +To implement a self-incrementing list component with recursive rendering is super simple. On the contrary, it would be a bit cumbersome to implement related styles. In short, the core is to use the SchemaField component and the mutators API, the specific API will be described in detail later. #### How to implement ultra-complex custom components? @@ -697,7 +697,7 @@ registerFormField('complex', ({ path }) => { }) ``` -In this pseudo-code, we mainly use two core APIs, mainly useFormEffects and InternalField,useFormEffects gives developers a place to write the effects logic locally, so that the effects logic can be easily reused. InternalField is the @uform/react Field component. You can take a look at the @uform/react documentation. SchemaForm also uses @uform/react internally, it can share the same Context, so we can easily use InternalField inside a custom component. Also note that **when using InternalField directly, the name we registered is the root level name, if you want to reuse the path of the current custom component, you can use FormPath to resolve the path, and then concat.** +In this pseudo-code, we mainly use two core APIs, mainly useFormEffects and InternalField,useFormEffects gives developers a place to write the effects logic locally, so that the effects logic can be easily reused. InternalField is the @uform/react Field component. You can take a look at the @uform/react documentation. SchemaForm also uses @uform/react internally, it can share the same Context, so we can easily use InternalField inside a custom component. Also note that **when using InternalField directly, the name we registered is the root level name, if you want to reuse the path of the current custom component, you can use FormPath to resolve the path, and then concat.** ### API @@ -723,8 +723,6 @@ import { Select } from 'antd' registerFormField('select', connect()(Select)) ``` - - #### registerFormField > Register custom component functions @@ -738,8 +736,6 @@ registerFormField( ) ``` - - #### registerFormFields > Bulk registration of custom components @@ -754,8 +750,6 @@ registerFormFields( ) ``` - - #### registerFormComponent > Register Form Style Component @@ -768,8 +762,6 @@ registerFormComponent( ) ``` - - #### registerFormItemComponent > Register the FormItem style component @@ -782,8 +774,6 @@ registerFormItemComponent( ) ``` - - #### registerVirtualBox > Register the virtual box component, mainly used to describe the layout in JSON Schema @@ -797,8 +787,6 @@ registerVirtualBox( ) ``` - - #### createVirtualBox > Create a virtual box component, the returned component can be used directly in SchemaMarkupForm @@ -836,8 +824,6 @@ export default () => ( ) ``` - - #### createControllerBox > Creating a virtual box component, the returned component can be used directly in SchemaMarkupForm, it is different from createVirtualBox mainly because the component receives the props @@ -877,8 +863,6 @@ export default () => ( ) ``` - - #### getRegistry > Get the registry, all components registered through the registerFormXXX API are managed in the registry @@ -889,8 +873,6 @@ export default () => ( getRegistry(): ISchemaFormRegistry ``` - - #### cleanRegistry > Clear the registry and clear all components registered via the registerFormXXX API @@ -901,8 +883,6 @@ getRegistry(): ISchemaFormRegistry cleanRegistry(): void ``` - - ### Classes > The whole Class inherits @uform/core completely, such as FormPath and FormLifeCyle. Only the classes specific to @uform/react-schema-renderer are listed below. @@ -913,45 +893,43 @@ cleanRegistry(): void **Attributes** -| Attribute name | Description | Type | -| -------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | -| title | Field title | `React.ReactNode` | -| description | Field description | `React.ReactNode` | -| default | Field default | `any` | -| readOnly | Whether read-only and editable | `boolean` | -| type | Field Type | `'string' | 'object' | 'array' | 'number' | string` | -| enum | Enumerate data | `Array` | -| const | Check if the field value is equal to the value of const | `any` | -| multipleOf | Check if the field value can be divisible by the value of multipleOf | `number` | -| maximum | Check the maximum value (greater than) | `number` | -| exclusiveMaximum | Check the maximum value (greater than or equal to) | `number` | -| minimum | Check minimum value (less than) | `number` | -| exclusiveMinimum | Minimum value (less than or equal to) | `number` | -| maxLength | Check maximum length | `number` | -| minLength | Check minimum length | `number` | -| pattern | Regular check rule | `string | RegExp` | -| maxItems | Maximum number of entries | `number` | -| minItems | Minimum number of entries | `number` | -| uniqueItems | Whether to check for duplicates | `boolean` | -| maxProperties | Maximum number of attributes | `number` | -| minProperties | Minimum number of attributes | `number` | -| required | Required | `boolean` | -| format | Regular rule type | `InternalFormats` | -| properties | Object property | `{[key : string]:Schema}` | -| items | Array description | `Schema | Schema[]` | -| additionalItems | Extra array element description | `Schema` | -| patternProperties | Dynamically match the schema of an attribute of an object | `{[key : string]:Schema}` | -| additionalProperties | Schema matching the extra attributes of the object | `Schema` | -| editable | Whether the field is editable | `boolean` | -| x-props | Field extension attribute | `{ [name: string]: any }` | -| x-index | Field order | `number` | -| x-rules | Field check rule | `ValidatePatternRules` | -| x-component | Field UI component | `string` | -| x-component-props | Field UI component properties | `{}` | -| x-render | Field extension rendering function | `(props: T & { renderComponent: () => React.ReactElement}) => React.ReactElement` | -| x-effect | Field side effect trigger | `(dispatch: (type: string, payload: any) => void,option?:object) => { [key: string]: any }` | - - +| Attribute name | Description | Type | +| -------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| title | Field title | `React.ReactNode` | +| description | Field description | `React.ReactNode` | +| default | Field default | `any` | +| readOnly | Whether read-only and editable | `boolean` | +| type | Field Type | `'string' | 'object' | 'array' | 'number' | string` | +| enum | Enumerate data | `Array` | +| const | Check if the field value is equal to the value of const | `any` | +| multipleOf | Check if the field value can be divisible by the value of multipleOf | `number` | +| maximum | Check the maximum value (greater than) | `number` | +| exclusiveMaximum | Check the maximum value (greater than or equal to) | `number` | +| minimum | Check minimum value (less than) | `number` | +| exclusiveMinimum | Minimum value (less than or equal to) | `number` | +| maxLength | Check maximum length | `number` | +| minLength | Check minimum length | `number` | +| pattern | Regular check rule | `string | RegExp` | +| maxItems | Maximum number of entries | `number` | +| minItems | Minimum number of entries | `number` | +| uniqueItems | Whether to check for duplicates | `boolean` | +| maxProperties | Maximum number of attributes | `number` | +| minProperties | Minimum number of attributes | `number` | +| required | Required | `boolean` | +| format | Regular rule type | `InternalFormats` | +| properties | Object property | `{[key : string]:Schema}` | +| items | Array description | `Schema | Schema[]` | +| additionalItems | Extra array element description | `Schema` | +| patternProperties | Dynamically match the schema of an attribute of an object | `{[key : string]:Schema}` | +| additionalProperties | Schema matching the extra attributes of the object | `Schema` | +| editable | Whether the field is editable | `boolean` | +| x-props | Field extension attribute | `{ [name: string]: any }` | +| x-index | Field order | `number` | +| x-rules | Field check rule | `ValidatePatternRules` | +| x-component | Field UI component | `string` | +| x-component-props | Field UI component properties | `{}` | +| x-render | Field extension rendering function | `(props: T & { renderComponent: () => React.ReactElement}) => React.ReactElement` | +| x-effect | Field side effect trigger | `(dispatch: (type: string, payload: any) => void,option?:object) => { [key: string]: any }` | **Method** @@ -969,15 +947,15 @@ Usage ```typescript const schema = new Schema({ - type:"object", - properties:{ - array:{ - type:'array', - items:{ - type:'object', - properties:{ - input:{ - type:'string' + type: 'object', + properties: { + array: { + type: 'array', + items: { + type: 'object', + properties: { + input: { + type: 'string' } } } @@ -988,9 +966,7 @@ const schema = new Schema({ schema.get('array[0].input') //{type:"string"} ``` - - -##### `merge ` +##### `merge` > Merge Schema @@ -1004,15 +980,15 @@ Usage ```typescript const schema = new Schema({ - type:"object", - properties:{ - array:{ - type:'array', - items:{ - type:'object', - properties:{ - input:{ - type:'string' + type: 'object', + properties: { + array: { + type: 'array', + items: { + type: 'object', + properties: { + input: { + type: 'string' } } } @@ -1021,7 +997,7 @@ const schema = new Schema({ }) schema.merge({ - title:'root object' + title: 'root object' }) /** { @@ -1044,8 +1020,6 @@ schema.merge({ **/ ``` - - ##### `getEmptyValue` > Get the null value of the current Schema based on the type of Schema @@ -1060,15 +1034,15 @@ Usage ```typescript const schema = new Schema({ - type:"object", - properties:{ - array:{ - type:'array', - items:{ - type:'object', - properties:{ - input:{ - type:'string' + type: 'object', + properties: { + array: { + type: 'array', + items: { + type: 'object', + properties: { + input: { + type: 'string' } } } @@ -1080,11 +1054,8 @@ schema.get('array.0.input').getEmptyValue() // '' schema.get('array.0').getEmptyValue() // {} schema.get('array').getEmptyValue() // [] schema.getEmptyValue() // {} - ``` - - ##### `getSelfProps` > Get no nested Schema properties (does not include nested properties like properties/items) @@ -1099,15 +1070,15 @@ Usage ```typescript const schema = new Schema({ - type:"object", - properties:{ - array:{ - type:'array', - items:{ - type:'object', - properties:{ - input:{ - type:'string' + type: 'object', + properties: { + array: { + type: 'array', + items: { + type: 'object', + properties: { + input: { + type: 'string' } } } @@ -1116,11 +1087,8 @@ const schema = new Schema({ }) schema.getSelfProps() // { type:"object" } - ``` - - ##### `getExtendsRules` > Get the extended check rule. This method is more complicated. It will parse the attributes and x-rules attributes of all the check types of the current Schema, and finally merge them into a unified rules structure. @@ -1146,8 +1114,6 @@ const schema = new Schema({ schema.getExtendsRules() // [{required:true},{max:10},{pattern:/^\d+$/}] ``` - - ##### `getExtendsRequired` > Obtaining whether it is required or not is actually reading the required attribute of the Schema. Why is it encapsulated into a method to ensure that the user is not aware of the protocol upgrade? We only need to ensure backward compatibility of the method. @@ -1173,8 +1139,6 @@ const schema = new Schema({ schema.getExtendsRequired() // true ``` - - ##### `getExtendsEditable` > Get the editable state of the Schema, consistent with the getExtendsEditable ability, also to smooth out the protocol differences @@ -1189,40 +1153,38 @@ Usage ```typescript const schema1 = new Schema({ - type:"string", - editable:false + type: 'string', + editable: false }) schema1.getExtendsEditable() // false const schema2 = new Schema({ - type:"string", - readOnly:true + type: 'string', + readOnly: true }) schema2.getExtendsEditable() // false const schema3 = new Schema({ - type:"string", - "x-props":{ - editable:false + type: 'string', + 'x-props': { + editable: false } }) schema3.getExtendsEditable() // false const schema4 = new Schema({ - type:"string", - "x-component-props":{ - editable:false + type: 'string', + 'x-component-props': { + editable: false } }) schema4.getExtendsEditable() // false ``` - - ##### `getExtendsTriggerType` > Get the triggerType, which is consistent with the getExtendsTriggerType capability, and is capable of providing protocol differences. @@ -1237,27 +1199,27 @@ Usage ```typescript const schema1 = new Schema({ - type:"string", - "x-props":{ - triggerType:"onBlur" + type: 'string', + 'x-props': { + triggerType: 'onBlur' } }) schema1.getExtendsTriggerType() // onBlur const schema2 = new Schema({ - type:"string", - "x-component-props":{ - triggerType:"onBlur" + type: 'string', + 'x-component-props': { + triggerType: 'onBlur' } }) schema2.getExtendsTriggerType() // onBlur const schema3 = new Schema({ - type:"string", - "x-item-props":{ - triggerType:"onBlur" + type: 'string', + 'x-item-props': { + triggerType: 'onBlur' } }) @@ -1274,8 +1236,6 @@ Signature getExtendsProps() : {} ``` - - ##### `getExtendsComponent` > Get the x-props attribute @@ -1286,8 +1246,6 @@ Signature getExtendsComponent() : string ``` - - ##### `getExtendsComponentProps` > Get the x-component-props property, which is the component property of the x-component @@ -1298,8 +1256,6 @@ Signature getExtendsComponentProps() : {} ``` - - ##### `getExtendsRenderer` > Get the x-render attribute @@ -1314,8 +1270,6 @@ getExtendsRenderer() : ( ) => React.ReactElement ``` - - ##### `getExtendsEffect` > Get the x-effect attribute @@ -1329,8 +1283,6 @@ getExtendsEffect() : ( ) => { [key: string]: any } ``` - - ##### `setProperty` > Set properties for the current schema @@ -1341,8 +1293,6 @@ Signature setProperty(key: string, schema: ISchema): Schema ``` - - ##### `setProperties` > Set properties for the current Schema batch @@ -1353,8 +1303,6 @@ Signature setProperties(properties: {[key : string]:ISchema}) : {[key : string]:Schema} ``` - - ##### `setArrayItems` > Set the items property to the current schema @@ -1365,8 +1313,6 @@ Signature setArrayItems(schema:Ischema) : Schema ``` - - ##### `getOrderProperties` > Give all properties in x-index order @@ -1377,8 +1323,6 @@ Signature getOrderProperties() : {schema:Schema,key:string}[] ``` - - ##### `mapProperties` > Traverse the properties of the schema in order (x-index) @@ -1389,8 +1333,6 @@ Signature mapProperties(callback?: (schema: Schema, key: string) => any):any[] ``` - - ##### `toJSON` > Output no loop dependent json data structure @@ -1401,8 +1343,6 @@ Signature toJSON() : ISchema ``` - - ##### `fromJSON` > Generate a Schema object based on a json parsing @@ -1413,8 +1353,6 @@ Signature fromJSON(json : ISchema) : Schema ``` - - ##### `isObject` > Determine whether the current schema is an object type @@ -1425,8 +1363,6 @@ Signature isObject() : boolean ``` - - ##### `isArray()` > Determine if the current schema is an array type @@ -1437,8 +1373,6 @@ Signature isArray() : boolean ``` - - ### Components --- @@ -1459,33 +1393,33 @@ interface ISchemaFormProps< FormActions = ISchemaFormActions | ISchemaFormAsyncActions > { //Form value - value?: Value; + value?: Value //Form default value - defaultValue?: DefaultValue; + defaultValue?: DefaultValue //Form default value, weakly controlled - initialValues?: DefaultValue; + initialValues?: DefaultValue //Form actions - actions?: FormActions; + actions?: FormActions //Form effects - effects?: IFormEffect; + effects?: IFormEffect //Form instance - form?: IForm; + form?: IForm //Form change callback - onChange?: (values: Value) => void; + onChange?: (values: Value) => void //Form submission callback - onSubmit?: (values: Value) => void | Promise; + onSubmit?: (values: Value) => void | Promise //Form reset callback - onReset?: () => void; + onReset?: () => void //Form validation failure callback - onValidateFailed?: (valideted: IFormValidateResult) => void; + onValidateFailed?: (valideted: IFormValidateResult) => void //Form child node - children?: React.ReactElement; + children?: React.ReactElement //Whether to open the dirty check - useDirty?: boolean; + useDirty?: boolean //Is it editable - editable?: boolean | ((name: string) => boolean); + editable?: boolean | ((name: string) => boolean) //Whether to enable pessimistic verification, if the first verification fails, stop the remaining verification - validateFirst?: boolean; + validateFirst?: boolean //Form Schema object schema?: ISchema //Instance level registration custom component @@ -1499,8 +1433,6 @@ interface ISchemaFormProps< } ``` - - #### `` > Based on a Data Path, it automatically finds and renders the internal components of the Schema node, mainly used to implement recursive rendering within the custom component. @@ -1514,8 +1446,6 @@ interface ISchemaFieldProps { } ``` - - #### `` > Let SchemaForm support the Form component of jsx tabbed notation, which needs to be used together with SchemaMarkupField @@ -1530,33 +1460,33 @@ interface ISchemaFormProps< FormActions = ISchemaFormActions | ISchemaFormAsyncActions > { //Form value - value?: Value; + value?: Value //Form default value - defaultValue?: DefaultValue; + defaultValue?: DefaultValue //Form default value, weakly controlled - initialValues?: DefaultValue; + initialValues?: DefaultValue //Form actions - actions?: FormActions; + actions?: FormActions //Form effects - effects?: IFormEffect; + effects?: IFormEffect //Form instance - form?: IForm; + form?: IForm //Form change callback - onChange?: (values: Value) => void; + onChange?: (values: Value) => void //Form submission callback - onSubmit?: (values: Value) => void | Promise; + onSubmit?: (values: Value) => void | Promise //Form reset callback - onReset?: () => void; + onReset?: () => void //Form validation failure callback - onValidateFailed?: (valideted: IFormValidateResult) => void; + onValidateFailed?: (valideted: IFormValidateResult) => void //Form child node - children?: React.ReactElement; + children?: React.ReactElement //Whether to open the dirty check - useDirty?: boolean; + useDirty?: boolean //Is it editable - editable?: boolean | ((name: string) => boolean); + editable?: boolean | ((name: string) => boolean) //Whether to enable pessimistic verification, if the first verification fails, stop the remaining verification - validateFirst?: boolean; + validateFirst?: boolean //Form Schema object schema?: ISchema //Instance level registration custom component @@ -1578,10 +1508,10 @@ import { SchemaMarkupField as Field } from '@uform/react-schema-renderer' -export default ()=>{ +export default () => { return ( - + ) } @@ -1594,17 +1524,13 @@ export default ()=>{ **Attributes** ```typescript -type IMarkupSchemaFieldProps = ISchema +type IMarkupSchemaFieldProps = ISchema ``` - - #### `` > The core Form is the same as the Form component in @uform/react - - #### `` > The core Field, like the Field component in @uform/react, is mainly used in complex custom components. @@ -1687,8 +1613,6 @@ interface ISchemaFieldComponentProps { } ``` - - #### ISchemaVirtualFieldComponentProps > The attributes received by the virtual field component, as long as it involves registering the virtual field, you need to understand the protocol. @@ -1727,8 +1651,6 @@ interface ISchemaVirtualFieldComponentProps { } ``` - - #### ISchemaFormRegistry > The component registry, whether it is a normal field, a virtual field, or a Form/FormItem will be registered here. @@ -1737,17 +1659,17 @@ interface ISchemaVirtualFieldComponentProps { interface ISchemaFormRegistry { fields: { [key: string]: React.JSXElementConstructor - }, + } virtualFields: { - [key: string]: React.JSXElementConstructor - }, + [key: string]: React.JSXElementConstructor< + ISchemaVirtualFieldComponentProps + > + } formItemComponent: React.JSXElementConstructor formComponent: string | React.JSXElementConstructor } ``` - - #### ISchema > Schema protocol object, mainly used to constrain a json structure to satisfy the Schema protocol @@ -1761,7 +1683,7 @@ interface ISchema { readOnly?: boolean writeOnly?: boolean type?: 'string' | 'object' | 'array' | 'number' | string - enum?: Array + enum?: Array const?: any multipleOf?: number maximum?: number @@ -1807,8 +1729,6 @@ interface ISchema { } ``` - - #### ISchemaFormActions > The core actions inherit @uform/react's IFormActions, mainly adding the getSchema API @@ -1831,8 +1751,6 @@ interface ISchemaFormAsyncActions extends IFormAsyncActions { } ``` - - #### IFormValidateResult > Calibration result @@ -1850,8 +1768,6 @@ interface IFormValidateResult { } ``` - - #### InternalFormats > Built-in format checksum @@ -1873,8 +1789,6 @@ type InternalFormats = | string ``` - - #### ValidateDescription > Original check description @@ -1913,8 +1827,6 @@ interface ValidateDescription { } ``` - - #### ValidateResponse ```typescript @@ -1930,8 +1842,6 @@ type AsyncValidateResponse = Promise type ValidateResponse = SyncValidateResponse | AsyncValidateResponse ``` - - #### CustomValidator > Custom check function @@ -1943,20 +1853,14 @@ type CustomValidator = ( ) => ValidateResponse ``` - - - - #### ValidatePatternRules > Verification rule set ```typescript - type ValidatePatternRules = InternalFormats +type ValidatePatternRules = + | InternalFormats | CustomValidator | ValidateDescription - | Array< - InternalFormats | CustomValidator | ValidateDescription -> + | Array ``` - diff --git a/packages/react-schema-renderer/README.zh-cn.md b/packages/react-schema-renderer/README.zh-cn.md index 243228508e8..4a3cb1450ab 100644 --- a/packages/react-schema-renderer/README.zh-cn.md +++ b/packages/react-schema-renderer/README.zh-cn.md @@ -15,54 +15,54 @@ npm install --save @uform/react-schema-renderer -- [使用方式](#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F) - * [快速开始](#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B) - * [JSON Schema 驱动表单渲染](#json-schema-%E9%A9%B1%E5%8A%A8%E8%A1%A8%E5%8D%95%E6%B8%B2%E6%9F%93) - * [JSchema 驱动表单渲染](#jschema-%E9%A9%B1%E5%8A%A8%E8%A1%A8%E5%8D%95%E6%B8%B2%E6%9F%93) - * [非单例注册组件](#%E9%9D%9E%E5%8D%95%E4%BE%8B%E6%B3%A8%E5%86%8C%E7%BB%84%E4%BB%B6) +- [使用方式](#/%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F) + - [`快速开始`](#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B) + - [`JSON Schema 驱动表单渲染`](#json-schema-%E9%A9%B1%E5%8A%A8%E8%A1%A8%E5%8D%95%E6%B8%B2%E6%9F%93) + - [`JSchema 驱动表单渲染`](#jschema-%E9%A9%B1%E5%8A%A8%E8%A1%A8%E5%8D%95%E6%B8%B2%E6%9F%93) + - [`非单例注册组件`](#%E9%9D%9E%E5%8D%95%E4%BE%8B%E6%B3%A8%E5%86%8C%E7%BB%84%E4%BB%B6) - [高级教程](#%E9%AB%98%E7%BA%A7%E6%95%99%E7%A8%8B) - * [如何接入第三方组件库?](#%E5%A6%82%E4%BD%95%E6%8E%A5%E5%85%A5%E7%AC%AC%E4%B8%89%E6%96%B9%E7%BB%84%E4%BB%B6%E5%BA%93) - * [如何接入 Form/FormItem 组件?](#%E5%A6%82%E4%BD%95%E6%8E%A5%E5%85%A5-formformitem-%E7%BB%84%E4%BB%B6) - * [如何接入表单组件?](#%E5%A6%82%E4%BD%95%E6%8E%A5%E5%85%A5%E8%A1%A8%E5%8D%95%E7%BB%84%E4%BB%B6) - * [如何处理表单布局?](#%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86%E8%A1%A8%E5%8D%95%E5%B8%83%E5%B1%80) - * [如何实现递归渲染组件?](#%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E9%80%92%E5%BD%92%E6%B8%B2%E6%9F%93%E7%BB%84%E4%BB%B6) - * [如何实现自增列表组件?](#%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%A2%9E%E5%88%97%E8%A1%A8%E7%BB%84%E4%BB%B6) - * [如何实现超复杂自定义组件?](#%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E8%B6%85%E5%A4%8D%E6%9D%82%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%84%E4%BB%B6) + - [`如何接入第三方组件库?`](#%E5%A6%82%E4%BD%95%E6%8E%A5%E5%85%A5%E7%AC%AC%E4%B8%89%E6%96%B9%E7%BB%84%E4%BB%B6%E5%BA%93) + - [`如何接入 Form/FormItem 组件?`](#%E5%A6%82%E4%BD%95%E6%8E%A5%E5%85%A5-formformitem-%E7%BB%84%E4%BB%B6) + - [`如何接入表单组件?`](#%E5%A6%82%E4%BD%95%E6%8E%A5%E5%85%A5%E8%A1%A8%E5%8D%95%E7%BB%84%E4%BB%B6) + - [`如何处理表单布局?`](#%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86%E8%A1%A8%E5%8D%95%E5%B8%83%E5%B1%80) + - [`如何实现递归渲染组件?`](#%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E9%80%92%E5%BD%92%E6%B8%B2%E6%9F%93%E7%BB%84%E4%BB%B6) + - [`如何实现自增列表组件?`](#%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%A2%9E%E5%88%97%E8%A1%A8%E7%BB%84%E4%BB%B6) + - [`如何实现超复杂自定义组件?`](#%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E8%B6%85%E5%A4%8D%E6%9D%82%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%84%E4%BB%B6) - [API](#api) - * [connect](#connect) - * [registerFormField](#registerformfield) - * [registerFormFields](#registerformfields) - * [registerFormComponent](#registerformcomponent) - * [registerFormItemComponent](#registerformitemcomponent) - * [registerVirtualBox](#registervirtualbox) - * [createVirtualBox](#createvirtualbox) - * [createControllerBox](#createcontrollerbox) - * [getRegistry](#getregistry) - * [cleanRegistry](#cleanregistry) + - [`connect`](#connect) + - [`registerFormField`](#registerformfield) + - [`registerFormFields`](#registerformfields) + - [`registerFormComponent`](#registerformcomponent) + - [`registerFormItemComponent`](#registerformitemcomponent) + - [`registerVirtualBox`](#registervirtualbox) + - [`createVirtualBox`](#createvirtualbox) + - [`createControllerBox`](#createcontrollerbox) + - [`getRegistry`](#getregistry) + - [`cleanRegistry`](#cleanregistry) - [Classes](#classes) - * [new Schema(json : ISchema)](#new-schemajson--ischema) - + [`get`](#get) - + [`merge `](#merge-) - + [`getEmptyValue`](#getemptyvalue) - + [`getSelfProps`](#getselfprops) - + [`getExtendsRules`](#getextendsrules) - + [`getExtendsRequired`](#getextendsrequired) - + [`getExtendsEditable`](#getextendseditable) - + [`getExtendsTriggerType`](#getextendstriggertype) - + [`getExtendsProps`](#getextendsprops) - + [`getExtendsComponent`](#getextendscomponent) - + [`getExtendsComponentProps`](#getextendscomponentprops) - + [`getExtendsRenderer`](#getextendsrenderer) - + [`getExtendsEffect`](#getextendseffect) - + [`setProperty`](#setproperty) - + [`setProperties`](#setproperties) - + [`setArrayItems`](#setarrayitems) - + [`getOrderProperties`](#getorderproperties) - + [`mapProperties`](#mapproperties) - + [`toJSON`](#tojson) - + [`fromJSON`](#fromjson) - + [`isObject`](#isobject) - + [`isArray()`](#isarray) + - [new Schema(json : ISchema)](#new-schemajson--ischema) + - [`get`](#get) + - [`merge`](#merge-) + - [`getEmptyValue`](#getemptyvalue) + - [`getSelfProps`](#getselfprops) + - [`getExtendsRules`](#getextendsrules) + - [`getExtendsRequired`](#getextendsrequired) + - [`getExtendsEditable`](#getextendseditable) + - [`getExtendsTriggerType`](#getextendstriggertype) + - [`getExtendsProps`](#getextendsprops) + - [`getExtendsComponent`](#getextendscomponent) + - [`getExtendsComponentProps`](#getextendscomponentprops) + - [`getExtendsRenderer`](#getextendsrenderer) + - [`getExtendsEffect`](#getextendseffect) + - [`setProperty`](#setproperty) + - [`setProperties`](#setproperties) + - [`setArrayItems`](#setarrayitems) + - [`getOrderProperties`](#getorderproperties) + - [`mapProperties`](#mapproperties) + - [`toJSON`](#tojson) + - [`fromJSON`](#fromjson) + - [`isObject`](#isobject) + - [`isArray()`](#isarray) - [Components](#components) - [``](#SchemaForm) - [``](#SchemaField) @@ -71,18 +71,18 @@ npm install --save @uform/react-schema-renderer - [``](#InternalForm) - [``](#InternalField) - [Interfaces](#interfaces) - * [ISchemaFieldComponentProps](#ischemafieldcomponentprops) - * [ISchemaVirtualFieldComponentProps](#ischemavirtualfieldcomponentprops) - * [ISchemaFormRegistry](#ischemaformregistry) - * [ISchema](#ischema) - * [ISchemaFormActions](#ischemaformactions) - * [ISchemaFormAsyncActions](#ischemaformasyncactions) - * [IFormValidateResult](#iformvalidateresult) - * [InternalFormats](#internalformats) - * [ValidateDescription](#validatedescription) - * [ValidateResponse](#validateresponse) - * [CustomValidator](#customvalidator) - * [ValidatePatternRules](#validatepatternrules) + - [`ISchemaFieldComponentProps`](#ischemafieldcomponentprops) + - [`ISchemaVirtualFieldComponentProps`](#ischemavirtualfieldcomponentprops) + - [`ISchemaFormRegistry`](#ischemaformregistry) + - [`ISchema`](#ischema) + - [`ISchemaFormActions`](#ischemaformactions) + - [`ISchemaFormAsyncActions`](#ischemaformasyncactions) + - [`IFormValidateResult`](#iformvalidateresult) + - [`InternalFormats`](#internalformats) + - [`ValidateDescription`](#validatedescription) + - [`ValidateResponse`](#validateresponse) + - [`CustomValidator`](#customvalidator) + - [`ValidatePatternRules`](#validatepatternrules) @@ -94,7 +94,7 @@ npm install --save @uform/react-schema-renderer 如果您是直接基于@uform/react-schema-renderer 做开发的,那么您必须在开发前将自定义组件注册到渲染器里去,否则,我们的 JSON-Schema 协议是不能渲染表单的。所以: -```jsx +```tsx import React from 'react' import { SchemaForm, @@ -206,7 +206,7 @@ export default () => { 在前面的例子中,我们使用了 registerFormField API 来注册了自定义组件,这种方式是单例注册的方式,它的主要优点就是方便,但是也会存在一些问题,就是单例容易受污染,特别是在 SPA 页面中,如果不同页面的开发者是不一样的,因为共享同一个内存环境,那么 A 开发者可能会注册 B 开发者同名的自定义组件,这样就很容易导致线上故障,所以,我们更加推荐用户使用非单例注册方式: -```jsx +```tsx import React, { useMemo } from 'react' import { SchemaForm, @@ -721,8 +721,6 @@ import { Select } from 'antd' registerFormField('select', connect()(Select)) ``` - - #### registerFormField > 注册自定义组件函数 @@ -736,8 +734,6 @@ registerFormField( ) ``` - - #### registerFormFields > 批量注册自定义组件 @@ -752,8 +748,6 @@ registerFormFields( ) ``` - - #### registerFormComponent > 注册 Form 样式组件 @@ -766,8 +760,6 @@ registerFormComponent( ) ``` - - #### registerFormItemComponent > 注册 FormItem 样式组件 @@ -780,8 +772,6 @@ registerFormItemComponent( ) ``` - - #### registerVirtualBox > 注册虚拟盒子组件,主要用于在 JSON Schema 中描述布局 @@ -795,8 +785,6 @@ registerVirtualBox( ) ``` - - #### createVirtualBox > 创建一个虚拟盒子组件,返回的组件可以在 SchemaMarkupForm 中直接使用 @@ -834,8 +822,6 @@ export default () => ( ) ``` - - #### createControllerBox > 创建一个虚拟盒子组件,返回的组件可以在 SchemaMarkupForm 中直接使用,它与 createVirtualBox 的不同主要是组件接收的 props @@ -875,11 +861,9 @@ export default () => ( ) ``` - - #### getRegistry -> 获取注册中心,所有通过registerFormXXX API 注册的组件都统一在registry中管理 +> 获取注册中心,所有通过 registerFormXXX API 注册的组件都统一在 registry 中管理 **签名** @@ -887,11 +871,9 @@ export default () => ( getRegistry(): ISchemaFormRegistry ``` - - #### cleanRegistry -> 清空注册中心,清除所有通过registerFormXXX API注册的组件 +> 清空注册中心,清除所有通过 registerFormXXX API 注册的组件 **签名** @@ -899,63 +881,59 @@ getRegistry(): ISchemaFormRegistry cleanRegistry(): void ``` - - ### Classes > 整体 Class 完全继承@uform/core,比如 FormPath 与 FormLifeCyle,下面只列举@uform/react-schema-renderer 特有的 Class #### new Schema(json : ISchema) -> Schema解析引擎,给定一份满足JSON Schema的数据,我们会将其解析成对应的Schema实例,可以借助一些工具方法快速处理一些事情,同时该Schema Class提供了统一的协议差异抹平能力,保证协议升级的时候可以无缝平滑升级 +> Schema 解析引擎,给定一份满足 JSON Schema 的数据,我们会将其解析成对应的 Schema 实例,可以借助一些工具方法快速处理一些事情,同时该 Schema Class 提供了统一的协议差异抹平能力,保证协议升级的时候可以无缝平滑升级 **属性** -| 属性名 | 描述 | 类型 | -| -------------------- | ------------------------------------ | ------------------------------------------------------------ | -| title | 字段标题 | `React.ReactNode` | -| description | 字段描述 | `React.ReactNode` | -| default | 字段默认值 | `any` | -| readOnly | 是否只读与editable一致 | `boolean` | -| type | 字段类型 | `'string' | 'object' | 'array' | 'number' | string` | -| enum | 枚举数据 | `Array` | -| const | 校验字段值是否与const的值相等 | `any` | -| multipleOf | 校验字段值是否可被multipleOf的值整除 | `number` | -| maximum | 校验最大值(大于) | `number` | -| exclusiveMaximum | 校验最大值(大于等于) | `number` | -| minimum | 校验最小值(小于) | `number` | -| exclusiveMinimum | 最小值(小于等于) | `number` | -| maxLength | 校验最大长度 | `number` | -| minLength | 校验最小长度 | `number` | -| pattern | 正则校验规则 | `string | RegExp` | -| maxItems | 最大条目数 | `number` | -| minItems | 最小条目数 | `number` | -| uniqueItems | 是否校验重复 | `boolean` | -| maxProperties | 最大属性数量 | `number` | -| minProperties | 最小属性数量 | `number` | -| required | 必填 | `boolean` | -| format | 正则规则类型 | `InternalFormats` | -| properties | 对象属性 | `{[key : string]:Schema}` | -| items | 数组描述 | `Schema | Schema[]` | -| additionalItems | 额外数组元素描述 | `Schema` | -| patternProperties | 动态匹配对象的某个属性的Schema | `{[key : string]:Schema}` | -| additionalProperties | 匹配对象额外属性的Schema | `Schema` | -| editable | 字段是否可编辑 | `boolean` | -| x-props | 字段扩展属性 | `{ [name: string]: any }` | -| x-index | 字段顺序 | `number` | -| x-rules | 字段校验规则 | `ValidatePatternRules` | -| x-component | 字段UI组件 | `string` | -| x-component-props | 字段UI组件属性 | `{}` | -| x-render | 字段扩展渲染函数 | `(props: T & { renderComponent: () => React.ReactElement}) => React.ReactElement` | -| x-effect | 字段副作用触发器 | `(dispatch: (type: string, payload: any) => void,option?:object) => { [key: string]: any }` | - - +| 属性名 | 描述 | 类型 | +| -------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| title | 字段标题 | `React.ReactNode` | +| description | 字段描述 | `React.ReactNode` | +| default | 字段默认值 | `any` | +| readOnly | 是否只读与 editable 一致 | `boolean` | +| type | 字段类型 | `'string' | 'object' | 'array' | 'number' | string` | +| enum | 枚举数据 | `Array` | +| const | 校验字段值是否与 const 的值相等 | `any` | +| multipleOf | 校验字段值是否可被 multipleOf 的值整除 | `number` | +| maximum | 校验最大值(大于) | `number` | +| exclusiveMaximum | 校验最大值(大于等于) | `number` | +| minimum | 校验最小值(小于) | `number` | +| exclusiveMinimum | 最小值(小于等于) | `number` | +| maxLength | 校验最大长度 | `number` | +| minLength | 校验最小长度 | `number` | +| pattern | 正则校验规则 | `string | RegExp` | +| maxItems | 最大条目数 | `number` | +| minItems | 最小条目数 | `number` | +| uniqueItems | 是否校验重复 | `boolean` | +| maxProperties | 最大属性数量 | `number` | +| minProperties | 最小属性数量 | `number` | +| required | 必填 | `boolean` | +| format | 正则规则类型 | `InternalFormats` | +| properties | 对象属性 | `{[key : string]:Schema}` | +| items | 数组描述 | `Schema | Schema[]` | +| additionalItems | 额外数组元素描述 | `Schema` | +| patternProperties | 动态匹配对象的某个属性的 Schema | `{[key : string]:Schema}` | +| additionalProperties | 匹配对象额外属性的 Schema | `Schema` | +| editable | 字段是否可编辑 | `boolean` | +| x-props | 字段扩展属性 | `{ [name: string]: any }` | +| x-index | 字段顺序 | `number` | +| x-rules | 字段校验规则 | `ValidatePatternRules` | +| x-component | 字段 UI 组件 | `string` | +| x-component-props | 字段 UI 组件属性 | `{}` | +| x-render | 字段扩展渲染函数 | `(props: T & { renderComponent: () => React.ReactElement}) => React.ReactElement` | +| x-effect | 字段副作用触发器 | `(dispatch: (type: string, payload: any) => void,option?:object) => { [key: string]: any }` | **方法** ##### `get` -> 根据数据路径获取Schema子节点 +> 根据数据路径获取 Schema 子节点 签名 @@ -967,15 +945,15 @@ get(path?: FormPathPattern): Schema ```typescript const schema = new Schema({ - type:"object", - properties:{ - array:{ - type:'array', - items:{ - type:'object', - properties:{ - input:{ - type:'string' + type: 'object', + properties: { + array: { + type: 'array', + items: { + type: 'object', + properties: { + input: { + type: 'string' } } } @@ -986,11 +964,9 @@ const schema = new Schema({ schema.get('array[0].input') //{type:"string"} ``` +##### `merge` - -##### `merge ` - -> 合并Schema +> 合并 Schema 签名 @@ -1002,15 +978,15 @@ merge(spec:ISchema): Schema ```typescript const schema = new Schema({ - type:"object", - properties:{ - array:{ - type:'array', - items:{ - type:'object', - properties:{ - input:{ - type:'string' + type: 'object', + properties: { + array: { + type: 'array', + items: { + type: 'object', + properties: { + input: { + type: 'string' } } } @@ -1019,7 +995,7 @@ const schema = new Schema({ }) schema.merge({ - title:'root object' + title: 'root object' }) /** { @@ -1042,11 +1018,9 @@ schema.merge({ **/ ``` - - ##### `getEmptyValue` -> 基于Schema的type获取当前Schema的空值 +> 基于 Schema 的 type 获取当前 Schema 的空值 签名 @@ -1058,15 +1032,15 @@ getEmptyValue() : '' | [] | {} | 0 ```typescript const schema = new Schema({ - type:"object", - properties:{ - array:{ - type:'array', - items:{ - type:'object', - properties:{ - input:{ - type:'string' + type: 'object', + properties: { + array: { + type: 'array', + items: { + type: 'object', + properties: { + input: { + type: 'string' } } } @@ -1078,14 +1052,11 @@ schema.get('array.0.input').getEmptyValue() // '' schema.get('array.0').getEmptyValue() // {} schema.get('array').getEmptyValue() // [] schema.getEmptyValue() // {} - ``` - - ##### `getSelfProps` -> 获取无嵌套Schema属性(不会包含properties/items这类嵌套属性) +> 获取无嵌套 Schema 属性(不会包含 properties/items 这类嵌套属性) 签名 @@ -1097,15 +1068,15 @@ getSelfProps() : ISchema ```typescript const schema = new Schema({ - type:"object", - properties:{ - array:{ - type:'array', - items:{ - type:'object', - properties:{ - input:{ - type:'string' + type: 'object', + properties: { + array: { + type: 'array', + items: { + type: 'object', + properties: { + input: { + type: 'string' } } } @@ -1114,14 +1085,11 @@ const schema = new Schema({ }) schema.getSelfProps() // { type:"object" } - ``` - - ##### `getExtendsRules` -> 获取扩展校验规则,该方法比较复杂,会解析当前Schema的所有校验类型的属性与x-rules属性,最终合并为一个统一的rules结构 +> 获取扩展校验规则,该方法比较复杂,会解析当前 Schema 的所有校验类型的属性与 x-rules 属性,最终合并为一个统一的 rules 结构 签名 @@ -1144,11 +1112,9 @@ const schema = new Schema({ schema.getExtendsRules() // [{required:true},{max:10},{pattern:/^\d+$/}] ``` - - ##### `getExtendsRequired` -> 获取是否必填,其实就是读取Schema的required属性,为什么封装成方法,是为了保证协议升级的时候对用户无感知,我们只需要保证方法的向后兼容即可 +> 获取是否必填,其实就是读取 Schema 的 required 属性,为什么封装成方法,是为了保证协议升级的时候对用户无感知,我们只需要保证方法的向后兼容即可 签名 @@ -1171,11 +1137,9 @@ const schema = new Schema({ schema.getExtendsRequired() // true ``` - - ##### `getExtendsEditable` -> 获取Schema的editable状态,与getExtendsEditable能力一致,也是为了抹平协议差异 +> 获取 Schema 的 editable 状态,与 getExtendsEditable 能力一致,也是为了抹平协议差异 签名 @@ -1187,43 +1151,41 @@ getExtendsEditable() : void | boolean ```typescript const schema1 = new Schema({ - type:"string", - editable:false + type: 'string', + editable: false }) schema1.getExtendsEditable() // false const schema2 = new Schema({ - type:"string", - readOnly:true + type: 'string', + readOnly: true }) schema2.getExtendsEditable() // false const schema3 = new Schema({ - type:"string", - "x-props":{ - editable:false + type: 'string', + 'x-props': { + editable: false } }) schema3.getExtendsEditable() // false const schema4 = new Schema({ - type:"string", - "x-component-props":{ - editable:false + type: 'string', + 'x-component-props': { + editable: false } }) schema4.getExtendsEditable() // false ``` - - ##### `getExtendsTriggerType` -> 获取triggerType,与getExtendsTriggerType能力一致,都是提供协议差异抹平的能力 +> 获取 triggerType,与 getExtendsTriggerType 能力一致,都是提供协议差异抹平的能力 签名 @@ -1235,27 +1197,27 @@ getExtendsTriggerType() : 'onBlur' | 'onChange' | string ```typescript const schema1 = new Schema({ - type:"string", - "x-props":{ - triggerType:"onBlur" + type: 'string', + 'x-props': { + triggerType: 'onBlur' } }) schema1.getExtendsTriggerType() // onBlur const schema2 = new Schema({ - type:"string", - "x-component-props":{ - triggerType:"onBlur" + type: 'string', + 'x-component-props': { + triggerType: 'onBlur' } }) schema2.getExtendsTriggerType() // onBlur const schema3 = new Schema({ - type:"string", - "x-item-props":{ - triggerType:"onBlur" + type: 'string', + 'x-item-props': { + triggerType: 'onBlur' } }) @@ -1264,7 +1226,7 @@ schema3.getExtendsTriggerType() // onBlur ##### `getExtendsProps` -> 获取x-props属性 +> 获取 x-props 属性 签名 @@ -1272,11 +1234,9 @@ schema3.getExtendsTriggerType() // onBlur getExtendsProps() : {} ``` - - ##### `getExtendsComponent` -> 获取x-component属性 +> 获取 x-component 属性 签名 @@ -1284,11 +1244,9 @@ getExtendsProps() : {} getExtendsComponent() : string ``` - - ##### `getExtendsComponentProps` -> 获取x-component-props属性,也就是x-component的组件属性 +> 获取 x-component-props 属性,也就是 x-component 的组件属性 签名 @@ -1296,11 +1254,9 @@ getExtendsComponent() : string getExtendsComponentProps() : {} ``` - - ##### `getExtendsRenderer` -> 获取x-render属性 +> 获取 x-render 属性 签名 @@ -1312,11 +1268,9 @@ getExtendsRenderer() : ( ) => React.ReactElement ``` - - ##### `getExtendsEffect` -> 获取x-effect属性 +> 获取 x-effect 属性 签名 @@ -1327,11 +1281,9 @@ getExtendsEffect() : ( ) => { [key: string]: any } ``` - - ##### `setProperty` -> 给当前Schema设置properties +> 给当前 Schema 设置 properties 签名 @@ -1339,11 +1291,9 @@ getExtendsEffect() : ( setProperty(key: string, schema: ISchema): Schema ``` - - ##### `setProperties` -> 给当前Schema批量设置properties +> 给当前 Schema 批量设置 properties 签名 @@ -1351,11 +1301,9 @@ setProperty(key: string, schema: ISchema): Schema setProperties(properties: {[key : string]:ISchema}) : {[key : string]:Schema} ``` - - ##### `setArrayItems` -> 给当前Schema设置items属性 +> 给当前 Schema 设置 items 属性 签名 @@ -1363,11 +1311,9 @@ setProperties(properties: {[key : string]:ISchema}) : {[key : string]:Schema} setArrayItems(schema:Ischema) : Schema ``` - - ##### `getOrderProperties` -> 按照x-index顺序给出所有properties +> 按照 x-index 顺序给出所有 properties 签名 @@ -1375,11 +1321,9 @@ setArrayItems(schema:Ischema) : Schema getOrderProperties() : {schema:Schema,key:string}[] ``` - - ##### `mapProperties` -> 按顺序(x-index)遍历Schema的properties属性 +> 按顺序(x-index)遍历 Schema 的 properties 属性 签名 @@ -1387,11 +1331,9 @@ getOrderProperties() : {schema:Schema,key:string}[] mapProperties(callback?: (schema: Schema, key: string) => any):any[] ``` - - ##### `toJSON` -> 输出无循环依赖json数据结构 +> 输出无循环依赖 json 数据结构 签名 @@ -1399,11 +1341,9 @@ mapProperties(callback?: (schema: Schema, key: string) => any):any[] toJSON() : ISchema ``` - - ##### `fromJSON` -> 基于一份json解析生成Schema对象 +> 基于一份 json 解析生成 Schema 对象 签名 @@ -1411,11 +1351,9 @@ toJSON() : ISchema fromJSON(json : ISchema) : Schema ``` - - ##### `isObject` -> 判断当前Schema是否是object类型 +> 判断当前 Schema 是否是 object 类型 签名 @@ -1423,11 +1361,9 @@ fromJSON(json : ISchema) : Schema isObject() : boolean ``` - - ##### `isArray()` -> 判断当前Schema是否是array类型 +> 判断当前 Schema 是否是 array 类型 签名 @@ -1435,8 +1371,6 @@ isObject() : boolean isArray() : boolean ``` - - ### Components --- @@ -1445,7 +1379,7 @@ isArray() : boolean #### `` -> 最核心的JSON Schema渲染组件 +> 最核心的 JSON Schema 渲染组件 **属性** @@ -1457,33 +1391,33 @@ interface ISchemaFormProps< FormActions = ISchemaFormActions | ISchemaFormAsyncActions > { //表单值 - value?: Value; + value?: Value //表单默认值 - defaultValue?: DefaultValue; + defaultValue?: DefaultValue //表单默认值,弱受控 - initialValues?: DefaultValue; + initialValues?: DefaultValue //表单actions - actions?: FormActions; + actions?: FormActions //表单effects - effects?: IFormEffect; + effects?: IFormEffect //form实例 - form?: IForm; + form?: IForm //表单变化回调 - onChange?: (values: Value) => void; + onChange?: (values: Value) => void //表单提交回调 - onSubmit?: (values: Value) => void | Promise; + onSubmit?: (values: Value) => void | Promise //表单重置回调 - onReset?: () => void; + onReset?: () => void //表单校验失败回调 - onValidateFailed?: (valideted: IFormValidateResult) => void; + onValidateFailed?: (valideted: IFormValidateResult) => void //表单子节点 - children?: React.ReactElement; + children?: React.ReactElement //是否开启脏检查 - useDirty?: boolean; + useDirty?: boolean //是否可编辑 - editable?: boolean | ((name: string) => boolean); + editable?: boolean | ((name: string) => boolean) //是否开启悲观校验,遇到第一个校验失败,则停止剩余校验 - validateFirst?: boolean; + validateFirst?: boolean //Form Schema对象 schema?: ISchema //实例级注册自定义组件 @@ -1497,11 +1431,9 @@ interface ISchemaFormProps< } ``` - - #### `` -> 基于一个Data Path,自动寻找Schema节点并渲染的内部组件,主要用于在自定义组件内实现递归渲染 +> 基于一个 Data Path,自动寻找 Schema 节点并渲染的内部组件,主要用于在自定义组件内实现递归渲染 **属性** @@ -1512,11 +1444,9 @@ interface ISchemaFieldProps { } ``` - - #### `` -> 让SchemaForm支持jsx标签式写法的Form组件,需要配合SchemaMarkupField一起使用 +> 让 SchemaForm 支持 jsx 标签式写法的 Form 组件,需要配合 SchemaMarkupField 一起使用 **属性** @@ -1528,33 +1458,33 @@ interface ISchemaFormProps< FormActions = ISchemaFormActions | ISchemaFormAsyncActions > { //表单值 - value?: Value; + value?: Value //表单默认值 - defaultValue?: DefaultValue; + defaultValue?: DefaultValue //表单默认值,弱受控 - initialValues?: DefaultValue; + initialValues?: DefaultValue //表单actions - actions?: FormActions; + actions?: FormActions //表单effects - effects?: IFormEffect; + effects?: IFormEffect //form实例 - form?: IForm; + form?: IForm //表单变化回调 - onChange?: (values: Value) => void; + onChange?: (values: Value) => void //表单提交回调 - onSubmit?: (values: Value) => void | Promise; + onSubmit?: (values: Value) => void | Promise //表单重置回调 - onReset?: () => void; + onReset?: () => void //表单校验失败回调 - onValidateFailed?: (valideted: IFormValidateResult) => void; + onValidateFailed?: (valideted: IFormValidateResult) => void //表单子节点 - children?: React.ReactElement; + children?: React.ReactElement //是否开启脏检查 - useDirty?: boolean; + useDirty?: boolean //是否可编辑 - editable?: boolean | ((name: string) => boolean); + editable?: boolean | ((name: string) => boolean) //是否开启悲观校验,遇到第一个校验失败,则停止剩余校验 - validateFirst?: boolean; + validateFirst?: boolean //Form Schema对象 schema?: ISchema //实例级注册自定义组件 @@ -1576,10 +1506,10 @@ import { SchemaMarkupField as Field } from '@uform/react-schema-renderer' -export default ()=>{ +export default () => { return ( - + ) } @@ -1587,25 +1517,21 @@ export default ()=>{ #### `` -> 让SchemaForm支持jsx标签式写法的Field组件,需要配合SchemaMarkupForm一起使用 +> 让 SchemaForm 支持 jsx 标签式写法的 Field 组件,需要配合 SchemaMarkupForm 一起使用 **属性** ```typescript -type IMarkupSchemaFieldProps = ISchema +type IMarkupSchemaFieldProps = ISchema ``` - - #### `` -> 核心Form,与@uform/react中的Form组件一样 - - +> 核心 Form,与@uform/react 中的 Form 组件一样 #### `` -> 核心Field,与@uform/react中的Field组件一样,主要用于复杂自定义组件内使用 +> 核心 Field,与@uform/react 中的 Field 组件一样,主要用于复杂自定义组件内使用 ### Interfaces @@ -1685,8 +1611,6 @@ interface ISchemaFieldComponentProps { } ``` - - #### ISchemaVirtualFieldComponentProps > 虚拟字段组件所接收的属性,只要涉及注册虚拟字段的,都需要了解该协议 @@ -1725,30 +1649,28 @@ interface ISchemaVirtualFieldComponentProps { } ``` - - #### ISchemaFormRegistry -> 组件注册中心,不管是普通字段,还是虚拟字段,还是Form/FormItem都会注册在这里 +> 组件注册中心,不管是普通字段,还是虚拟字段,还是 Form/FormItem 都会注册在这里 ```typescript interface ISchemaFormRegistry { fields: { [key: string]: React.JSXElementConstructor - }, + } virtualFields: { - [key: string]: React.JSXElementConstructor - }, + [key: string]: React.JSXElementConstructor< + ISchemaVirtualFieldComponentProps + > + } formItemComponent: React.JSXElementConstructor formComponent: string | React.JSXElementConstructor } ``` - - #### ISchema -> Schema协议对象,主要用于约束一份json结构满足Schema协议 +> Schema 协议对象,主要用于约束一份 json 结构满足 Schema 协议 ```typescript interface ISchema { @@ -1759,7 +1681,7 @@ interface ISchema { readOnly?: boolean writeOnly?: boolean type?: 'string' | 'object' | 'array' | 'number' | string - enum?: Array + enum?: Array const?: any multipleOf?: number maximum?: number @@ -1805,11 +1727,9 @@ interface ISchema { } ``` - - #### ISchemaFormActions -> 核心actions继承@uform/react的IFormActions,主要增加了getSchema API +> 核心 actions 继承@uform/react 的 IFormActions,主要增加了 getSchema API ```typescript interface ISchemaFormActions extends IFormActions { @@ -1820,7 +1740,7 @@ interface ISchemaFormActions extends IFormActions { #### ISchemaFormAsyncActions -> 核心actions继承@uform/react的IFormAsyncActions,主要增加了getSchema API +> 核心 actions 继承@uform/react 的 IFormAsyncActions,主要增加了 getSchema API ```typescript interface ISchemaFormAsyncActions extends IFormAsyncActions { @@ -1829,8 +1749,6 @@ interface ISchemaFormAsyncActions extends IFormAsyncActions { } ``` - - #### IFormValidateResult > 校验结果 @@ -1848,8 +1766,6 @@ interface IFormValidateResult { } ``` - - #### InternalFormats > 内置格式校验集 @@ -1871,8 +1787,6 @@ type InternalFormats = | string ``` - - #### ValidateDescription > 原始校验描述 @@ -1912,8 +1826,6 @@ interface ValidateDescription { } ``` - - #### ValidateResponse ```typescript @@ -1929,8 +1841,6 @@ type AsyncValidateResponse = Promise type ValidateResponse = SyncValidateResponse | AsyncValidateResponse ``` - - #### CustomValidator > 自定义校验函数 @@ -1942,20 +1852,14 @@ type CustomValidator = ( ) => ValidateResponse ``` - - - - #### ValidatePatternRules > 校验规则集 ```typescript - type ValidatePatternRules = InternalFormats +type ValidatePatternRules = + | InternalFormats | CustomValidator | ValidateDescription - | Array< - InternalFormats | CustomValidator | ValidateDescription -> + | Array ``` - diff --git a/packages/react/README.md b/packages/react/README.md index 40778b7af3d..c17bd60d01c 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -97,7 +97,7 @@ import { FormProvider, FormConsumer, FormEffectHooks -} from './src' +} from '@uform/react' const { onFormInit$, onFormInputChange$, onFieldInputChange$ } = FormEffectHooks const actions = createFormActions() @@ -171,7 +171,7 @@ The type of rules is [ValidatePatternRules](#ValidatePatternRules) which is [Int ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions } from './src' +import { Form, Field, createFormActions } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -288,7 +288,7 @@ Example:User info `user(username, age)` ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions } from './src' +import { Form, Field, createFormActions } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -378,7 +378,7 @@ Example:Id list ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions } from './src' +import { Form, Field, createFormActions } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -435,7 +435,7 @@ Example:User list ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions } from './src' +import { Form, Field, createFormActions } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -550,7 +550,7 @@ Example: see how `display` 与 `visible` affect values ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, LifeCycleTypes, FormSpy } from './src' +import { Form, Field, createFormActions, LifeCycleTypes, FormSpy } from '@uform/react' const InputField = props => ( @@ -655,7 +655,7 @@ Example:Show/hide field and modified props/value by using effects ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, LifeCycleTypes } from './src' +import { Form, Field, createFormActions, LifeCycleTypes } from '@uform/react' const InputField = props => ( @@ -737,7 +737,7 @@ Example:Change dataSource in select asynchronously by effects ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, LifeCycleTypes } from './src' +import { Form, Field, createFormActions, LifeCycleTypes } from '@uform/react' const InputField = props => ( @@ -841,7 +841,7 @@ Example:validation when form mounted and re-trigger validation when field chan ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, LifeCycleTypes } from './src' +import { Form, Field, createFormActions, LifeCycleTypes } from '@uform/react' const InputField = props => ( @@ -898,7 +898,7 @@ Example:See how ArrayField communicate with other field by using effects ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, LifeCycleTypes } from './src' +import { Form, Field, createFormActions, LifeCycleTypes } from '@uform/react' const InputField = props => ( @@ -1029,7 +1029,7 @@ Example:Combo value of username and age. Check [FormSpy](#FormSpy) for more in ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, FormSpy } from './src' +import { Form, Field, createFormActions, FormSpy } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -1089,7 +1089,7 @@ Example:Cross-file consumption form state, Check [FormProvider](#FormProvider) ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, FormSpy, FormProvider } from './src' +import { Form, Field, createFormActions, FormSpy, FormProvider } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -1148,7 +1148,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React, { useState } from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, FormSpy, FormPath } from './src' +import { Form, Field, createFormActions, FormSpy, FormPath } from '@uform/react' const actions = createFormActions() @@ -1201,7 +1201,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React, { useState } from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, FormSpy, FormPath } from './src' +import { Form, Field, createFormActions, FormSpy, FormPath } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -1357,7 +1357,7 @@ Example:All type of field ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions } from './src' +import { Form, Field, createFormActions } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -1572,7 +1572,7 @@ Example:Setting `` size from 100x100 to 200x200 ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, VirtualField } from './src' +import { Form, Field, createFormActions, VirtualField } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -1667,7 +1667,7 @@ Example1: Form state change counter ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, FormSpy, LifeCycleTypes } from './src' +import { Form, Field, createFormActions, FormSpy, LifeCycleTypes } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -1718,7 +1718,7 @@ Example2:Combo ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, FormSpy } from './src' +import { Form, Field, createFormActions, FormSpy } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -1774,7 +1774,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, FormSpy, FormProvider } from './src' +import { Form, Field, createFormActions, FormSpy, FormProvider } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -1852,7 +1852,7 @@ interface IFormConsumerProps { ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, useFormEffects, LifeCycleTypes } from './src' +import { Form, Field, createFormActions, useFormEffects, LifeCycleTypes } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -2183,7 +2183,7 @@ const App = () => { **Usage** ```jsx -import { Form, createEffectHook, createFormActions } from './src' +import { Form, createEffectHook, createFormActions } from '@uform/react' const actions = createFormActions() const diyHook1$ = createEffectHook('diy1') diff --git a/packages/react/README.zh-cn.md b/packages/react/README.zh-cn.md index ab539700359..7e5c84f98ae 100644 --- a/packages/react/README.zh-cn.md +++ b/packages/react/README.zh-cn.md @@ -95,7 +95,7 @@ import { FormProvider, FormConsumer, FormEffectHooks -} from './src' +} from '@uform/react' const { onFormInit$, onFormInputChange$, onFieldInputChange$ } = FormEffectHooks const actions = createFormActions() @@ -169,7 +169,7 @@ const InputField = props => ( ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions } from './src' +import { Form, Field, createFormActions } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -288,7 +288,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions } from './src' +import { Form, Field, createFormActions } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -380,7 +380,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions } from './src' +import { Form, Field, createFormActions } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -439,7 +439,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions } from './src' +import { Form, Field, createFormActions } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -555,7 +555,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, LifeCycleTypes, FormSpy } from './src' +import { Form, Field, createFormActions, LifeCycleTypes, FormSpy } from '@uform/react' const InputField = props => ( @@ -661,7 +661,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, LifeCycleTypes } from './src' +import { Form, Field, createFormActions, LifeCycleTypes } from '@uform/react' const InputField = props => ( @@ -743,7 +743,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, LifeCycleTypes } from './src' +import { Form, Field, createFormActions, LifeCycleTypes } from '@uform/react' const InputField = props => ( @@ -847,7 +847,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, LifeCycleTypes } from './src' +import { Form, Field, createFormActions, LifeCycleTypes } from '@uform/react' const InputField = props => ( @@ -904,7 +904,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, LifeCycleTypes } from './src' +import { Form, Field, createFormActions, LifeCycleTypes } from '@uform/react' const InputField = props => ( @@ -1034,7 +1034,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, FormSpy } from './src' +import { Form, Field, createFormActions, FormSpy } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -1096,7 +1096,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, FormSpy, FormProvider } from './src' +import { Form, Field, createFormActions, FormSpy, FormProvider } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -1157,7 +1157,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React, { useState } from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, FormSpy, FormPath } from './src' +import { Form, Field, createFormActions, FormSpy, FormPath } from '@uform/react' const actions = createFormActions() @@ -1211,7 +1211,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React, { useState } from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, FormSpy, FormPath } from './src' +import { Form, Field, createFormActions, FormSpy, FormPath } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -1379,7 +1379,7 @@ interface IFieldStateUIProps { ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions } from './src' +import { Form, Field, createFormActions } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -1601,7 +1601,7 @@ interface IVirtualFieldProps { ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, VirtualField } from './src' +import { Form, Field, createFormActions, VirtualField } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -1698,7 +1698,7 @@ interface IFormSpyProps { ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, FormSpy, LifeCycleTypes } from './src' +import { Form, Field, createFormActions, FormSpy, LifeCycleTypes } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -1751,7 +1751,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, FormSpy } from './src' +import { Form, Field, createFormActions, FormSpy } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -1808,7 +1808,7 @@ ReactDOM.render(, document.getElementById('root')) ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, FormSpy, FormProvider } from './src' +import { Form, Field, createFormActions, FormSpy, FormProvider } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -1887,7 +1887,7 @@ interface IFormConsumerProps { ```jsx import React from 'react' import ReactDOM from 'react-dom' -import { Form, Field, createFormActions, useFormEffects, LifeCycleTypes } from './src' +import { Form, Field, createFormActions, useFormEffects, LifeCycleTypes } from '@uform/react' const actions = createFormActions() const InputField = props => ( @@ -2197,7 +2197,7 @@ const App = () => { **Usage** ```jsx -import { Form, createEffectHook, createFormActions } from './src' +import { Form, createEffectHook, createFormActions } from '@uform/react' const actions = createFormActions() const diyHook1$ = createEffectHook('diy1') From 29c6846e7b59dda1582e6b5ca6453a1e2a623d31 Mon Sep 17 00:00:00 2001 From: Janry Date: Fri, 22 Nov 2019 23:15:24 +0800 Subject: [PATCH 55/99] Improve docs (#414) --- README.md | 2 +- README.zh-cn.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d7383808302..e1f89f2a57b 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ https://uformjs.org - [@uform/antd](./packages/antd/README.md) - [@uform/next](./packages/next/README.md) -- [@uform/react-schema-renderer](./packages/react-schema-renderder/README.md) +- [@uform/react-schema-renderer](./packages/react-schema-renderer/README.md) - [@uform/react](./packages/react/README.md) - [@uform/core](./packages/core/README.md) diff --git a/README.zh-cn.md b/README.zh-cn.md index 73221375f05..2a12b70dfda 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -67,7 +67,7 @@ https://uformjs.org - [@uform/antd](./packages/antd/README.md) - [@uform/next](./packages/next/README.md) -- [@uform/react-schema-renderer](./packages/react-schema-renderder/README.md) +- [@uform/react-schema-renderer](./packages/react-schema-renderer/README.md) - [@uform/react](./packages/react/README.md) - [@uform/core](./packages/core/README.md) From 4be17407a6a71726a81a6ec5cffb3c9c55ab6c04 Mon Sep 17 00:00:00 2001 From: Janry Date: Fri, 22 Nov 2019 23:16:35 +0800 Subject: [PATCH 56/99] update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e1f89f2a57b..3e1762ba31d 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ English | [简体中文](./README.zh-cn.md)

    UForm - a high-performance React form solution for handling complicated enterprise requirements.

    + --- ## Background From 0aeeed9c774a518402965c86086e217771b71006 Mon Sep 17 00:00:00 2001 From: Janry Date: Fri, 22 Nov 2019 23:17:13 +0800 Subject: [PATCH 57/99] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e1762ba31d..da43d55b5ee 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Use uform react package: npm install --save @uform/react ``` -Use UForm core package: +Use uform core package: ```bash npm install --save @uform/core From 2f56bf3de3dd0b1b42c298c5589f832842c151c0 Mon Sep 17 00:00:00 2001 From: Janry Date: Fri, 22 Nov 2019 23:22:17 +0800 Subject: [PATCH 58/99] update readme --- README.zh-cn.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.zh-cn.md b/README.zh-cn.md index 2a12b70dfda..e346234903b 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -65,11 +65,11 @@ https://uformjs.org ## 文档 -- [@uform/antd](./packages/antd/README.md) -- [@uform/next](./packages/next/README.md) -- [@uform/react-schema-renderer](./packages/react-schema-renderer/README.md) -- [@uform/react](./packages/react/README.md) -- [@uform/core](./packages/core/README.md) +- [@uform/antd](./packages/antd/README.zh-cn.md) +- [@uform/next](./packages/next/README.zh-cn.md) +- [@uform/react-schema-renderer](./packages/react-schema-renderer/README.zh-cn.md) +- [@uform/react](./packages/react/README.zh-cn.md) +- [@uform/core](./packages/core/README.zh-cn.md) ## 入门案例 From d5922f73af52da0f9e27c557aeae8b679a87592d Mon Sep 17 00:00:00 2001 From: quirkyvar Date: Sat, 23 Nov 2019 11:54:33 +0800 Subject: [PATCH 59/99] core: fix remove function of object field mutator (#410) --- packages/antd/src/index.tsx | 4 ++- packages/core/src/__tests__/index.spec.ts | 11 +++++++++ packages/core/src/index.ts | 30 ++++++++++++++++------- packages/next/src/index.tsx | 4 ++- packages/react/README.md | 6 ++--- 5 files changed, 41 insertions(+), 14 deletions(-) diff --git a/packages/antd/src/index.tsx b/packages/antd/src/index.tsx index 0cda03521a4..650d557f424 100644 --- a/packages/antd/src/index.tsx +++ b/packages/antd/src/index.tsx @@ -10,6 +10,8 @@ export * from '@uform/react-schema-renderer' export * from './components' export * from './types' export { mapStyledProps, mapTextComponent } from './shared' -export const SchemaForm: React.FC = SchemaMarkupForm as any +export const SchemaForm: React.FC< + IAntdSchemaFormProps +> = SchemaMarkupForm as any export const Field: React.FC = SchemaMarkupField export default SchemaForm diff --git a/packages/core/src/__tests__/index.spec.ts b/packages/core/src/__tests__/index.spec.ts index 3fe78924ffe..2ce768770e8 100644 --- a/packages/core/src/__tests__/index.spec.ts +++ b/packages/core/src/__tests__/index.spec.ts @@ -1175,6 +1175,17 @@ describe('createMutators', () => { expect(form.getFieldValue('mm')).toEqual(arr.slice(0, 1)) }) + test('remove object key', async () => { + const form = createForm({ useDirty: true }) + const initialValue = { username : '1234' } + const user = form.registerField({ path: 'user', initialValue }) + form.registerField({ path: 'user.username' }) + const mutators = form.createMutators(user) + expect(form.getFieldValue('user')).toEqual(initialValue) + mutators.remove('username') + expect(form.getFieldValue('user')).toEqual({}) + }) + test('exist', async () => { const form = createForm() const mm = form.registerField({ path: 'mm', value: arr }) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 698c79385fc..78afa0e4641 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -230,9 +230,8 @@ export function createForm( syncFormMessages('warnings', published.name, published.warnings) } heart.publish(LifeCycleTypes.ON_FIELD_CHANGE, field) - if (!(!env.shadowStage || env.leadingStage)) { - return false - } + if (env.leadingStage) return + if (env.shadowStage) return false } } @@ -525,12 +524,25 @@ export function createForm( function removeValue(key: string | number) { const name = field.unsafe_getSourceState(state => state.name) - env.removeNodes[name] = true - field.setState((state: IFieldState) => { - state.value = undefined - state.values = [] - }, true) - deleteFormValuesIn(key ? FormPath.parse(name).concat(key) : name) + leadingUpdate(() => { + if (isValid(key)) { + const childPath = FormPath.parse(name).concat(key) + const child = graph.get(childPath) + env.removeNodes[childPath.toString()] = true + deleteFormValuesIn(childPath) + child.setState((fieldState: IFieldState) => { + fieldState.value = undefined + fieldState.values = [] + }, true) + } else { + env.removeNodes[name] = true + deleteFormValuesIn(name) + field.setState((fieldState: IFieldState) => { + fieldState.value = undefined + fieldState.values = [] + }, true) + } + }) heart.publish(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, field) heart.publish(LifeCycleTypes.ON_FIELD_INPUT_CHANGE, field) heart.publish(LifeCycleTypes.ON_FORM_INPUT_CHANGE, state) diff --git a/packages/next/src/index.tsx b/packages/next/src/index.tsx index 7056bbbed78..85d9569b902 100644 --- a/packages/next/src/index.tsx +++ b/packages/next/src/index.tsx @@ -10,6 +10,8 @@ export * from '@uform/react-schema-renderer' export * from './components' export * from './types' export { mapStyledProps, mapTextComponent } from './shared' -export const SchemaForm: React.FC = SchemaMarkupForm as any +export const SchemaForm: React.FC< + INextSchemaFormProps +> = SchemaMarkupForm as any export const Field: React.FC = SchemaMarkupField export default SchemaForm diff --git a/packages/react/README.md b/packages/react/README.md index c17bd60d01c..5a790c37a45 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -205,7 +205,7 @@ const App = () => { name="age" rules={[ val => - val === undefiend + !val ? { type: 'error', message: 'age is required' } : undefined ]} @@ -217,7 +217,7 @@ const App = () => { name="gender" rules={[ val => - val === undefiend + !val ? { type: 'warning', message: 'gender is required' } : undefined ]} @@ -242,7 +242,7 @@ const App = () => { rules={[ { validator(value) { - return value === undefiend + return !value ? 'This field can not be empty, please enter {{scope.outerVariable}}' : undefined }, From 737fa548a812d44c896b0afa9df30b06bc77a43a Mon Sep 17 00:00:00 2001 From: Janry Date: Sat, 23 Nov 2019 14:04:32 +0800 Subject: [PATCH 60/99] Fix remove logics (#415) --- packages/antd/src/index.tsx | 4 +--- packages/core/src/index.ts | 47 +++++++++++++++++-------------------- packages/next/src/index.tsx | 4 +--- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/packages/antd/src/index.tsx b/packages/antd/src/index.tsx index 650d557f424..0cda03521a4 100644 --- a/packages/antd/src/index.tsx +++ b/packages/antd/src/index.tsx @@ -10,8 +10,6 @@ export * from '@uform/react-schema-renderer' export * from './components' export * from './types' export { mapStyledProps, mapTextComponent } from './shared' -export const SchemaForm: React.FC< - IAntdSchemaFormProps -> = SchemaMarkupForm as any +export const SchemaForm: React.FC = SchemaMarkupForm as any export const Field: React.FC = SchemaMarkupField export default SchemaForm diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 78afa0e4641..a0db0b559be 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -74,8 +74,8 @@ export function createForm( field.setState(state => { if (state.visible) { if (valuesChanged) { - const path = FormPath.parse(state.name) - const parent = graph.getLatestParent(path) + const dataPath = FormPath.parse(state.name) + const parent = graph.getLatestParent(dataPath) const parentValue = getFormValuesIn(parent.path) const value = getFormValuesIn(state.name) /** @@ -84,20 +84,20 @@ export function createForm( let removed = false if ( isArr(parentValue) && - !path.existIn(parentValue, parent.path) + !dataPath.existIn(parentValue, parent.path) ) { if ( !parent.path - .getNearestChildPathBy(path) + .getNearestChildPathBy(dataPath) .existIn(parentValue, parent.path) ) { - graph.remove(state.name) + graph.remove(state.path) removed = true } } else { each(env.removeNodes, (_, name) => { - if (path.includes(name)) { - graph.remove(path) + if (dataPath.includes(name)) { + graph.remove(state.path) delete env.removeNodes[name] removed = true } @@ -523,26 +523,21 @@ export function createForm( } function removeValue(key: string | number) { - const name = field.unsafe_getSourceState(state => state.name) - leadingUpdate(() => { - if (isValid(key)) { - const childPath = FormPath.parse(name).concat(key) - const child = graph.get(childPath) - env.removeNodes[childPath.toString()] = true - deleteFormValuesIn(childPath) - child.setState((fieldState: IFieldState) => { - fieldState.value = undefined - fieldState.values = [] - }, true) - } else { - env.removeNodes[name] = true - deleteFormValuesIn(name) - field.setState((fieldState: IFieldState) => { - fieldState.value = undefined - fieldState.values = [] - }, true) + const nodePath = field.unsafe_getSourceState(state => state.path) + const dataPath = field.unsafe_getSourceState(state => state.name) + if (isValid(key)) { + const childDataPath = FormPath.parse(dataPath).concat(key) + env.removeNodes[childDataPath.toString()] = true + deleteFormValuesIn(childDataPath) + field.notify(field.getState()) + } else { + const parent = graph.selectParent(nodePath) + env.removeNodes[dataPath.toString()] = true + deleteFormValuesIn(dataPath) + if (parent) { + parent.notify(parent.getState()) } - }) + } heart.publish(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, field) heart.publish(LifeCycleTypes.ON_FIELD_INPUT_CHANGE, field) heart.publish(LifeCycleTypes.ON_FORM_INPUT_CHANGE, state) diff --git a/packages/next/src/index.tsx b/packages/next/src/index.tsx index 85d9569b902..7056bbbed78 100644 --- a/packages/next/src/index.tsx +++ b/packages/next/src/index.tsx @@ -10,8 +10,6 @@ export * from '@uform/react-schema-renderer' export * from './components' export * from './types' export { mapStyledProps, mapTextComponent } from './shared' -export const SchemaForm: React.FC< - INextSchemaFormProps -> = SchemaMarkupForm as any +export const SchemaForm: React.FC = SchemaMarkupForm as any export const Field: React.FC = SchemaMarkupField export default SchemaForm From 0fc5f7385650b25f89c4e902fdef17f8c1a7928c Mon Sep 17 00:00:00 2001 From: Janry Date: Sat, 23 Nov 2019 14:12:12 +0800 Subject: [PATCH 61/99] Improve docs (#416) --- docs/Examples/antd/Relations.md | 90 +++++++++++++++++++ docs/Examples/next/Relations.md | 148 ++++++++++++++++++++++++++------ 2 files changed, 210 insertions(+), 28 deletions(-) diff --git a/docs/Examples/antd/Relations.md b/docs/Examples/antd/Relations.md index 286c2104eb7..b65ce80b940 100644 --- a/docs/Examples/antd/Relations.md +++ b/docs/Examples/antd/Relations.md @@ -202,6 +202,96 @@ const App = () => { ReactDOM.render(, document.getElementById('root')) ``` + +### 循环联动 + +> 联动关系 +> 总价 = 单价 \* 数量 +> 数量 = 总价 / 单价 +> 单价 = 总价 / 数量 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormPath, + FormBlock, + FormLayout +} from '@uform/next' +import { filter, withLatestFrom, map, debounceTime } from 'rxjs/operators' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' + +const App = () => ( + + { + $('onFieldValueChange', 'total').subscribe(({ value }) => { + if (!value) return + setFieldState('count', state => { + const price = getFieldState('price', state => state.value) + if (!price) return + state.value = value / price + }) + setFieldState('price', state => { + const count = getFieldState('count', state => state.value) + if (!count) return + state.value = value / count + }) + }) + $('onFieldValueChange', 'price').subscribe(({ value }) => { + if (!value) return + setFieldState('total', state => { + const count = getFieldState('count', state => state.value) + if (!count) return + state.value = value * count + }) + setFieldState('count', state => { + const total = getFieldState('total', state => state.value) + if (!total) return + state.value = total / value + }) + }) + $('onFieldValueChange', 'count').subscribe(({ value }) => { + if (!value) return + setFieldState('total', state => { + const price = getFieldState('price', state => state.value) + if (!price) return + state.value = value * price + }) + setFieldState('price', state => { + const total = getFieldState('total', state => state.value) + if (!total) return + state.value = total / value + }) + }) + }} + onChange={v => console.log(v)} + labelCol={6} + wrapperCol={4} + onSubmit={v => console.log(v)} + > + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + ### 异步数据联动 > 当前例子主要演示了从某个字段的变化,引起某些异步操作,然后再去更新某些字段的状 diff --git a/docs/Examples/next/Relations.md b/docs/Examples/next/Relations.md index 479a49c8804..47561dbdccd 100644 --- a/docs/Examples/next/Relations.md +++ b/docs/Examples/next/Relations.md @@ -63,35 +63,32 @@ const App = () => { } }) }) - $('onFieldValueChange', '*(aa,bb)') - .subscribe(fieldState => { - console.log('aa或者bb发生变化了') + $('onFieldValueChange', '*(aa,bb)').subscribe(fieldState => { + console.log('aa或者bb发生变化了') + }) + $('onFieldValueChange', 'aa').subscribe(fieldState => { + setFieldState('bb', state => { + state.visible = !fieldState.value }) - $('onFieldValueChange', 'aa') - .subscribe(fieldState => { - setFieldState('bb', state => { - state.visible = !fieldState.value - }) + }) + $('onFieldValueChange', 'cc').subscribe(fieldState => { + setFieldState('dd', state => { + state.visible = !fieldState.value }) - $('onFieldValueChange', 'cc') - .subscribe(fieldState => { - setFieldState('dd', state => { - state.visible = !fieldState.value - }) - setFieldState('gg', state => { - if (fieldState.value) { - state.value = 'aaaa' - state.props.enum = [ - { label: 'aaaa', value: 'aaaa', extra: ['x1', 'x2', 'x3'] }, - { label: 'bbbb', value: 'bbbb', extra: ['x4', 'x5', 'x6'] }, - { label: 'cccc', value: 'cccc', extra: ['x7', 'x8', 'x9'] } - ] - } else { - state.value = '123333' - state.props.enum = ['123333', '333333'] - } - }) + setFieldState('gg', state => { + if (fieldState.value) { + state.value = 'aaaa' + state.props.enum = [ + { label: 'aaaa', value: 'aaaa', extra: ['x1', 'x2', 'x3'] }, + { label: 'bbbb', value: 'bbbb', extra: ['x4', 'x5', 'x6'] }, + { label: 'cccc', value: 'cccc', extra: ['x7', 'x8', 'x9'] } + ] + } else { + state.value = '123333' + state.props.enum = ['123333', '333333'] + } }) + }) $('onFieldValueChange', 'gg') .pipe( combineLatest($('onChangeOption')), @@ -143,7 +140,10 @@ const App = () => { type="boolean" x-component="radio" default={true} - enum={[{ label: '是', value: true }, { label: '否', value: false }]} + enum={[ + { label: '是', value: true }, + { label: '否', value: false } + ]} title="是否隐藏AA" /> @@ -153,7 +153,10 @@ const App = () => { title="是否隐藏DD" default={true} x-component="radio" - enum={[{ label: '是', value: true }, { label: '否', value: false }]} + enum={[ + { label: '是', value: true }, + { label: '否', value: false } + ]} />
    @@ -192,6 +195,95 @@ const App = () => { ReactDOM.render(, document.getElementById('root')) ``` +### 循环联动 + +> 联动关系 +> 总价 = 单价 \* 数量 +> 数量 = 总价 / 单价 +> 单价 = 总价 / 数量 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormPath, + FormBlock, + FormLayout +} from '@uform/next' +import { filter, withLatestFrom, map, debounceTime } from 'rxjs/operators' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' + +const App = () => ( + + { + $('onFieldValueChange', 'total').subscribe(({ value }) => { + if (!value) return + setFieldState('count', state => { + const price = getFieldState('price', state => state.value) + if (!price) return + state.value = value / price + }) + setFieldState('price', state => { + const count = getFieldState('count', state => state.value) + if (!count) return + state.value = value / count + }) + }) + $('onFieldValueChange', 'price').subscribe(({ value }) => { + if (!value) return + setFieldState('total', state => { + const count = getFieldState('count', state => state.value) + if (!count) return + state.value = value * count + }) + setFieldState('count', state => { + const total = getFieldState('total', state => state.value) + if (!total) return + state.value = total / value + }) + }) + $('onFieldValueChange', 'count').subscribe(({ value }) => { + if (!value) return + setFieldState('total', state => { + const price = getFieldState('price', state => state.value) + if (!price) return + state.value = value * price + }) + setFieldState('price', state => { + const total = getFieldState('total', state => state.value) + if (!total) return + state.value = total / value + }) + }) + }} + onChange={v => console.log(v)} + labelCol={6} + wrapperCol={4} + onSubmit={v => console.log(v)} + > + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + ### 异步数据联动 > 当前例子主要演示了从某个字段的变化,引起某些异步操作,然后再去更新某些字段的状 From 623e3300fd6a052ef09c952b22067aa2b9efb582 Mon Sep 17 00:00:00 2001 From: Janry Date: Sat, 23 Nov 2019 14:56:07 +0800 Subject: [PATCH 62/99] Fix registry (#417) --- .../src/hooks/useSchemaForm.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/react-schema-renderer/src/hooks/useSchemaForm.ts b/packages/react-schema-renderer/src/hooks/useSchemaForm.ts index 4c0a419c9e4..492ca6b0f85 100644 --- a/packages/react-schema-renderer/src/hooks/useSchemaForm.ts +++ b/packages/react-schema-renderer/src/hooks/useSchemaForm.ts @@ -1,12 +1,20 @@ import { useMemo, useRef } from 'react' import { useForm } from '@uform/react' import { Schema } from '../shared/schema' -import { deprecate } from '@uform/shared' +import { deprecate, each, lowercase } from '@uform/shared' import { useEva } from 'react-eva' import { ISchemaFormProps } from '../types' import { createSchemaFormActions } from '../shared/actions' import { getRegistry } from '../shared/registry' +const lowercaseKeys = (obj: any) => { + const result = {} + each(obj, (value, key) => { + result[lowercase(key)] = value + }) + return result +} + const useInternalSchemaForm = (props: ISchemaFormProps) => { const { fields, @@ -37,14 +45,14 @@ const useInternalSchemaForm = (props: ISchemaFormProps) => { return { form: useForm(props), formComponentProps, - fields: { + fields: lowercaseKeys({ ...registry.fields, ...fields - }, - virtualFields: { + }), + virtualFields: lowercaseKeys({ ...registry.virtualFields, ...virtualFields - }, + }), formComponent: formComponent ? formComponent : registry.formComponent, formItemComponent: formItemComponent ? formItemComponent From 79d7d36f080310e776404377350c818628b78a58 Mon Sep 17 00:00:00 2001 From: janrywang Date: Sat, 23 Nov 2019 15:53:08 +0800 Subject: [PATCH 63/99] chore(publish): v1.0.0-alpha.0 --- packages/antd/package.json | 12 ++++++------ packages/core/package.json | 6 +++--- packages/next/package.json | 18 +++++++++--------- packages/printer/package.json | 4 ++-- packages/react-schema-renderer/package.json | 8 ++++---- packages/react-shared-components/package.json | 6 +++--- packages/react/package.json | 14 +++++++------- packages/shared/package.json | 2 +- packages/validator/package.json | 4 ++-- 9 files changed, 37 insertions(+), 37 deletions(-) diff --git a/packages/antd/package.json b/packages/antd/package.json index 639ceb5266b..7ad71217587 100644 --- a/packages/antd/package.json +++ b/packages/antd/package.json @@ -1,6 +1,6 @@ { "name": "@uform/antd", - "version": "0.4.0", + "version": "1.0.0-alpha.0", "license": "MIT", "main": "lib", "module": "esm", @@ -24,16 +24,16 @@ }, "peerDependencies": { "@babel/runtime": "^7.4.4", + "@types/styled-components": "^4.1.19", "antd": "^3.14.1", "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "@types/styled-components": "^4.1.19" + "react-dom": ">=16.8.0" }, "dependencies": { - "@uform/react-schema-renderer": "^0.4.0", - "@uform/react-shared-components":"^0.4.0", + "@uform/react-schema-renderer": "^1.0.0-alpha.0", + "@uform/react-shared-components": "^1.0.0-alpha.0", + "@uform/shared": "^1.0.0-alpha.0", "@uform/types": "^0.4.0", - "@uform/shared": "^0.4.0", "classnames": "^2.2.6", "moveto": "^1.7.4", "react-stikky": "^0.1.15", diff --git a/packages/core/package.json b/packages/core/package.json index 41fa458efb1..a29a5680152 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@uform/core", - "version": "0.4.0", + "version": "1.0.0-alpha.0", "license": "MIT", "main": "lib", "repository": { @@ -26,9 +26,9 @@ "scheduler": ">=0.11.2" }, "dependencies": { + "@uform/shared": "^1.0.0-alpha.0", "@uform/types": "^0.4.0", - "@uform/shared": "^0.4.0", - "@uform/validator": "^0.4.0", + "@uform/validator": "^1.0.0-alpha.0", "immer": "^3.2.0" }, "publishConfig": { diff --git a/packages/next/package.json b/packages/next/package.json index e40e33414ae..b491fb785be 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@uform/next", - "version": "0.4.0", + "version": "1.0.0-alpha.0", "license": "MIT", "main": "lib", "module": "esm", @@ -21,23 +21,23 @@ }, "peerDependencies": { "@alifd/next": "^1.13.1", + "@babel/runtime": "^7.4.4", "@types/classnames": "^2.2.9", "@types/styled-components": "^4.1.19", - "@babel/runtime": "^7.4.4", "react": ">=16.8.0", "react-dom": ">=16.8.0" }, "dependencies": { - "@uform/react-schema-renderer": "^0.4.0", - "@uform/react-shared-components":"^0.4.0", + "@uform/react-schema-renderer": "^1.0.0-alpha.0", + "@uform/react-shared-components": "^1.0.0-alpha.0", + "@uform/shared": "^1.0.0-alpha.0", "@uform/types": "^0.4.0", - "@uform/shared": "^0.4.0", "classnames": "^2.2.6", "moveto": "^1.7.4", + "react-eva": "^1.0.0-alpha.0", "react-stikky": "^0.1.15", - "styled-components": "^4.1.1", - "react-eva": "^1.0.0", - "rxjs": "^6.5.1" + "rxjs": "^6.5.1", + "styled-components": "^4.1.1" }, "devDependencies": { "@alifd/next": "^1.13.1", @@ -48,4 +48,4 @@ "access": "public" }, "gitHead": "4d068dad6183e8da294a4c899a158326c0b0b050" -} \ No newline at end of file +} diff --git a/packages/printer/package.json b/packages/printer/package.json index 920ca202bed..e9c5163ae8d 100644 --- a/packages/printer/package.json +++ b/packages/printer/package.json @@ -1,6 +1,6 @@ { "name": "@uform/printer", - "version": "0.4.0", + "version": "1.0.0-alpha.0", "license": "MIT", "main": "lib", "repository": { @@ -26,7 +26,7 @@ "typescript": "^3.5.2" }, "dependencies": { - "@uform/react-schema-renderer": "^0.4.0", + "@uform/react-schema-renderer": "^1.0.0-alpha.0", "react-modal": "^3.8.1", "styled-components": "^4.1.1" }, diff --git a/packages/react-schema-renderer/package.json b/packages/react-schema-renderer/package.json index d14d82987e5..7a79d4b4dae 100644 --- a/packages/react-schema-renderer/package.json +++ b/packages/react-schema-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@uform/react-schema-renderer", - "version": "0.4.0", + "version": "1.0.0-alpha.0", "license": "MIT", "main": "lib", "repository": { @@ -30,9 +30,9 @@ "react-eva": "^1.1.7" }, "dependencies": { - "@uform/react": "^0.4.0", - "@uform/shared": "^0.4.0", - "@uform/validator": "^0.4.0", + "@uform/react": "^1.0.0-alpha.0", + "@uform/shared": "^1.0.0-alpha.0", + "@uform/validator": "^1.0.0-alpha.0", "pascal-case": "^2.0.1" }, "publishConfig": { diff --git a/packages/react-shared-components/package.json b/packages/react-shared-components/package.json index a479c36aa14..274d5b2a08c 100644 --- a/packages/react-shared-components/package.json +++ b/packages/react-shared-components/package.json @@ -1,6 +1,6 @@ { "name": "@uform/react-shared-components", - "version": "0.4.0", + "version": "1.0.0-alpha.0", "license": "MIT", "main": "lib", "types": "lib/index.d.ts", @@ -29,7 +29,7 @@ "@babel/runtime": "^7.4.4" }, "dependencies": { - "@uform/types": "^0.4.0", - "@uform/shared": "^0.4.0" + "@uform/shared": "^1.0.0-alpha.0", + "@uform/types": "^0.4.0" } } diff --git a/packages/react/package.json b/packages/react/package.json index 47a8480d8a8..0fc982210b6 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@uform/react", - "version": "0.4.0", + "version": "1.0.0-alpha.0", "license": "MIT", "main": "lib", "repository": { @@ -23,19 +23,19 @@ }, "peerDependencies": { "@babel/runtime": "^7.4.4", - "scheduler": ">=0.11.2", "@types/react": "^16.8.23", "react": ">=16.8.0", "react-dom": ">=16.8.0", - "react-eva": "^1.0.0", - "rxjs": "^6.5.1" + "react-eva": "^1.0.0-alpha.0", + "rxjs": "^6.5.1", + "scheduler": ">=0.11.2" }, "dependencies": { - "@uform/core": "^0.4.0", - "@uform/shared": "^0.4.0" + "@uform/core": "^1.0.0-alpha.0", + "@uform/shared": "^1.0.0-alpha.0" }, "publishConfig": { "access": "public" }, "gitHead": "4d068dad6183e8da294a4c899a158326c0b0b050" -} \ No newline at end of file +} diff --git a/packages/shared/package.json b/packages/shared/package.json index fdf93918d05..732aa804b4b 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@uform/shared", - "version": "0.4.0", + "version": "1.0.0-alpha.0", "license": "MIT", "main": "lib", "types": "lib/index.d.ts", diff --git a/packages/validator/package.json b/packages/validator/package.json index d3298e901bb..a3efacdab91 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -1,6 +1,6 @@ { "name": "@uform/validator", - "version": "0.4.0", + "version": "1.0.0-alpha.0", "license": "MIT", "main": "lib", "repository": { @@ -25,7 +25,7 @@ "@babel/runtime": "^7.4.4" }, "dependencies": { - "@uform/shared": "^0.4.0", + "@uform/shared": "^1.0.0-alpha.0", "@uform/types": "^0.4.0" }, "publishConfig": { From c2ca2ba9b1914ad33f3ae47d879da7686824438c Mon Sep 17 00:00:00 2001 From: Janry Date: Sat, 23 Nov 2019 16:40:28 +0800 Subject: [PATCH 64/99] fix(@uform/react-shared-components): fix preview (#418) --- packages/react-shared-components/src/PreviewText.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/react-shared-components/src/PreviewText.tsx b/packages/react-shared-components/src/PreviewText.tsx index cfc570ea5af..aa5319d2364 100644 --- a/packages/react-shared-components/src/PreviewText.tsx +++ b/packages/react-shared-components/src/PreviewText.tsx @@ -20,11 +20,18 @@ export const PreviewText: React.FC = props => { ) } return ( -

    +

    {props.addonBefore ? ' ' + props.addonBefore : ''} {props.innerBefore ? ' ' + props.innerBefore : ''} {props.addonTextBefore ? ' ' + props.addonTextBefore : ''} - {!value ? 'N/A' : value} + {value === '' || + value === undefined || + (Array.isArray(value) && value.length === 0) + ? 'N/A' + : String(value)} {props.addonTextAfter ? ' ' + props.addonTextAfter : ''} {props.innerAfter ? ' ' + props.innerAfter : ''} {props.addonAfter ? ' ' + props.addonAfter : ''} From 204b2ad87332d5a6ca65551d5e4550e31c6bb03b Mon Sep 17 00:00:00 2001 From: Janry Date: Sat, 23 Nov 2019 22:54:19 +0800 Subject: [PATCH 65/99] feat(@uform/antd/next): Support error scroll (#419) --- packages/antd/src/index.tsx | 36 +++++++++++++++++++++-- packages/core/src/__tests__/index.spec.ts | 6 ++-- packages/core/src/index.ts | 7 +++-- packages/next/src/index.tsx | 36 +++++++++++++++++++++-- 4 files changed, 76 insertions(+), 9 deletions(-) diff --git a/packages/antd/src/index.tsx b/packages/antd/src/index.tsx index 0cda03521a4..2142e852b6a 100644 --- a/packages/antd/src/index.tsx +++ b/packages/antd/src/index.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useRef } from 'react' import { SchemaMarkupForm, SchemaMarkupField @@ -10,6 +10,38 @@ export * from '@uform/react-schema-renderer' export * from './components' export * from './types' export { mapStyledProps, mapTextComponent } from './shared' -export const SchemaForm: React.FC = SchemaMarkupForm as any +export const SchemaForm: React.FC = props => { + const formRef = useRef() + + return ( +

    + { + if (props.onValidateFailed) { + props.onValidateFailed(result) + } + if (formRef.current) { + setTimeout(() => { + const elements = formRef.current.querySelectorAll( + '.ant-form-item-control.has-error' + ) + if (elements && elements.length) { + if (!elements[0].scrollIntoView) return + elements[0].scrollIntoView({ + behavior: 'smooth', + inline: 'center', + block: 'center' + }) + } + }, 30) + } + }} + > + {props.children} + +
    + ) +} export const Field: React.FC = SchemaMarkupField export default SchemaForm diff --git a/packages/core/src/__tests__/index.spec.ts b/packages/core/src/__tests__/index.spec.ts index 2ce768770e8..5302e0fbcd5 100644 --- a/packages/core/src/__tests__/index.spec.ts +++ b/packages/core/src/__tests__/index.spec.ts @@ -408,7 +408,9 @@ describe('validate', () => { path: 'c', rules: [() => ({ type: 'error', message: 'error msg' })] }) // CustomValidator error - await form.validate() + try { + await form.submit() + } catch (e) {} expect(onValidateFailedTrigger).toBeCalledTimes(1) }) @@ -1177,7 +1179,7 @@ describe('createMutators', () => { test('remove object key', async () => { const form = createForm({ useDirty: true }) - const initialValue = { username : '1234' } + const initialValue = { username: '1234' } const user = form.registerField({ path: 'user', initialValue }) form.registerField({ path: 'user.username' }) const mutators = form.createMutators(user) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a0db0b559be..c9754788737 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -732,6 +732,9 @@ export function createForm( state.submitting = false }) heart.publish(LifeCycleTypes.ON_FORM_SUBMIT_END, state) + if (isFn(options.onValidateFailed)) { + options.onValidateFailed(validated) + } return Promise.reject(validated.errors) } if (isFn(onSubmit)) { @@ -748,6 +751,7 @@ export function createForm( state.setState(state => { state.submitting = false }) + heart.publish(LifeCycleTypes.ON_FORM_SUBMIT_END, state) if (errors.length) { return Promise.reject(errors) @@ -781,9 +785,6 @@ export function createForm( state.setState(state => { state.validating = false }) - if (isFn(options.onValidateFailed) && payload.errors.length) { - options.onValidateFailed(payload) - } heart.publish(LifeCycleTypes.ON_FORM_VALIDATE_END, state) return payload }) diff --git a/packages/next/src/index.tsx b/packages/next/src/index.tsx index 7056bbbed78..27c4fac3fc1 100644 --- a/packages/next/src/index.tsx +++ b/packages/next/src/index.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useRef } from 'react' import { SchemaMarkupForm, SchemaMarkupField @@ -10,6 +10,38 @@ export * from '@uform/react-schema-renderer' export * from './components' export * from './types' export { mapStyledProps, mapTextComponent } from './shared' -export const SchemaForm: React.FC = SchemaMarkupForm as any +export const SchemaForm: React.FC = props => { + const formRef = useRef() + + return ( +
    + { + if (props.onValidateFailed) { + props.onValidateFailed(result) + } + if (formRef.current) { + setTimeout(() => { + const elements = formRef.current.querySelectorAll( + '.next-form-item.has-error' + ) + if (elements && elements.length) { + if (!elements[0].scrollIntoView) return + elements[0].scrollIntoView({ + behavior: 'smooth', + inline: 'center', + block: 'center' + }) + } + }, 30) + } + }} + > + {props.children} + +
    + ) +} export const Field: React.FC = SchemaMarkupField export default SchemaForm From 908882a2968db2b0fbde4792f6c8ca7dc2725016 Mon Sep 17 00:00:00 2001 From: Janry Date: Sun, 24 Nov 2019 14:26:06 +0800 Subject: [PATCH 66/99] feat(@uform/core): support pass visible/display of register method (#421) --- docs/Examples/antd/Sample.md | 1 + packages/antd/src/compat/Form.tsx | 27 +++++++------ packages/antd/src/components/FormStep.tsx | 32 +++++++-------- packages/antd/src/types.ts | 4 +- packages/core/README.md | 4 ++ packages/core/README.zh-cn.md | 4 ++ packages/core/src/index.ts | 33 ++++++++++++++-- packages/core/src/types.ts | 4 ++ packages/next/src/compat/Form.tsx | 24 +++++++----- packages/next/src/components/FormStep.tsx | 35 ++++++++--------- packages/next/src/types.ts | 4 +- packages/react-schema-renderer/README.md | 28 ++++++++++++- .../react-schema-renderer/README.zh-cn.md | 36 +++++++++++++++-- .../src/components/SchemaField.tsx | 2 + .../src/shared/schema.ts | 39 +++++++++++++++++-- packages/react-schema-renderer/src/types.ts | 2 + .../src/PreviewText.tsx | 21 ++++++++-- packages/react/README.md | 5 +++ packages/react/README.zh-cn.md | 8 ++++ packages/react/src/hooks/useField.ts | 37 +++++++++--------- packages/react/src/hooks/useFormEffects.ts | 3 +- packages/react/src/hooks/useFormSpy.ts | 4 +- packages/react/src/hooks/useVirtualField.ts | 5 +-- 23 files changed, 265 insertions(+), 97 deletions(-) diff --git a/docs/Examples/antd/Sample.md b/docs/Examples/antd/Sample.md index 8320628a3a1..d3ef12a4f55 100644 --- a/docs/Examples/antd/Sample.md +++ b/docs/Examples/antd/Sample.md @@ -56,6 +56,7 @@ ReactDOM.render( enum={['1', '2', '3', '4']} title="Radio" name="radio" + default={'2'} /> = props => { +import { + PreviewText, + PreviewTextConfigProps +} from '@uform/react-shared-components' +export const CompatAntdForm: React.FC = props => { return ( -
    + + + ) } diff --git a/packages/antd/src/components/FormStep.tsx b/packages/antd/src/components/FormStep.tsx index e7da9be9c79..2a960efe34a 100644 --- a/packages/antd/src/components/FormStep.tsx +++ b/packages/antd/src/components/FormStep.tsx @@ -1,9 +1,8 @@ -import React, { useState, useMemo, useRef } from 'react' +import React, { useState, useMemo, useRef, Fragment } from 'react' import { createControllerBox, ISchemaVirtualFieldComponentProps, createEffectHook, - FormEffectHooks, useFormEffects } from '@uform/react-schema-renderer' import { toArr } from '@uform/shared' @@ -31,7 +30,7 @@ type StepComponentExtendsProps = StateMap export const FormStep: React.FC & StepComponentExtendsProps = createControllerBox( 'step', - ({ form, schema }: ISchemaVirtualFieldComponentProps) => { + ({ form, schema, children }: ISchemaVirtualFieldComponentProps) => { const [current, setCurrent] = useState(0) const ref = useRef(current) const { dataSource, ...stepProps } = schema.getExtendsComponentProps() @@ -44,21 +43,17 @@ export const FormStep: React.FC & setCurrent(cur) } useFormEffects(({ setFieldState }) => { - FormEffectHooks.onFormInit$().subscribe(() => { - items.forEach(({ name }, index) => { - setFieldState(name, (state: any) => { - state.display = index === current - }) + items.forEach(({ name }, index) => { + setFieldState(name, (state: any) => { + state.display = index === current }) }) EffectHooks.onStepCurrentChange$().subscribe(({ value }) => { items.forEach(({ name }, index) => { if (!name) throw new Error('FormStep dataSource must include `name` property') - setTimeout(() => { - setFieldState(name, (state: any) => { - state.display = index === value - }) + setFieldState(name, (state: any) => { + state.display = index === value }) }) }) @@ -92,11 +87,14 @@ export const FormStep: React.FC & }, []) ref.current = current return ( - - {items.map((props, key) => { - return - })} - + + + {items.map((props, key) => { + return + })} + {' '} + {children} + ) } ) as any diff --git a/packages/antd/src/types.ts b/packages/antd/src/types.ts index 1862a88a983..581c913bcd9 100644 --- a/packages/antd/src/types.ts +++ b/packages/antd/src/types.ts @@ -9,13 +9,15 @@ import { IMarkupSchemaFieldProps, ISchemaFieldComponentProps } from '@uform/react-schema-renderer' +import { PreviewTextConfigProps } from '@uform/react-shared-components' import { StyledComponent } from 'styled-components' type ColSpanType = number | string export type IAntdSchemaFormProps = ISchemaFormProps & FormProps & - IFormItemTopProps + IFormItemTopProps & + PreviewTextConfigProps export type IAntdSchemaFieldProps = IMarkupSchemaFieldProps diff --git a/packages/core/README.md b/packages/core/README.md index 545dc1e2277..60f09ca6b66 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -594,6 +594,8 @@ interface IForm { value?: any // Field multi-value values?: any[] // Field initial value initialValue?: any // Field extension properties + visible?: boolean //Field initial visible status(Whether the data is visible) + display?: boolean //Field initial display status(Whether the style is visible) props?: any // Field check rule rules?: ValidatePatternRules[] // Field is required required?: boolean // Is the field editable? @@ -608,6 +610,8 @@ interface IForm { // Node path path?: FormPathPattern // Data path name?: string // Field extension properties + visible?: boolean //Field initial visible status(Whether the data and style is visible) + display?: boolean //Field initial display status(Whether the style is visible) props?: any // Whether the field is dirty check useDirty?: boolean // Field state calculation container, mainly used to extend the core linkage rules computeState?: (draft: IFieldState, prevState: IFieldState) => void diff --git a/packages/core/README.zh-cn.md b/packages/core/README.zh-cn.md index d5f118c85c8..f627f75d4e3 100644 --- a/packages/core/README.zh-cn.md +++ b/packages/core/README.zh-cn.md @@ -604,6 +604,10 @@ interface IForm { values?: any[] //字段初始值 initialValue?: any + //数据与样式是否可见 + visible?: boolean + //样式是否可见 + display?: boolean //字段扩展属性 props?: any //字段校验规则 diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c9754788737..50b7d57f2c1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -287,6 +287,8 @@ export function createForm( name, path, props, + display, + visible, computeState, useDirty }: IVirtualFieldStateProps): IVirtualField { @@ -309,6 +311,12 @@ export function createForm( field.setState((state: IVirtualFieldState) => { state.initialized = true state.props = props + if (isValid(visible)) { + state.visible = visible + } + if (isValid(display)) { + state.display = display + } }) batchRunTaskQueue(field, nodePath) }) @@ -335,6 +343,8 @@ export function createForm( required, rules, editable, + visible, + display, computeState, useDirty, props @@ -370,7 +380,12 @@ export function createForm( state.initialValue = isValid(initialValue) ? initialValue : formInitialValue - + if (isValid(visible)) { + state.visible = visible + } + if (isValid(display)) { + state.display = display + } state.props = props state.required = required state.rules = rules as any @@ -380,9 +395,21 @@ export function createForm( batchRunTaskQueue(field, nodePath) }) validator.register(nodePath, validate => { - const { value, rules, editable, visible, unmounted } = field.getState() + const { + value, + rules, + editable, + visible, + unmounted, + display + } = field.getState() // 不需要校验的情况有: 非编辑态(editable),已销毁(unmounted), 逻辑上不可见(visible) - if (editable === false || visible === false || unmounted === true) + if ( + editable === false || + visible === false || + unmounted === true || + display === false + ) return validate(value, []) clearTimeout((field as any).validateTimer) ;(field as any).validateTimer = setTimeout(() => { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 223e8df8a58..ec8ba9692d1 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -161,6 +161,8 @@ export interface IFieldStateProps { rules?: ValidatePatternRules[] required?: boolean editable?: boolean + visible?: boolean + display?: boolean useDirty?: boolean computeState?: (draft: IFieldState, prevState: IFieldState) => void } @@ -244,6 +246,8 @@ export interface IVirtualFieldStateProps { path?: FormPathPattern dataPath?: FormPathPattern nodePath?: FormPathPattern + display?: boolean + visible?: boolean useDirty?: boolean computeState?: ( draft: IVirtualFieldState, diff --git a/packages/next/src/compat/Form.tsx b/packages/next/src/compat/Form.tsx index 3ea7255f778..5ee05f200b7 100644 --- a/packages/next/src/compat/Form.tsx +++ b/packages/next/src/compat/Form.tsx @@ -4,18 +4,24 @@ import { FormProps } from '@alifd/next/types/form' import { IFormItemTopProps } from '../types' import { FormItemProvider } from './context' import { normalizeCol } from '../shared' +import { + PreviewText, + PreviewTextConfigProps +} from '@uform/react-shared-components' -export const CompatNextForm: React.FC< - FormProps & IFormItemTopProps -> = props => { +export const CompatNextForm: React.FC = props => { return ( - + + + ) } diff --git a/packages/next/src/components/FormStep.tsx b/packages/next/src/components/FormStep.tsx index 739fc3d8135..d40175409e1 100644 --- a/packages/next/src/components/FormStep.tsx +++ b/packages/next/src/components/FormStep.tsx @@ -1,10 +1,9 @@ -import React, { useState, useMemo, useRef } from 'react' +import React, { useState, useMemo, useRef, Fragment } from 'react' import { createControllerBox, ISchemaVirtualFieldComponentProps, createEffectHook, - useFormEffects, - FormEffectHooks + useFormEffects } from '@uform/react-schema-renderer' import { toArr } from '@uform/shared' import { Step } from '@alifd/next' @@ -31,7 +30,7 @@ type StepComponentExtendsProps = StateMap export const FormStep: React.FC & StepComponentExtendsProps = createControllerBox( 'step', - ({ props, form }: ISchemaVirtualFieldComponentProps) => { + ({ props, form, children }: ISchemaVirtualFieldComponentProps) => { const [current, setCurrent] = useState(0) const ref = useRef(current) const { dataSource, ...stepProps } = props['x-component-props'] || {} @@ -44,21 +43,18 @@ export const FormStep: React.FC & setCurrent(cur) } useFormEffects(({ setFieldState }) => { - FormEffectHooks.onFormInit$().subscribe(() => { - items.forEach(({ name }, index) => { - setFieldState(name, (state: any) => { - state.display = index === current - }) + items.forEach(({ name }, index) => { + setFieldState(name, (state: any) => { + state.display = index === current }) }) EffectHooks.onStepCurrentChange$().subscribe(({ value }) => { items.forEach(({ name }, index) => { if (!name) throw new Error('FormStep dataSource must include `name` property') - setTimeout(() => { - setFieldState(name, (state: any) => { - state.display = index === value - }) + + setFieldState(name, (state: any) => { + state.display = index === value }) }) }) @@ -92,11 +88,14 @@ export const FormStep: React.FC & }, []) ref.current = current return ( - - {items.map((props, key) => { - return - })} - + + + {items.map((props, key) => { + return + })} + + {children} + ) } ) as any diff --git a/packages/next/src/types.ts b/packages/next/src/types.ts index b93217d9e24..9caa9688002 100644 --- a/packages/next/src/types.ts +++ b/packages/next/src/types.ts @@ -6,13 +6,15 @@ import { IMarkupSchemaFieldProps, ISchemaFieldComponentProps } from '@uform/react-schema-renderer' +import { PreviewTextConfigProps } from '@uform/react-shared-components' import { StyledComponent } from 'styled-components' type ColSpanType = number | string export type INextSchemaFormProps = ISchemaFormProps & FormProps & - IFormItemTopProps + IFormItemTopProps & + PreviewTextConfigProps export type INextSchemaFieldProps = IMarkupSchemaFieldProps diff --git a/packages/react-schema-renderer/README.md b/packages/react-schema-renderer/README.md index 5696d051485..d67a3ab0f3a 100644 --- a/packages/react-schema-renderer/README.md +++ b/packages/react-schema-renderer/README.md @@ -50,6 +50,8 @@ npm install --save @uform/react-schema-renderer - [`getExtendsRules`](#getextendsrules) - [`getExtendsRequired`](#getextendsrequired) - [`getExtendsEditable`](#getextendseditable) + - [`getExtendsVisible`](#getextendsvisible) + - [`getExtendsDisplay`](#getextendsdisplay) - [`getExtendsTriggerType`](#getextendstriggertype) - [`getExtendsProps`](#getextendsprops) - [`getExtendsComponent`](#getextendscomponent) @@ -922,7 +924,9 @@ cleanRegistry(): void | additionalItems | Extra array element description | `Schema` | | patternProperties | Dynamically match the schema of an attribute of an object | `{[key : string]:Schema}` | | additionalProperties | Schema matching the extra attributes of the object | `Schema` | -| editable | Whether the field is editable | `boolean` | +| editable | Whether the field is editable | `boolean` | +| visible | Whether the data and style is visible | `boolean` | +| display | Whether the style is visible | `boolean` | | x-props | Field extension attribute | `{ [name: string]: any }` | | x-index | Field order | `number` | | x-rules | Field check rule | `ValidatePatternRules` | @@ -1185,6 +1189,26 @@ const schema4 = new Schema({ schema4.getExtendsEditable() // false ``` +##### `getExtendsVisible` + +> Get data and style visible property + +签名 + +```typescript +getExtendsVisible(): boolean +``` + +##### `getExtendsDisplay` + +> Get style visible property + +签名 + +``` +getExtendsDisplay() : boolean +``` + ##### `getExtendsTriggerType` > Get the triggerType, which is consistent with the getExtendsTriggerType capability, and is capable of providing protocol differences. @@ -1711,6 +1735,8 @@ interface ISchema { } additionalProperties?: ISchema /** extend json schema specs */ + visible?: boolean //Field initial visible status(Whether the data is visible) + display?: boolean //Field initial display status(Whether the style is visible) editable?: boolean ['x-props']?: { [name: string]: any } ['x-index']?: number diff --git a/packages/react-schema-renderer/README.zh-cn.md b/packages/react-schema-renderer/README.zh-cn.md index 4a3cb1450ab..c9628d7b855 100644 --- a/packages/react-schema-renderer/README.zh-cn.md +++ b/packages/react-schema-renderer/README.zh-cn.md @@ -920,8 +920,10 @@ cleanRegistry(): void | additionalItems | 额外数组元素描述 | `Schema` | | patternProperties | 动态匹配对象的某个属性的 Schema | `{[key : string]:Schema}` | | additionalProperties | 匹配对象额外属性的 Schema | `Schema` | -| editable | 字段是否可编辑 | `boolean` | -| x-props | 字段扩展属性 | `{ [name: string]: any }` | +| editable | 字段是否可编辑 | `boolean` | +| visible | 字段是否可见(数据+样式) | `boolean` | +| display | 字段样式是否可见 | `boolean` | +| x-props | 字段扩展属性 | `{ [name: string]: any }` | | x-index | 字段顺序 | `number` | | x-rules | 字段校验规则 | `ValidatePatternRules` | | x-component | 字段 UI 组件 | `string` | @@ -1183,9 +1185,33 @@ const schema4 = new Schema({ schema4.getExtendsEditable() // false ``` + + +##### `getExtendsVisible` + +> 获取数据样式可见属性 + +签名 + +```typescript +getExtendsVisible(): boolean +``` + +##### `getExtendsDisplay` + +> 获取样式可见属性 + +签名 + +``` +getExtendsDisplay() : boolean +``` + + + ##### `getExtendsTriggerType` -> 获取 triggerType,与 getExtendsTriggerType 能力一致,都是提供协议差异抹平的能力 +> 获取 triggerType,与 getExtendsEditable 能力一致,都是提供协议差异抹平的能力 签名 @@ -1710,6 +1736,10 @@ interface ISchema { additionalProperties?: ISchema /** extend json schema specs */ editable?: boolean + //数据与样式是否可见 + visible?: boolean + //样式是否可见 + display?: boolean ['x-props']?: { [name: string]: any } ['x-index']?: number ['x-rules']?: ValidatePatternRules diff --git a/packages/react-schema-renderer/src/components/SchemaField.tsx b/packages/react-schema-renderer/src/components/SchemaField.tsx index 49e1e0cdd25..1742a23e86a 100644 --- a/packages/react-schema-renderer/src/components/SchemaField.tsx +++ b/packages/react-schema-renderer/src/components/SchemaField.tsx @@ -62,6 +62,8 @@ export const SchemaField: React.FunctionComponent = ( props={fieldSchema.getSelfProps()} triggerType={fieldSchema.getExtendsTriggerType()} editable={fieldSchema.getExtendsEditable()} + visible={fieldSchema.getExtendsVisible()} + display={fieldSchema.getExtendsDisplay()} required={fieldSchema.getExtendsRequired()} rules={fieldSchema.getExtendsRules()} computeState={computeSchemaState} diff --git a/packages/react-schema-renderer/src/shared/schema.ts b/packages/react-schema-renderer/src/shared/schema.ts index 576a4dd4dff..435a017dcbe 100644 --- a/packages/react-schema-renderer/src/shared/schema.ts +++ b/packages/react-schema-renderer/src/shared/schema.ts @@ -60,6 +60,8 @@ export class Schema implements ISchema { public additionalProperties?: Schema /** extend json schema specs */ public editable?: boolean + public visible?: boolean + public display?: boolean public ['x-props']?: { [name: string]: any } public ['x-index']?: number public ['x-rules']?: ValidatePatternRules @@ -146,7 +148,7 @@ export class Schema implements ISchema { return props } getExtendsRules() { - let rules: ValidateArrayRules= [] + let rules: ValidateArrayRules = [] if (this.format) { rules.push({ format: this.format }) } @@ -238,12 +240,39 @@ export class Schema implements ISchema { return this.editable } else if (isValid(this['x-props']) && isValid(this['x-props'].editable)) { return this['x-props'].editable - } else if(isValid(this['x-component-props']) && isValid(this['x-component-props'].editable)){ + } else if ( + isValid(this['x-component-props']) && + isValid(this['x-component-props'].editable) + ) { return this['x-component-props'].editable - }else if (isValid(this.readOnly)) { + } else if (isValid(this.readOnly)) { return !this.readOnly } } + getExtendsVisible(): boolean { + if (isValid(this.visible)) { + return this.visible + } else if (isValid(this['x-props']) && isValid(this['x-props'].visible)) { + return this['x-props'].visible + } else if ( + isValid(this['x-component-props']) && + isValid(this['x-component-props'].visible) + ) { + return this['x-component-props'].visible + } + } + getExtendsDisplay(): boolean { + if (isValid(this.display)) { + return this.display + } else if (isValid(this['x-props']) && isValid(this['x-props'].display)) { + return this['x-props'].display + } else if ( + isValid(this['x-component-props']) && + isValid(this['x-component-props'].display) + ) { + return this['x-component-props'].display + } + } getExtendsTriggerType() { const itemProps = this.getExtendsItemProps() const props = this.getExtendsProps() @@ -374,7 +403,9 @@ export class Schema implements ISchema { return Schema.getOrderProperties(this, 'patternProperties') } - unrelease_mapPatternProperties(callback?: (schema: Schema, key: string) => any) { + unrelease_mapPatternProperties( + callback?: (schema: Schema, key: string) => any + ) { return this.unrelease_getOrderPatternProperties().map(({ schema, key }) => { return callback(schema, key) }) diff --git a/packages/react-schema-renderer/src/types.ts b/packages/react-schema-renderer/src/types.ts index 1c81a529519..ed4ed74d7f2 100644 --- a/packages/react-schema-renderer/src/types.ts +++ b/packages/react-schema-renderer/src/types.ts @@ -110,6 +110,8 @@ export interface ISchema { additionalProperties?: ISchema /** extend json schema specs */ editable?: boolean + visible?: boolean + display?: boolean ['x-props']?: { [name: string]: any } ['x-index']?: number ['x-rules']?: ValidatePatternRules diff --git a/packages/react-shared-components/src/PreviewText.tsx b/packages/react-shared-components/src/PreviewText.tsx index aa5319d2364..63e0c44f58d 100644 --- a/packages/react-shared-components/src/PreviewText.tsx +++ b/packages/react-shared-components/src/PreviewText.tsx @@ -1,7 +1,17 @@ -import React from 'react' +import React, { useContext, createContext } from 'react' +import { isFn } from '@uform/shared' import { IPreviewTextProps } from './types' -export const PreviewText: React.FC = props => { +export interface PreviewTextConfigProps { + previewPlaceholder?: string | ((props: IPreviewTextProps) => string) +} + +const PreviewTextContext = createContext({}) + +export const PreviewText: React.FC & { + ConfigProvider: React.Context['Provider'] +} = props => { + const context = useContext(PreviewTextContext) || {} let value: any if (props.dataSource && props.dataSource.length) { let find = props.dataSource.filter(({ value }) => @@ -19,6 +29,9 @@ export const PreviewText: React.FC = props => { props.value === undefined || props.value === null ? '' : props.value ) } + const placeholder = isFn(context.previewPlaceholder) + ? context.previewPlaceholder(props) + : context.previewPlaceholder return (

    = props => { {value === '' || value === undefined || (Array.isArray(value) && value.length === 0) - ? 'N/A' + ? placeholder || 'N/A' : String(value)} {props.addonTextAfter ? ' ' + props.addonTextAfter : ''} {props.innerAfter ? ' ' + props.innerAfter : ''} @@ -38,3 +51,5 @@ export const PreviewText: React.FC = props => {

    ) } + +PreviewText.ConfigProvider = PreviewTextContext.Provider diff --git a/packages/react/README.md b/packages/react/README.md index 5a790c37a45..4160a59bf14 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -1334,7 +1334,10 @@ interface IFieldStateUIProps { name?: string // Field value, is equal to values[0] value?: any // Field multi-parameter value, such as when the field onChange trigger, the event callback passed multi-parameter data, then the value of all parameters will be stored here values?: any[] // Initial value + initialValue?: any // field extension properties + visible?: boolean //Field initial visible status(Whether the data and style is visible) + display?: boolean //Field initial display status(Whether the style is visible) props?: FieldProps // Check the rules, the specific type description refers to the following documents rules?: ValidatePatternRules[] // Is it required? required?: boolean // Is it editable? @@ -1556,6 +1559,8 @@ interface IVirtualFieldProps { path?: FormPathPattern // Node path nodePath?: FormPathPattern // Data path dataPath?: FormPathPattern // Data path + visible?: boolean //Field initial visible status(Whether the data and style is visible) + display?: boolean //Field initial display status(Whether the style is visible) name?: string // Form extension properties props?: FieldProps // Whether to use the dirty check, the default will go immer accurate update useDirty?: boolean diff --git a/packages/react/README.zh-cn.md b/packages/react/README.zh-cn.md index 7e5c84f98ae..b386a62c820 100644 --- a/packages/react/README.zh-cn.md +++ b/packages/react/README.zh-cn.md @@ -1352,6 +1352,10 @@ interface IFieldStateUIProps { values?: any[] //初始值 initialValue?: any + //数据与样式是否可见 + visible?: boolean + //样式是否可见 + display?: boolean //字段扩展属性 props?: FieldProps //校验规则,具体类型描述参考后面文档 @@ -1582,6 +1586,10 @@ interface IVirtualFieldProps { nodePath?: FormPathPattern //数据路径 dataPath?: FormPathPattern + //数据与样式是否可见 + visible?: boolean + //样式是否可见 + display?: boolean //数据路径 name?: string //字段扩展属性 diff --git a/packages/react/src/hooks/useField.ts b/packages/react/src/hooks/useField.ts index 39876b8d174..96ac92c4b43 100644 --- a/packages/react/src/hooks/useField.ts +++ b/packages/react/src/hooks/useField.ts @@ -1,18 +1,16 @@ import { useMemo, useEffect, useRef, useContext } from 'react' import { each, isFn } from '@uform/shared' -import { - IFieldState, - IForm, - IField, - IMutators -} from '@uform/core' -import { raf, getValueFromEvent } from '../shared' +import { IFieldState, IForm, IField, IMutators } from '@uform/core' +import { getValueFromEvent } from '../shared' import { useDirty } from './useDirty' import { useForceUpdate } from './useForceUpdate' import { IFieldHook, IFieldStateUIProps } from '../types' import FormContext from '../context' -const extendMutators = (mutators: IMutators, props: IFieldStateUIProps): IMutators => { +const extendMutators = ( + mutators: IMutators, + props: IFieldStateUIProps +): IMutators => { return { ...mutators, change: (...args) => { @@ -23,18 +21,23 @@ const extendMutators = (mutators: IMutators, props: IFieldStateUIProps): IMutato }, blur: () => { mutators.blur() - if (props.triggerType === 'onBlur') { + if (props.triggerType === 'onBlur') { mutators.validate() } } } } -export const useField = ( - options: IFieldStateUIProps -): IFieldHook => { +export const useField = (options: IFieldStateUIProps): IFieldHook => { const forceUpdate = useForceUpdate() - const dirty = useDirty(options, ['props', 'rules', 'required', 'editable']) + const dirty = useDirty(options, [ + 'props', + 'rules', + 'required', + 'editable', + 'visible', + 'display' + ]) const ref = useRef<{ field: IField unmounted: boolean @@ -52,6 +55,7 @@ export const useField = ( let initialized = false ref.current.field = form.registerField(options) ref.current.subscriberId = ref.current.field.subscribe(fieldState => { + if (ref.current.unmounted) return /** * 同步Field状态只需要forceUpdate一下触发重新渲染,因为字段状态全部代理在uform core内部 */ @@ -61,15 +65,12 @@ export const useField = ( mutators.validate() } } - raf(() => { - if (ref.current.unmounted) return - forceUpdate() - }) + forceUpdate() } }) initialized = true return extendMutators(form.createMutators(ref.current.field), options) - }, []) + }, [true]) useEffect(() => { if (dirty.num > 0) { diff --git a/packages/react/src/hooks/useFormEffects.ts b/packages/react/src/hooks/useFormEffects.ts index aeee34fb8f6..878e5ccc494 100644 --- a/packages/react/src/hooks/useFormEffects.ts +++ b/packages/react/src/hooks/useFormEffects.ts @@ -1,5 +1,5 @@ import { useContext, useEffect } from 'react' -import { isStateModel, LifeCycleTypes } from '@uform/core' +import { isStateModel } from '@uform/core' import FormContext from '../context' import { useEva } from 'react-eva' import { IFormEffect } from '../types' @@ -17,7 +17,6 @@ export function useFormEffects(effects: IFormEffect) { return isStateModel(payload) ? payload.getState() : payload }) }) - dispatch(LifeCycleTypes.ON_FORM_INIT, form.getFormState()) return () => { form.unsubscribe(subscribeId) } diff --git a/packages/react/src/hooks/useFormSpy.ts b/packages/react/src/hooks/useFormSpy.ts index 7d194487ae6..0c6778e7d32 100644 --- a/packages/react/src/hooks/useFormSpy.ts +++ b/packages/react/src/hooks/useFormSpy.ts @@ -7,7 +7,7 @@ import { useState, useReducer } from 'react' -import { FormHeartSubscriber, LifeCycleTypes } from '@uform/core' +import { FormHeartSubscriber, LifeCycleTypes, IForm } from '@uform/core' import { isStr, FormPath, isArr } from '@uform/shared' import { IFormSpyProps, ISpyHook } from '../types' import FormContext, { BroadcastContext } from '../context' @@ -58,7 +58,7 @@ export const useFormSpy = (props: IFormSpyProps): ISpyHook => { } } }, []) - const formApi = form ? form : broadcast && broadcast.getContext() + const formApi: IForm = form ? form : broadcast && broadcast.getContext() return { form: formApi, type, diff --git a/packages/react/src/hooks/useVirtualField.ts b/packages/react/src/hooks/useVirtualField.ts index 47f3806e441..21da603a92f 100644 --- a/packages/react/src/hooks/useVirtualField.ts +++ b/packages/react/src/hooks/useVirtualField.ts @@ -3,7 +3,6 @@ import { each } from '@uform/shared' import { IVirtualFieldStateProps, IVirtualFieldState, IForm } from '@uform/core' import { useDirty } from './useDirty' import { useForceUpdate } from './useForceUpdate' -import { raf } from '../shared' import { IVirtualFieldHook } from '../types' import FormContext from '../context' @@ -29,9 +28,7 @@ export const useVirtualField = ( * 同步Field状态只需要forceUpdate一下触发重新渲染,因为字段状态全部代理在uform core内部 */ if (initialized) { - raf(() => { - forceUpdate() - }) + forceUpdate() } }) initialized = true From 670fadbe75935051b5b3a202423975ef223afe5a Mon Sep 17 00:00:00 2001 From: quirkyvar Date: Sun, 24 Nov 2019 15:33:17 +0800 Subject: [PATCH 67/99] feat(@uform/react): remove raf and fix unittest (#422) --- packages/react/README.md | 91 +++++++++++++++++- packages/react/README.zh-cn.md | 93 ++++++++++++++++++- .../react/src/__tests__/useField.spec.tsx | 32 ++++--- .../src/__tests__/useVirtualField.spec.tsx | 10 +- 4 files changed, 203 insertions(+), 23 deletions(-) diff --git a/packages/react/README.md b/packages/react/README.md index 4160a59bf14..40d16020d9d 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -37,6 +37,7 @@ npm install --save @uform/react - [`Async Linkage`](#Async-Linkage) - [`Linkage Validation`](#Linkage-Validation) - [`Complex Linkage`](#Complex-Linkage) + - [`Reuse Effects`](#Reuse-Effects) - [`Combo`](#Combo) - [`Provide and FormSpy`](#Provide-and-FormSpy) - [`Deconstruction`](#Deconstruction) @@ -1022,6 +1023,94 @@ const App = () => { ReactDOM.render(, document.getElementById('root')) ``` +#### Reuse Effects + +Make your own reusable effects. + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, FormEffectHooks } from '@uform/react' + + +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) +const CheckedField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : { + mutators.change(!state.value) + }} checked={!!state.value} /> } + {state.errors} + {state.warnings} + + }} + +) + +const { onFormMount$, onFieldValueChange$ } = FormEffectHooks +const getEffects = ()=>{ + const actions = createFormActions() + onFormMount$().subscribe(() => { + actions.setFieldState('a~', state => state.visible = false) + }) + + onFieldValueChange$('trigger').subscribe((triggerState) => { + actions.setFieldState('a~', state => { + state.visible = triggerState.value + }) + }) + + onFieldValueChange$('a').subscribe((fieldState) => { + actions.setFieldState('a-copy', state => { + state.value = fieldState.value + }) + }) +} + +const actions = createFormActions() +const App = () => { + return ( + { + getEffects() + }} + > + +
    + +
    +
    + +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + #### Combo Example:Combo value of username and age. Check [FormSpy](#FormSpy) for more inforation. @@ -1610,7 +1699,7 @@ const App = () => { {({ state, mutator }) => { return ( - + {({ state: layoutState }) => { return ( { ReactDOM.render(, document.getElementById('root')) ``` +#### 复用Effects + +自定义可复用的effects + + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, createFormActions, FormEffectHooks } from '@uform/react' + + +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) +const CheckedField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : { + mutators.change(!state.value) + }} checked={!!state.value} /> } + {state.errors} + {state.warnings} + + }} + +) + +const { onFormMount$, onFieldValueChange$ } = FormEffectHooks +const getEffects = ()=>{ + const actions = createFormActions() + onFormMount$().subscribe(() => { + actions.setFieldState('a~', state => state.visible = false) + }) + + onFieldValueChange$('trigger').subscribe((triggerState) => { + actions.setFieldState('a~', state => { + state.visible = triggerState.value + }) + }) + + onFieldValueChange$('a').subscribe((fieldState) => { + actions.setFieldState('a-copy', state => { + state.value = fieldState.value + }) + }) +} + +const actions = createFormActions() +const App = () => { + return ( +
    { + getEffects() + }} + > + +
    + +
    +
    + +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + + #### combo 字段 示例:combo username 和 age 字段, 更多用法,请点击[FormSpy](#FormSpy)查看 @@ -1644,7 +1735,7 @@ const App = () => { {({ state, mutator }) => { return ( - + {({ state: layoutState }) => { return ( { return {children} } - const { result, waitForNextUpdate } = renderHook(() => useField({ name: 'username' }), { wrapper: formWrapper }) + const { result } = renderHook(() => useField({ name: 'username' }), { wrapper: formWrapper }) expect(result.current.state.value).toEqual(undefined) act(() => { globalForm.setFormState(state => state.values.username = 'abcd') }) - // forceUpdate will trigger in raf, use waitForNextUpdate - expect(result.current.state.value).toEqual(undefined) - await waitForNextUpdate() - expect(result.current.state.value).toEqual('abcd') + expect(result.current.state.value).toEqual('abcd') }) test('mounted change', async ()=>{ @@ -95,7 +92,7 @@ describe('useField hook',()=>{ editable: true, rules: [], } - const { result, waitForNextUpdate, rerender } = renderHook(() => useField(initialProps), { wrapper: formWrapper }) + const { result, rerender } = renderHook(() => useField(initialProps), { wrapper: formWrapper }) expect(result.current.props).toEqual({ disabled: true }) expect(result.current.state.required).toEqual(false) expect(result.current.state.editable).toEqual(true) @@ -112,7 +109,6 @@ describe('useField hook',()=>{ expect(result.current.state.rules).toEqual([]) rerender() - await waitForNextUpdate() expect(result.current.props).toEqual(initialProps.props) expect(result.current.state.required).toEqual(initialProps.required) @@ -152,11 +148,15 @@ describe('useField hook',()=>{ } const fieldProps = { name: 'username', required: true } - const { result: result1, waitForNextUpdate: waitForNextUpdate1 } = renderHook(() => useField(fieldProps), { wrapper: formWrapper }) + const { result: result1, rerender } = renderHook(() => useField(fieldProps), { wrapper: formWrapper }) expect(result1.current.state.errors).toEqual('') expect(result1.current.state.value).toEqual(undefined) - result1.current.mutators.change('') - await waitForNextUpdate1() + rerender() + act(() => { + result1.current.mutators.change('') + }) + + // await waitForNextUpdate1() expect(result1.current.state.value).toEqual('') expect(result1.current.state.errors).toEqual('') @@ -164,7 +164,10 @@ describe('useField hook',()=>{ expect(result2.current.state.errors).toEqual('') expect(result2.current.state.value).toEqual(undefined) - result2.current.mutators.change('') + act(() => { + result2.current.mutators.change('') + }) + await waitForNextUpdate2() expect(result2.current.state.value).toEqual('') expect(result2.current.state.errors).toEqual('This field is required') @@ -179,8 +182,11 @@ describe('useField hook',()=>{ const fieldProps = { name: 'username', required: true } const { result: result1, rerender } = renderHook(() => useField(fieldProps), { wrapper: formWrapper }) expect(result1.current.state.errors).toEqual('') - result1.current.mutators.blur() rerender() + act(() => { + result1.current.mutators.blur() + }) + expect(result1.current.state.errors).toEqual('') const { result: result2, waitForNextUpdate: waitForNextUpdate2 } = renderHook(() => useField({ ...fieldProps, triggerType: 'onBlur' }), { wrapper: formWrapper }) @@ -188,7 +194,7 @@ describe('useField hook',()=>{ act(() => { result2.current.mutators.blur() - }) + }) await waitForNextUpdate2() expect(result2.current.state.errors).toEqual('This field is required') }) diff --git a/packages/react/src/__tests__/useVirtualField.spec.tsx b/packages/react/src/__tests__/useVirtualField.spec.tsx index f4d74068d78..fb282d5ec29 100644 --- a/packages/react/src/__tests__/useVirtualField.spec.tsx +++ b/packages/react/src/__tests__/useVirtualField.spec.tsx @@ -48,15 +48,12 @@ describe('useVirtualField hook',()=>{ return {children} } - const { result, rerender } = renderHook(() => useVirtualField({ name: 'username' }), { wrapper: formWrapper }) + const { result } = renderHook(() => useVirtualField({ name: 'username' }), { wrapper: formWrapper }) expect(result.current.state.visible).toEqual(true) act(() => { globalForm.setFieldState('username', state => state.visible = false) }) - // forceUpdate will trigger in raf, use waitForNextUpdate - expect(result.current.state.visible).toEqual(true) - rerender() expect(result.current.state.visible).toEqual(false) }) @@ -91,15 +88,12 @@ describe('useVirtualField hook',()=>{ name: 'username', props: { disabled: true }, } - const { result, waitForNextUpdate, rerender } = renderHook(() => useVirtualField(initialProps), { wrapper: formWrapper }) + const { result, rerender } = renderHook(() => useVirtualField(initialProps), { wrapper: formWrapper }) expect(result.current.props).toEqual({ disabled: true }) initialProps.props = { disabled: false } expect(result.current.props).toEqual({ disabled: true }) - rerender() - await waitForNextUpdate() - expect(result.current.props).toEqual(initialProps.props) }) }) \ No newline at end of file From fbf893749fcf9da77995553d0c1b1d816b58d194 Mon Sep 17 00:00:00 2001 From: janrywang Date: Sun, 24 Nov 2019 15:58:39 +0800 Subject: [PATCH 68/99] chore(publish): v1.0.0-alpha.1 --- packages/antd/package.json | 8 ++++---- packages/core/package.json | 6 +++--- packages/next/package.json | 8 ++++---- packages/printer/package.json | 4 ++-- packages/react-schema-renderer/package.json | 8 ++++---- packages/react-shared-components/package.json | 4 ++-- packages/react/package.json | 6 +++--- packages/shared/package.json | 2 +- packages/validator/package.json | 4 ++-- 9 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/antd/package.json b/packages/antd/package.json index 7ad71217587..89da30cea81 100644 --- a/packages/antd/package.json +++ b/packages/antd/package.json @@ -1,6 +1,6 @@ { "name": "@uform/antd", - "version": "1.0.0-alpha.0", + "version": "1.0.0-alpha.1", "license": "MIT", "main": "lib", "module": "esm", @@ -30,9 +30,9 @@ "react-dom": ">=16.8.0" }, "dependencies": { - "@uform/react-schema-renderer": "^1.0.0-alpha.0", - "@uform/react-shared-components": "^1.0.0-alpha.0", - "@uform/shared": "^1.0.0-alpha.0", + "@uform/react-schema-renderer": "^1.0.0-alpha.1", + "@uform/react-shared-components": "^1.0.0-alpha.1", + "@uform/shared": "^1.0.0-alpha.1", "@uform/types": "^0.4.0", "classnames": "^2.2.6", "moveto": "^1.7.4", diff --git a/packages/core/package.json b/packages/core/package.json index a29a5680152..bc036376cee 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@uform/core", - "version": "1.0.0-alpha.0", + "version": "1.0.0-alpha.1", "license": "MIT", "main": "lib", "repository": { @@ -26,9 +26,9 @@ "scheduler": ">=0.11.2" }, "dependencies": { - "@uform/shared": "^1.0.0-alpha.0", + "@uform/shared": "^1.0.0-alpha.1", "@uform/types": "^0.4.0", - "@uform/validator": "^1.0.0-alpha.0", + "@uform/validator": "^1.0.0-alpha.1", "immer": "^3.2.0" }, "publishConfig": { diff --git a/packages/next/package.json b/packages/next/package.json index b491fb785be..07796e683d3 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@uform/next", - "version": "1.0.0-alpha.0", + "version": "1.0.0-alpha.1", "license": "MIT", "main": "lib", "module": "esm", @@ -28,9 +28,9 @@ "react-dom": ">=16.8.0" }, "dependencies": { - "@uform/react-schema-renderer": "^1.0.0-alpha.0", - "@uform/react-shared-components": "^1.0.0-alpha.0", - "@uform/shared": "^1.0.0-alpha.0", + "@uform/react-schema-renderer": "^1.0.0-alpha.1", + "@uform/react-shared-components": "^1.0.0-alpha.1", + "@uform/shared": "^1.0.0-alpha.1", "@uform/types": "^0.4.0", "classnames": "^2.2.6", "moveto": "^1.7.4", diff --git a/packages/printer/package.json b/packages/printer/package.json index e9c5163ae8d..23f79b3c6eb 100644 --- a/packages/printer/package.json +++ b/packages/printer/package.json @@ -1,6 +1,6 @@ { "name": "@uform/printer", - "version": "1.0.0-alpha.0", + "version": "1.0.0-alpha.1", "license": "MIT", "main": "lib", "repository": { @@ -26,7 +26,7 @@ "typescript": "^3.5.2" }, "dependencies": { - "@uform/react-schema-renderer": "^1.0.0-alpha.0", + "@uform/react-schema-renderer": "^1.0.0-alpha.1", "react-modal": "^3.8.1", "styled-components": "^4.1.1" }, diff --git a/packages/react-schema-renderer/package.json b/packages/react-schema-renderer/package.json index 7a79d4b4dae..afb32300685 100644 --- a/packages/react-schema-renderer/package.json +++ b/packages/react-schema-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@uform/react-schema-renderer", - "version": "1.0.0-alpha.0", + "version": "1.0.0-alpha.1", "license": "MIT", "main": "lib", "repository": { @@ -30,9 +30,9 @@ "react-eva": "^1.1.7" }, "dependencies": { - "@uform/react": "^1.0.0-alpha.0", - "@uform/shared": "^1.0.0-alpha.0", - "@uform/validator": "^1.0.0-alpha.0", + "@uform/react": "^1.0.0-alpha.1", + "@uform/shared": "^1.0.0-alpha.1", + "@uform/validator": "^1.0.0-alpha.1", "pascal-case": "^2.0.1" }, "publishConfig": { diff --git a/packages/react-shared-components/package.json b/packages/react-shared-components/package.json index 274d5b2a08c..91953c4ba66 100644 --- a/packages/react-shared-components/package.json +++ b/packages/react-shared-components/package.json @@ -1,6 +1,6 @@ { "name": "@uform/react-shared-components", - "version": "1.0.0-alpha.0", + "version": "1.0.0-alpha.1", "license": "MIT", "main": "lib", "types": "lib/index.d.ts", @@ -29,7 +29,7 @@ "@babel/runtime": "^7.4.4" }, "dependencies": { - "@uform/shared": "^1.0.0-alpha.0", + "@uform/shared": "^1.0.0-alpha.1", "@uform/types": "^0.4.0" } } diff --git a/packages/react/package.json b/packages/react/package.json index 0fc982210b6..c5af4d58774 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@uform/react", - "version": "1.0.0-alpha.0", + "version": "1.0.0-alpha.1", "license": "MIT", "main": "lib", "repository": { @@ -31,8 +31,8 @@ "scheduler": ">=0.11.2" }, "dependencies": { - "@uform/core": "^1.0.0-alpha.0", - "@uform/shared": "^1.0.0-alpha.0" + "@uform/core": "^1.0.0-alpha.1", + "@uform/shared": "^1.0.0-alpha.1" }, "publishConfig": { "access": "public" diff --git a/packages/shared/package.json b/packages/shared/package.json index 732aa804b4b..7e7c4972e5d 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@uform/shared", - "version": "1.0.0-alpha.0", + "version": "1.0.0-alpha.1", "license": "MIT", "main": "lib", "types": "lib/index.d.ts", diff --git a/packages/validator/package.json b/packages/validator/package.json index a3efacdab91..35be4f21dc0 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -1,6 +1,6 @@ { "name": "@uform/validator", - "version": "1.0.0-alpha.0", + "version": "1.0.0-alpha.1", "license": "MIT", "main": "lib", "repository": { @@ -25,7 +25,7 @@ "@babel/runtime": "^7.4.4" }, "dependencies": { - "@uform/shared": "^1.0.0-alpha.0", + "@uform/shared": "^1.0.0-alpha.1", "@uform/types": "^0.4.0" }, "publishConfig": { From 06677ad8965c81c8b1b6fb2ec3672d02f69e5407 Mon Sep 17 00:00:00 2001 From: Janry Date: Sun, 24 Nov 2019 22:31:43 +0800 Subject: [PATCH 69/99] fix(@uform/core): fix onsubmit immutable values (#424) --- packages/core/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 50b7d57f2c1..28a524dea63 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -124,7 +124,7 @@ export function createForm( }) if (valuesChanged) { if (isFn(options.onChange)) { - options.onChange(published.values) + options.onChange(clone(published.values)) } heart.publish(LifeCycleTypes.ON_FORM_VALUES_CHANGE, state) } @@ -766,7 +766,7 @@ export function createForm( } if (isFn(onSubmit)) { return Promise.resolve( - onSubmit(state.getState(state => state.values)) + onSubmit(state.getState(state => clone(state.values))) ).then(payload => ({ validated, payload })) } return { validated, payload: undefined } From a758a1c0b87d6de3865743cab9553a2f1e0d8ff5 Mon Sep 17 00:00:00 2001 From: Janry Date: Mon, 25 Nov 2019 10:09:35 +0800 Subject: [PATCH 70/99] Fix schema name (#425) --- packages/react-schema-renderer/README.md | 1 + packages/react-schema-renderer/README.zh-cn.md | 1 + .../__snapshots__/markup.spec.tsx.snap | 5 +++++ .../__snapshots__/register.spec.tsx.snap | 4 ++++ .../react-schema-renderer/src/shared/schema.ts | 17 +++++++++++------ 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/react-schema-renderer/README.md b/packages/react-schema-renderer/README.md index d67a3ab0f3a..448ac665d40 100644 --- a/packages/react-schema-renderer/README.md +++ b/packages/react-schema-renderer/README.md @@ -898,6 +898,7 @@ cleanRegistry(): void | Attribute name | Description | Type | | -------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | title | Field title | `React.ReactNode` | +| name | The parent property name of the field | `string` | | description | Field description | `React.ReactNode` | | default | Field default | `any` | | readOnly | Whether read-only and editable | `boolean` | diff --git a/packages/react-schema-renderer/README.zh-cn.md b/packages/react-schema-renderer/README.zh-cn.md index c9628d7b855..7ac682a7b5b 100644 --- a/packages/react-schema-renderer/README.zh-cn.md +++ b/packages/react-schema-renderer/README.zh-cn.md @@ -894,6 +894,7 @@ cleanRegistry(): void | 属性名 | 描述 | 类型 | | -------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | title | 字段标题 | `React.ReactNode` | +| name | 字段所属的父节点属性名 | `string` | | description | 字段描述 | `React.ReactNode` | | default | 字段默认值 | `any` | | readOnly | 是否只读与 editable 一致 | `boolean` | diff --git a/packages/react-schema-renderer/src/__tests__/__snapshots__/markup.spec.tsx.snap b/packages/react-schema-renderer/src/__tests__/__snapshots__/markup.spec.tsx.snap index ec96f5025c0..31b34c8d77e 100644 --- a/packages/react-schema-renderer/src/__tests__/__snapshots__/markup.spec.tsx.snap +++ b/packages/react-schema-renderer/src/__tests__/__snapshots__/markup.spec.tsx.snap @@ -44,6 +44,7 @@ Object { "pristine": false, "props": Object { "default": "123", + "name": "aa", "type": "string", "x-props": Object { "data-testid": "input", @@ -101,6 +102,7 @@ Object { "name": "NO_NAME_FIELD_$0", "path": "NO_NAME_FIELD_$0", "props": Object { + "name": "NO_NAME_FIELD_$0", "type": "object", "x-component": "card", "x-component-props": Object { @@ -133,6 +135,7 @@ Object { "pristine": false, "props": Object { "default": "123", + "name": "aa", "type": "string", "x-props": Object { "data-testid": "input", @@ -162,9 +165,11 @@ exports[`test all apis markup virtualbox 2`] = ` Object { "properties": Object { "NO_NAME_FIELD_$0": Object { + "name": "NO_NAME_FIELD_$0", "properties": Object { "aa": Object { "default": "123", + "name": "aa", "type": "string", "x-props": Object { "data-testid": "input", diff --git a/packages/react-schema-renderer/src/__tests__/__snapshots__/register.spec.tsx.snap b/packages/react-schema-renderer/src/__tests__/__snapshots__/register.spec.tsx.snap index 4ee8f1fa33c..e1f3ba47e2d 100644 --- a/packages/react-schema-renderer/src/__tests__/__snapshots__/register.spec.tsx.snap +++ b/packages/react-schema-renderer/src/__tests__/__snapshots__/register.spec.tsx.snap @@ -44,6 +44,7 @@ Object { "pristine": false, "props": Object { "default": "123", + "name": "aa", "type": "string", "x-props": Object { "data-testid": "input", @@ -113,6 +114,7 @@ Object { "pristine": false, "props": Object { "default": "123", + "name": "aa", "type": "string", "x-props": Object { "data-testid": "input", @@ -170,6 +172,7 @@ Object { "name": "cc", "path": "cc", "props": Object { + "name": "cc", "type": "object", "x-component": "box", }, @@ -196,6 +199,7 @@ Object { "pristine": false, "props": Object { "default": "123", + "name": "aa", "type": "string", "x-props": Object { "data-testid": "input", diff --git a/packages/react-schema-renderer/src/shared/schema.ts b/packages/react-schema-renderer/src/shared/schema.ts index 435a017dcbe..4ae8f48b3d8 100644 --- a/packages/react-schema-renderer/src/shared/schema.ts +++ b/packages/react-schema-renderer/src/shared/schema.ts @@ -82,10 +82,15 @@ export class Schema implements ISchema { public _isJSONSchemaObject = true - constructor(json: ISchema, parent?: Schema) { + public name?: string + + constructor(json: ISchema, parent?: Schema, name?: string) { if (parent) { this.parent = parent } + if (name) { + this.name = name + } return this.fromJSON(json) as any } /** @@ -308,7 +313,7 @@ export class Schema implements ISchema { */ setProperty(key: string, schema: ISchema) { this.properties = this.properties || {} - this.properties[key] = new Schema(schema, this) + this.properties[key] = new Schema(schema, this, key) return this.properties[key] } setProperties(properties: SchemaProperties) { @@ -358,15 +363,15 @@ export class Schema implements ISchema { this['x-component'] = lowercase(json['x-component']) } if (!isEmpty(json.properties)) { - this.properties = map(json.properties, item => { - return new Schema(item, this) + this.properties = map(json.properties, (item, key) => { + return new Schema(item, this, key) }) if (isValid(json.additionalProperties)) { this.additionalProperties = new Schema(json.additionalProperties, this) } if (isValid(json.patternProperties)) { - this.patternProperties = map(json.patternProperties, item => { - return new Schema(item, this) + this.patternProperties = map(json.patternProperties, (item, key) => { + return new Schema(item, this, key) }) } } else if (!isEmpty(json.items)) { From 43c30a6fcce522335d2ce4ad6136e645aa861161 Mon Sep 17 00:00:00 2001 From: Janry Date: Mon, 25 Nov 2019 11:32:39 +0800 Subject: [PATCH 71/99] Improve perf (#426) * perf(@uform/core): improve visible/display toggle perf * fix(@uform/core): fix test --- .../__snapshots__/index.spec.ts.snap | 4 +-- packages/core/src/index.ts | 34 ++++++------------- packages/react/src/hooks/useField.ts | 2 +- packages/react/src/hooks/useVirtualField.ts | 6 ++-- 4 files changed, 16 insertions(+), 30 deletions(-) diff --git a/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap b/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap index fa3c5b7f29b..c550b2159fa 100644 --- a/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap +++ b/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap @@ -927,7 +927,7 @@ Object { "values": Array [ 123, ], - "visible": false, + "visible": true, "visited": false, "warnings": Array [], }, @@ -1215,7 +1215,7 @@ Object { "values": Array [ 123, ], - "visible": false, + "visible": true, "visited": false, "warnings": Array [], }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 28a524dea63..01526403196 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -195,7 +195,7 @@ export function createForm( if (displayChanged) { state.display = published.display } - }) + }, true) }, false ) @@ -239,7 +239,6 @@ export function createForm( return (published: IVirtualFieldState) => { const visibleChanged = field.isDirty('visible') const displayChanged = field.isDirty('display') - const unmountedChanged = field.isDirty('unmounted') const mountedChanged = field.isDirty('mounted') const initializedChnaged = field.isDirty('initialized') @@ -247,35 +246,22 @@ export function createForm( heart.publish(LifeCycleTypes.ON_FIELD_INIT, field) } - if (visibleChanged) { - graph.eachChildren(path, childState => { - childState.setState( - (state: IVirtualFieldState) => { - state.visible = published.visible - } - ) - }) - } - - if (displayChanged) { + if (visibleChanged || displayChanged) { graph.eachChildren(path, childState => { childState.setState( (state: IVirtualFieldState) => { - state.display = published.display - } + if (visibleChanged) { + state.visible = published.visible + } + if (displayChanged) { + state.display = published.display + } + }, + true ) }) } - if (unmountedChanged) { - graph.eachChildren(path, childState => { - childState.setState( - (state: IVirtualFieldState) => { - state.unmounted = published.unmounted - } - ) - }) - } if (mountedChanged && published.mounted) { heart.publish(LifeCycleTypes.ON_FIELD_MOUNT, field) } diff --git a/packages/react/src/hooks/useField.ts b/packages/react/src/hooks/useField.ts index 96ac92c4b43..fbf8c47c155 100644 --- a/packages/react/src/hooks/useField.ts +++ b/packages/react/src/hooks/useField.ts @@ -94,7 +94,7 @@ export const useField = (options: IFieldStateUIProps): IFieldHook => { ref.current.field.unsubscribe(ref.current.subscriberId) ref.current.field.setState((state: IFieldState) => { state.unmounted = true - }) + }, true) } }, []) diff --git a/packages/react/src/hooks/useVirtualField.ts b/packages/react/src/hooks/useVirtualField.ts index 21da603a92f..3f4adace206 100644 --- a/packages/react/src/hooks/useVirtualField.ts +++ b/packages/react/src/hooks/useVirtualField.ts @@ -47,16 +47,16 @@ export const useVirtualField = ( }) useEffect(() => { - ref.current.field.unsafe_setSourceState(state => { + ref.current.field.setState(state => { state.mounted = true - }) + }, true) ref.current.unmounted = false return () => { ref.current.unmounted = true ref.current.field.unsubscribe() ref.current.field.setState((state: IVirtualFieldState) => { state.unmounted = true - }) + }, true) } }, []) From 476d4cb97f78e766700061865e617e2845be2667 Mon Sep 17 00:00:00 2001 From: janrywang Date: Mon, 25 Nov 2019 11:35:57 +0800 Subject: [PATCH 72/99] chore(publish): v1.0.0-alpha.2 --- packages/antd/package.json | 8 ++++---- packages/core/package.json | 6 +++--- packages/next/package.json | 8 ++++---- packages/printer/package.json | 4 ++-- packages/react-schema-renderer/package.json | 8 ++++---- packages/react-shared-components/package.json | 4 ++-- packages/react/package.json | 6 +++--- packages/shared/package.json | 2 +- packages/validator/package.json | 4 ++-- 9 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/antd/package.json b/packages/antd/package.json index 89da30cea81..7577bab83fb 100644 --- a/packages/antd/package.json +++ b/packages/antd/package.json @@ -1,6 +1,6 @@ { "name": "@uform/antd", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "license": "MIT", "main": "lib", "module": "esm", @@ -30,9 +30,9 @@ "react-dom": ">=16.8.0" }, "dependencies": { - "@uform/react-schema-renderer": "^1.0.0-alpha.1", - "@uform/react-shared-components": "^1.0.0-alpha.1", - "@uform/shared": "^1.0.0-alpha.1", + "@uform/react-schema-renderer": "^1.0.0-alpha.2", + "@uform/react-shared-components": "^1.0.0-alpha.2", + "@uform/shared": "^1.0.0-alpha.2", "@uform/types": "^0.4.0", "classnames": "^2.2.6", "moveto": "^1.7.4", diff --git a/packages/core/package.json b/packages/core/package.json index bc036376cee..09b2e8ffd67 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@uform/core", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "license": "MIT", "main": "lib", "repository": { @@ -26,9 +26,9 @@ "scheduler": ">=0.11.2" }, "dependencies": { - "@uform/shared": "^1.0.0-alpha.1", + "@uform/shared": "^1.0.0-alpha.2", "@uform/types": "^0.4.0", - "@uform/validator": "^1.0.0-alpha.1", + "@uform/validator": "^1.0.0-alpha.2", "immer": "^3.2.0" }, "publishConfig": { diff --git a/packages/next/package.json b/packages/next/package.json index 07796e683d3..764a06cf420 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@uform/next", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "license": "MIT", "main": "lib", "module": "esm", @@ -28,9 +28,9 @@ "react-dom": ">=16.8.0" }, "dependencies": { - "@uform/react-schema-renderer": "^1.0.0-alpha.1", - "@uform/react-shared-components": "^1.0.0-alpha.1", - "@uform/shared": "^1.0.0-alpha.1", + "@uform/react-schema-renderer": "^1.0.0-alpha.2", + "@uform/react-shared-components": "^1.0.0-alpha.2", + "@uform/shared": "^1.0.0-alpha.2", "@uform/types": "^0.4.0", "classnames": "^2.2.6", "moveto": "^1.7.4", diff --git a/packages/printer/package.json b/packages/printer/package.json index 23f79b3c6eb..b449289db8a 100644 --- a/packages/printer/package.json +++ b/packages/printer/package.json @@ -1,6 +1,6 @@ { "name": "@uform/printer", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "license": "MIT", "main": "lib", "repository": { @@ -26,7 +26,7 @@ "typescript": "^3.5.2" }, "dependencies": { - "@uform/react-schema-renderer": "^1.0.0-alpha.1", + "@uform/react-schema-renderer": "^1.0.0-alpha.2", "react-modal": "^3.8.1", "styled-components": "^4.1.1" }, diff --git a/packages/react-schema-renderer/package.json b/packages/react-schema-renderer/package.json index afb32300685..d685e0d3cf8 100644 --- a/packages/react-schema-renderer/package.json +++ b/packages/react-schema-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@uform/react-schema-renderer", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "license": "MIT", "main": "lib", "repository": { @@ -30,9 +30,9 @@ "react-eva": "^1.1.7" }, "dependencies": { - "@uform/react": "^1.0.0-alpha.1", - "@uform/shared": "^1.0.0-alpha.1", - "@uform/validator": "^1.0.0-alpha.1", + "@uform/react": "^1.0.0-alpha.2", + "@uform/shared": "^1.0.0-alpha.2", + "@uform/validator": "^1.0.0-alpha.2", "pascal-case": "^2.0.1" }, "publishConfig": { diff --git a/packages/react-shared-components/package.json b/packages/react-shared-components/package.json index 91953c4ba66..b4839807dd3 100644 --- a/packages/react-shared-components/package.json +++ b/packages/react-shared-components/package.json @@ -1,6 +1,6 @@ { "name": "@uform/react-shared-components", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "license": "MIT", "main": "lib", "types": "lib/index.d.ts", @@ -29,7 +29,7 @@ "@babel/runtime": "^7.4.4" }, "dependencies": { - "@uform/shared": "^1.0.0-alpha.1", + "@uform/shared": "^1.0.0-alpha.2", "@uform/types": "^0.4.0" } } diff --git a/packages/react/package.json b/packages/react/package.json index c5af4d58774..6b670869448 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@uform/react", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "license": "MIT", "main": "lib", "repository": { @@ -31,8 +31,8 @@ "scheduler": ">=0.11.2" }, "dependencies": { - "@uform/core": "^1.0.0-alpha.1", - "@uform/shared": "^1.0.0-alpha.1" + "@uform/core": "^1.0.0-alpha.2", + "@uform/shared": "^1.0.0-alpha.2" }, "publishConfig": { "access": "public" diff --git a/packages/shared/package.json b/packages/shared/package.json index 7e7c4972e5d..d712418a99d 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@uform/shared", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "license": "MIT", "main": "lib", "types": "lib/index.d.ts", diff --git a/packages/validator/package.json b/packages/validator/package.json index 35be4f21dc0..e48bc188168 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -1,6 +1,6 @@ { "name": "@uform/validator", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "license": "MIT", "main": "lib", "repository": { @@ -25,7 +25,7 @@ "@babel/runtime": "^7.4.4" }, "dependencies": { - "@uform/shared": "^1.0.0-alpha.1", + "@uform/shared": "^1.0.0-alpha.2", "@uform/types": "^0.4.0" }, "publishConfig": { From b446c028af8552ebd0466564479d270945b040a5 Mon Sep 17 00:00:00 2001 From: Janry Date: Mon, 25 Nov 2019 21:59:48 +0800 Subject: [PATCH 73/99] feat(@uform/antd/next): improve form step (#431) --- packages/antd/src/components/FormStep.tsx | 16 ++++++++++++---- packages/core/src/types.ts | 3 +++ packages/next/src/components/FormStep.tsx | 18 +++++++++++++----- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/antd/src/components/FormStep.tsx b/packages/antd/src/components/FormStep.tsx index 2a960efe34a..e38fe19a5f2 100644 --- a/packages/antd/src/components/FormStep.tsx +++ b/packages/antd/src/components/FormStep.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useRef, Fragment } from 'react' +import React, { useMemo, useRef, Fragment } from 'react' import { createControllerBox, ISchemaVirtualFieldComponentProps, @@ -30,8 +30,13 @@ type StepComponentExtendsProps = StateMap export const FormStep: React.FC & StepComponentExtendsProps = createControllerBox( 'step', - ({ form, schema, children }: ISchemaVirtualFieldComponentProps) => { - const [current, setCurrent] = useState(0) + ({ + form, + path, + schema, + current, + children + }: ISchemaVirtualFieldComponentProps) => { const ref = useRef(current) const { dataSource, ...stepProps } = schema.getExtendsComponentProps() const items = toArr(dataSource) @@ -40,8 +45,11 @@ export const FormStep: React.FC & value: cur, preValue: current }) - setCurrent(cur) + form.setFieldState(path, state => { + state.current = cur + }) } + current = current || 0 useFormEffects(({ setFieldState }) => { items.forEach(({ name }, index) => { setFieldState(name, (state: any) => { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index ec8ba9692d1..f8fefb2a445 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -146,6 +146,7 @@ export interface IFieldState { mounted: boolean unmounted: boolean props: FieldProps + [key: string]: any } export type FieldStateDirtyMap = StateDirtyMap @@ -209,6 +210,7 @@ export interface IFormState { mounted: boolean unmounted: boolean props: FormProps + [key: string]: any } export type FormStateDirtyMap = StateDirtyMap @@ -239,6 +241,7 @@ export interface IVirtualFieldState { mounted: boolean unmounted: boolean props: FieldProps + [key: string]: any } export type VirtualFieldStateDirtyMap = StateDirtyMap diff --git a/packages/next/src/components/FormStep.tsx b/packages/next/src/components/FormStep.tsx index d40175409e1..b98926f068a 100644 --- a/packages/next/src/components/FormStep.tsx +++ b/packages/next/src/components/FormStep.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useRef, Fragment } from 'react' +import React, { useMemo, useRef, Fragment } from 'react' import { createControllerBox, ISchemaVirtualFieldComponentProps, @@ -30,8 +30,13 @@ type StepComponentExtendsProps = StateMap export const FormStep: React.FC & StepComponentExtendsProps = createControllerBox( 'step', - ({ props, form, children }: ISchemaVirtualFieldComponentProps) => { - const [current, setCurrent] = useState(0) + ({ + props, + form, + path, + current, + children + }: ISchemaVirtualFieldComponentProps) => { const ref = useRef(current) const { dataSource, ...stepProps } = props['x-component-props'] || {} const items = toArr(dataSource) @@ -40,8 +45,11 @@ export const FormStep: React.FC & value: cur, preValue: current }) - setCurrent(cur) + form.setFieldState(path, state => { + state.current = cur + }) } + current = current || 0 useFormEffects(({ setFieldState }) => { items.forEach(({ name }, index) => { setFieldState(name, (state: any) => { @@ -52,7 +60,7 @@ export const FormStep: React.FC & items.forEach(({ name }, index) => { if (!name) throw new Error('FormStep dataSource must include `name` property') - + setFieldState(name, (state: any) => { state.display = index === value }) From 9440ac4511cf6c964edb5044f68d716798b9582f Mon Sep 17 00:00:00 2001 From: Janry Date: Tue, 26 Nov 2019 11:59:59 +0800 Subject: [PATCH 74/99] feat(@uform/react): support useFieldState/useFormState (#433) * feat(@uform/antd/next): improve form step * feat(@uform/react): support useFieldState/useFormState --- docs/Examples/antd/Layout.md | 16 ++++ docs/Examples/next/Layout.md | 81 ++++++++++++++----- packages/antd/src/components/FormStep.tsx | 66 +++++++-------- packages/next/src/components/FormStep.tsx | 71 +++++++--------- packages/react/src/components/Field.tsx | 26 ++++-- .../react/src/components/VirtualField.tsx | 27 ++++--- packages/react/src/context.ts | 3 + packages/react/src/hooks/useField.ts | 1 + packages/react/src/hooks/useFieldState.ts | 24 ++++++ packages/react/src/hooks/useFormState.ts | 36 +++++++++ packages/react/src/hooks/useVirtualField.ts | 1 + packages/react/src/index.ts | 2 + packages/react/src/types.ts | 6 +- 13 files changed, 240 insertions(+), 120 deletions(-) create mode 100644 packages/react/src/hooks/useFieldState.ts create mode 100644 packages/react/src/hooks/useFormState.ts diff --git a/docs/Examples/antd/Layout.md b/docs/Examples/antd/Layout.md index 0fff0803570..659b47a9b02 100644 --- a/docs/Examples/antd/Layout.md +++ b/docs/Examples/antd/Layout.md @@ -297,6 +297,8 @@ const { onFormInit$ } = FormEffectHooks const actions = createFormActions() +let cache = {} + export default () => ( { @@ -340,6 +342,20 @@ export default () => ( + +
    ) diff --git a/docs/Examples/next/Layout.md b/docs/Examples/next/Layout.md index 6a7aef7ccac..864d0612f22 100644 --- a/docs/Examples/next/Layout.md +++ b/docs/Examples/next/Layout.md @@ -171,33 +171,55 @@ const App = () => { - - ​ - ​​ - - - - ​ - ​​ - - + + ​ + ​​ + + + + ​ + ​​ + + - - - - + + + + - ​ - + ​提交 @@ -258,7 +280,6 @@ ReactDOM.render(, document.getElementById('root')) ## 分步表单 - ```jsx import { SchemaForm, @@ -284,6 +305,8 @@ const { onFormInit$ } = FormEffectHooks const actions = createFormActions() +let cache = {} + export default () => ( { @@ -305,9 +328,9 @@ export default () => ( @@ -327,6 +350,20 @@ export default () => ( + +
    ) diff --git a/packages/antd/src/components/FormStep.tsx b/packages/antd/src/components/FormStep.tsx index e38fe19a5f2..e1c5f058af7 100644 --- a/packages/antd/src/components/FormStep.tsx +++ b/packages/antd/src/components/FormStep.tsx @@ -3,7 +3,8 @@ import { createControllerBox, ISchemaVirtualFieldComponentProps, createEffectHook, - useFormEffects + useFormEffects, + useFieldState } from '@uform/react-schema-renderer' import { toArr } from '@uform/shared' import { Steps } from 'antd' @@ -30,13 +31,10 @@ type StepComponentExtendsProps = StateMap export const FormStep: React.FC & StepComponentExtendsProps = createControllerBox( 'step', - ({ - form, - path, - schema, - current, - children - }: ISchemaVirtualFieldComponentProps) => { + ({ form, schema, children }: ISchemaVirtualFieldComponentProps) => { + const [{ current }, setFieldState] = useFieldState({ + current: 0 + }) const ref = useRef(current) const { dataSource, ...stepProps } = schema.getExtendsComponentProps() const items = toArr(dataSource) @@ -45,18 +43,17 @@ export const FormStep: React.FC & value: cur, preValue: current }) - form.setFieldState(path, state => { - state.current = cur + setFieldState({ + current: cur }) } - current = current || 0 - useFormEffects(({ setFieldState }) => { + useFormEffects(($, { setFieldState }) => { items.forEach(({ name }, index) => { setFieldState(name, (state: any) => { state.display = index === current }) }) - EffectHooks.onStepCurrentChange$().subscribe(({ value }) => { + $(StateMap.ON_FORM_STEP_CURRENT_CHANGE).subscribe(({ value }) => { items.forEach(({ name }, index) => { if (!name) throw new Error('FormStep dataSource must include `name` property') @@ -65,34 +62,27 @@ export const FormStep: React.FC & }) }) }) - }) - useMemo(() => { - update(ref.current) - form.subscribe(({ type, payload }) => { - switch (type) { - case StateMap.ON_FORM_STEP_NEXT: - form.validate().then(({ errors }) => { - if (errors.length === 0) { - update( - ref.current + 1 > items.length - 1 - ? ref.current - : ref.current + 1 - ) - } - }) - break - case StateMap.ON_FORM_STEP_PREVIOUS: - update(ref.current - 1 < 0 ? ref.current : ref.current - 1) - break - case StateMap.ON_FORM_STEP_GO_TO: - if (!(payload < 0 || payload > items.length)) { - update(payload) - } - break + $(StateMap.ON_FORM_STEP_NEXT).subscribe(() => { + form.validate().then(({ errors }) => { + if (errors.length === 0) { + update( + ref.current + 1 > items.length - 1 ? ref.current : ref.current + 1 + ) + } + }) + }) + + $(StateMap.ON_FORM_STEP_PREVIOUS).subscribe(() => { + update(ref.current - 1 < 0 ? ref.current : ref.current - 1) + }) + + $(StateMap.ON_FORM_STEP_GO_TO).subscribe(payload => { + if (!(payload < 0 || payload > items.length)) { + update(payload) } }) - }, []) + }) ref.current = current return ( diff --git a/packages/next/src/components/FormStep.tsx b/packages/next/src/components/FormStep.tsx index b98926f068a..31958ddbe3b 100644 --- a/packages/next/src/components/FormStep.tsx +++ b/packages/next/src/components/FormStep.tsx @@ -1,9 +1,10 @@ -import React, { useMemo, useRef, Fragment } from 'react' +import React, { useRef, Fragment } from 'react' import { createControllerBox, ISchemaVirtualFieldComponentProps, createEffectHook, - useFormEffects + useFormEffects, + useFieldState } from '@uform/react-schema-renderer' import { toArr } from '@uform/shared' import { Step } from '@alifd/next' @@ -30,70 +31,58 @@ type StepComponentExtendsProps = StateMap export const FormStep: React.FC & StepComponentExtendsProps = createControllerBox( 'step', - ({ - props, - form, - path, - current, - children - }: ISchemaVirtualFieldComponentProps) => { + ({ form, schema, children }: ISchemaVirtualFieldComponentProps) => { + const [{ current }, setFieldState] = useFieldState({ + current: 0 + }) const ref = useRef(current) - const { dataSource, ...stepProps } = props['x-component-props'] || {} + const { dataSource, ...stepProps } = schema.getExtendsComponentProps() const items = toArr(dataSource) const update = (cur: number) => { form.notify(StateMap.ON_FORM_STEP_CURRENT_CHANGE, { value: cur, preValue: current }) - form.setFieldState(path, state => { - state.current = cur + setFieldState({ + current: cur }) } - current = current || 0 - useFormEffects(({ setFieldState }) => { + useFormEffects(($, { setFieldState }) => { items.forEach(({ name }, index) => { setFieldState(name, (state: any) => { state.display = index === current }) }) - EffectHooks.onStepCurrentChange$().subscribe(({ value }) => { + $(StateMap.ON_FORM_STEP_CURRENT_CHANGE).subscribe(({ value }) => { items.forEach(({ name }, index) => { if (!name) throw new Error('FormStep dataSource must include `name` property') - setFieldState(name, (state: any) => { state.display = index === value }) }) }) - }) - useMemo(() => { - update(ref.current) - form.subscribe(({ type, payload }) => { - switch (type) { - case StateMap.ON_FORM_STEP_NEXT: - form.validate().then(({ errors }) => { - if (errors.length === 0) { - update( - ref.current + 1 > items.length - 1 - ? ref.current - : ref.current + 1 - ) - } - }) - break - case StateMap.ON_FORM_STEP_PREVIOUS: - update(ref.current - 1 < 0 ? ref.current : ref.current - 1) - break - case StateMap.ON_FORM_STEP_GO_TO: - if (!(payload < 0 || payload > items.length)) { - update(payload) - } - break + $(StateMap.ON_FORM_STEP_NEXT).subscribe(() => { + form.validate().then(({ errors }) => { + if (errors.length === 0) { + update( + ref.current + 1 > items.length - 1 ? ref.current : ref.current + 1 + ) + } + }) + }) + + $(StateMap.ON_FORM_STEP_PREVIOUS).subscribe(() => { + update(ref.current - 1 < 0 ? ref.current : ref.current - 1) + }) + + $(StateMap.ON_FORM_STEP_GO_TO).subscribe(payload => { + if (!(payload < 0 || payload > items.length)) { + update(payload) } }) - }, []) + }) ref.current = current return ( diff --git a/packages/react/src/components/Field.tsx b/packages/react/src/components/Field.tsx index d5f75d29b95..39b73dd035c 100644 --- a/packages/react/src/components/Field.tsx +++ b/packages/react/src/components/Field.tsx @@ -2,19 +2,29 @@ import React from 'react' import { useField } from '../hooks/useField' import { isFn } from '@uform/shared' import { IFieldStateUIProps } from '../types' +import { FieldContext } from '../context' export const Field: React.FC = props => { - const { state, props: innerProps, mutators, form } = useField(props) + const { state, field, props: innerProps, mutators, form } = useField(props) + if (!state.visible || !state.display) return if (isFn(props.children)) { - return props.children({ - form, - state, - props: innerProps, - mutators - }) + return ( + + {props.children({ + form, + state, + props: innerProps, + mutators + })} + + ) } else { - return {props.children} + return ( + + {props.children} + + ) } } diff --git a/packages/react/src/components/VirtualField.tsx b/packages/react/src/components/VirtualField.tsx index 84bacbdb4fe..ff46e98a91a 100644 --- a/packages/react/src/components/VirtualField.tsx +++ b/packages/react/src/components/VirtualField.tsx @@ -2,20 +2,27 @@ import React from 'react' import { useVirtualField } from '../hooks/useVirtualField' import { isFn } from '@uform/shared' import { IVirtualFieldProps } from '../types' +import { FieldContext } from '../context' -export const VirtualField: React.FunctionComponent< - IVirtualFieldProps -> = props => { - const { state, props: innerProps, form } = useVirtualField(props) +export const VirtualField: React.FunctionComponent = props => { + const { state, field, props: innerProps, form } = useVirtualField(props) if (!state.visible || !state.display) return if (isFn(props.children)) { - return props.children({ - form, - state, - props: innerProps - }) + return ( + + {props.children({ + form, + state, + props: innerProps + })} + + ) } else { - return props.children + return ( + + {props.children} + + ) } } diff --git a/packages/react/src/context.ts b/packages/react/src/context.ts index a90a7c3f514..3200d372c11 100644 --- a/packages/react/src/context.ts +++ b/packages/react/src/context.ts @@ -1,7 +1,10 @@ import { createContext } from 'react' import { Broadcast } from './shared' import { IForm } from '@uform/core' +import { IField, IVirtualField } from '@uform/core' export const BroadcastContext = createContext(null) +export const FieldContext = createContext(null) + export default createContext(null) diff --git a/packages/react/src/hooks/useField.ts b/packages/react/src/hooks/useField.ts index fbf8c47c155..861dc2e4a66 100644 --- a/packages/react/src/hooks/useField.ts +++ b/packages/react/src/hooks/useField.ts @@ -101,6 +101,7 @@ export const useField = (options: IFieldStateUIProps): IFieldHook => { const state = ref.current.field.getState() return { form, + field: ref.current.field, state: { ...state, errors: state.errors.join(', ') diff --git a/packages/react/src/hooks/useFieldState.ts b/packages/react/src/hooks/useFieldState.ts new file mode 100644 index 00000000000..e562614f8b5 --- /dev/null +++ b/packages/react/src/hooks/useFieldState.ts @@ -0,0 +1,24 @@ +import { useContext, useMemo, useRef } from 'react' +import { IFieldState } from '@uform/core' +import { FieldContext } from '../context' + +export const useFieldState = (defaultState: T) => { + const ref = useRef() + const field = useContext(FieldContext) + useMemo(() => { + field.unsafe_setSourceState(state => { + Object.assign(state, defaultState) + }) + }, []) + + ref.current = field.getState() + return [ + ref.current, + (nextState?: {}) => { + if (!nextState) return + field.setState(state => { + Object.assign(state, nextState) + }) + } + ] +} diff --git a/packages/react/src/hooks/useFormState.ts b/packages/react/src/hooks/useFormState.ts new file mode 100644 index 00000000000..3d246aa1bb1 --- /dev/null +++ b/packages/react/src/hooks/useFormState.ts @@ -0,0 +1,36 @@ +import { useContext, useEffect, useMemo, useRef } from 'react' +import { LifeCycleTypes, IFormState } from '@uform/core' +import { useForceUpdate } from './useForceUpdate' +import FormContext from '../context' + +export const useFormState = (defaultState: T) => { + const forceUpdate = useForceUpdate() + const ref = useRef<{ state: IFormState; subscribeId: number }>() + const form = useContext(FormContext) + ref.current.subscribeId = useMemo(() => { + form.setFormState(state => { + Object.assign(state, defaultState) + }, true) + return form.subscribe(({ type }) => { + if (type === LifeCycleTypes.ON_FORM_CHANGE) { + forceUpdate() + } + }) + }, []) + + ref.current.state = form.getFormState() + useEffect(() => { + return () => { + form.unsubscribe(ref.current.subscribeId) + } + }, []) + return [ + ref.current.state, + (nextState?: {}) => { + if (!nextState) return + form.setFormState(state => { + Object.assign(state, nextState) + }) + } + ] +} diff --git a/packages/react/src/hooks/useVirtualField.ts b/packages/react/src/hooks/useVirtualField.ts index 3f4adace206..964ae11c8fc 100644 --- a/packages/react/src/hooks/useVirtualField.ts +++ b/packages/react/src/hooks/useVirtualField.ts @@ -63,6 +63,7 @@ export const useVirtualField = ( const state = ref.current.field.getState() return { state, + field: ref.current.field, form, props: state.props } diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index df70b023a5d..17b08c39c73 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -11,6 +11,8 @@ export * from './components/VirtualField' export * from './components/FormSpy' export * from './components/FormProvider' export * from './components/FormConsumer' +export * from './hooks/useFieldState' +export * from './hooks/useFormState' export * from './hooks/useForm' export * from './hooks/useField' export * from './hooks/useVirtualField' diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index a69d2c106e6..186067331f2 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -10,7 +10,9 @@ import { IFormResetOptions, IFormSubmitResult, FormHeartSubscriber, - IFormGraph + IFormGraph, + IField, + IVirtualField } from '@uform/core' import { FormPathPattern } from '@uform/shared' import { Observable } from 'rxjs/internal/Observable' @@ -112,6 +114,7 @@ export interface IFormConsumerProps { export interface IFieldHook { form: IForm + field: IField state: IFieldState props: {} mutators: IMutators @@ -119,6 +122,7 @@ export interface IFieldHook { export interface IVirtualFieldHook { form: IForm + field: IVirtualField state: IFieldState props: {} } From 551d74c11ee51e5b74755ceb30fd0056eed82079 Mon Sep 17 00:00:00 2001 From: Janry Date: Tue, 26 Nov 2019 20:39:10 +0800 Subject: [PATCH 75/99] feat(@uform/react): actions support clearErrors (#434) --- .../src/PreviewText.tsx | 49 +++-- .../react/src/__tests__/useField.spec.tsx | 171 +++++++++++------- packages/react/src/hooks/useField.ts | 11 +- packages/react/src/shared.ts | 2 + 4 files changed, 156 insertions(+), 77 deletions(-) diff --git a/packages/react-shared-components/src/PreviewText.tsx b/packages/react-shared-components/src/PreviewText.tsx index 63e0c44f58d..2566bf73931 100644 --- a/packages/react-shared-components/src/PreviewText.tsx +++ b/packages/react-shared-components/src/PreviewText.tsx @@ -1,5 +1,5 @@ import React, { useContext, createContext } from 'react' -import { isFn } from '@uform/shared' +import { isFn, isEqual } from '@uform/shared' import { IPreviewTextProps } from './types' export interface PreviewTextConfigProps { @@ -14,20 +14,41 @@ export const PreviewText: React.FC & { const context = useContext(PreviewTextContext) || {} let value: any if (props.dataSource && props.dataSource.length) { - let find = props.dataSource.filter(({ value }) => - Array.isArray(props.value) - ? props.value.some(val => val == value) - : props.value == value - ) - value = find.reduce((buf, item, index) => { - return buf.concat(item.label, index < find.length - 1 ? ', ' : '') - }, []) + if (Array.isArray(props.value)) { + value = props.value.map((val, index) => { + const finded = props.dataSource.find(item => isEqual(item.value, val)) + if (finded) { + return ( + + {finded.label} + {index < props.value.length - 1 ? ' ,' : ''} + + ) + } + }) + } else { + const fined = props.dataSource.find(item => + isEqual(item.value, props.value) + ) + if (fined) { + value = fined.label + } + } } else { - value = Array.isArray(props.value) - ? props.value.join(' ~ ') - : String( - props.value === undefined || props.value === null ? '' : props.value + if (Array.isArray(props.value)) { + value = props.value.map((val, index) => { + return ( + + {val} + {index < props.value.length - 1 ? '~' : ''} + ) + }) + } else { + value = String( + props.value === undefined || props.value === null ? '' : props.value + ) + } } const placeholder = isFn(context.previewPlaceholder) ? context.previewPlaceholder(props) @@ -44,7 +65,7 @@ export const PreviewText: React.FC & { value === undefined || (Array.isArray(value) && value.length === 0) ? placeholder || 'N/A' - : String(value)} + : value} {props.addonTextAfter ? ' ' + props.addonTextAfter : ''} {props.innerAfter ? ' ' + props.innerAfter : ''} {props.addonAfter ? ' ' + props.addonAfter : ''} diff --git a/packages/react/src/__tests__/useField.spec.tsx b/packages/react/src/__tests__/useField.spec.tsx index dafa93cb017..f3559694814 100644 --- a/packages/react/src/__tests__/useField.spec.tsx +++ b/packages/react/src/__tests__/useField.spec.tsx @@ -1,24 +1,25 @@ import React from 'react' import { act, renderHook } from '@testing-library/react-hooks' -import FormContext from '../context'; +import { render } from '@testing-library/react' +import FormContext from '../context' import useForm from '../hooks/useForm' import useField from '../hooks/useField' -import { createForm } from '..'; -import { FormLifeCycle, LifeCycleTypes } from '@uform/core'; +import { createForm } from '..' +import { FormLifeCycle, LifeCycleTypes } from '@uform/core' -describe('useField hook',()=>{ - test('form is required', ()=>{ +describe('useField hook', () => { + test('form is required', () => { expect(() => { useField({}) }).toThrow() }) - test('basic ', ()=>{ + test('basic ', () => { let globalForm let globalGraph const formInstance = createForm({ lifecycles: [ - new FormLifeCycle(LifeCycleTypes.ON_FORM_GRAPH_CHANGE, (graph) => { + new FormLifeCycle(LifeCycleTypes.ON_FORM_GRAPH_CHANGE, graph => { globalGraph = graph }) ] @@ -28,61 +29,74 @@ describe('useField hook',()=>{ form: formInstance }) globalForm = form - return {children} + return ( + {children} + ) } - const { result } = renderHook(() => useField({ name: 'username' }), { wrapper: formWrapper }) + const { result } = renderHook(() => useField({ name: 'username' }), { + wrapper: formWrapper + }) expect(result.current.form).toEqual(globalForm) expect(result.current.state.props).toEqual({}) expect(result.current.state).toEqual({ ...globalGraph.get('username').getState(), - errors: '', - mounted: false, + errors: [], + mounted: false }) }) - test('update', async ()=>{ + test('update', async () => { let globalForm const formWrapper = ({ children }) => { const form = useForm({}) globalForm = form - return {children} + return ( + {children} + ) } - const { result } = renderHook(() => useField({ name: 'username' }), { wrapper: formWrapper }) + const { result } = renderHook(() => useField({ name: 'username' }), { + wrapper: formWrapper + }) expect(result.current.state.value).toEqual(undefined) act(() => { - globalForm.setFormState(state => state.values.username = 'abcd') + globalForm.setFormState(state => (state.values.username = 'abcd')) }) - + expect(result.current.state.value).toEqual('abcd') }) - test('mounted change', async ()=>{ + test('mounted change', async () => { const formWrapper = ({ children }) => { const form = useForm({}) - return {children} + return ( + {children} + ) } - const { result, rerender } = renderHook(() => useField({ name: 'username' }), { wrapper: formWrapper }) + const { result, rerender } = renderHook( + () => useField({ name: 'username' }), + { wrapper: formWrapper } + ) expect(result.current.state.mounted).toEqual(false) rerender() expect(result.current.state.mounted).toEqual(true) }) - test('dirty', async ()=>{ + test('dirty', async () => { const formWrapper = ({ children }) => { const formInstance = createForm({ lifecycles: [ - new FormLifeCycle(LifeCycleTypes.ON_FIELD_CHANGE, (field) => { - - }) + new FormLifeCycle(LifeCycleTypes.ON_FIELD_CHANGE, field => {}) ] }) const form = useForm({ form: formInstance }) - return {children} + return ( + {children} + ) } const initialProps = { @@ -90,41 +104,51 @@ describe('useField hook',()=>{ props: { disabled: true }, required: false, editable: true, - rules: [], + rules: [] } - const { result, rerender } = renderHook(() => useField(initialProps), { wrapper: formWrapper }) + const { result, rerender } = renderHook(() => useField(initialProps), { + wrapper: formWrapper + }) expect(result.current.props).toEqual({ disabled: true }) expect(result.current.state.required).toEqual(false) - expect(result.current.state.editable).toEqual(true) - expect(result.current.state.rules).toEqual([]) - + expect(result.current.state.editable).toEqual(true) + expect(result.current.state.rules).toEqual([]) + initialProps.required = true initialProps.editable = false initialProps.props = { disabled: false } initialProps.rules = [() => ({ type: 'warning', message: 'warning msg' })] - + expect(result.current.props).toEqual({ disabled: true }) expect(result.current.state.required).toEqual(false) - expect(result.current.state.editable).toEqual(true) - expect(result.current.state.rules).toEqual([]) + expect(result.current.state.editable).toEqual(true) + expect(result.current.state.rules).toEqual([]) rerender() expect(result.current.props).toEqual(initialProps.props) expect(result.current.state.required).toEqual(initialProps.required) expect(result.current.state.editable).toEqual(initialProps.editable) - expect(result.current.state.rules).toEqual([...initialProps.rules, { required: true }]) + expect(result.current.state.rules).toEqual([ + ...initialProps.rules, + { required: true } + ]) }) - test('extented mutator', ()=>{ + test('extented mutator', () => { const formWrapper = ({ children }) => { const form = useForm({}) - return {children} + return ( + {children} + ) } - const getValueFromEvent = (e) => (e.target.value) - const eventValue = { target: { value: 'abc' }} - const { result: result1, rerender: rerender1 } = renderHook(() => useField({ name: 'username' }), { wrapper: formWrapper }) + const getValueFromEvent = e => e.target.value + const eventValue = { target: { value: 'abc' } } + const { result: result1, rerender: rerender1 } = renderHook( + () => useField({ name: 'username' }), + { wrapper: formWrapper } + ) expect(result1.current.state.value).toEqual(undefined) act(() => { result1.current.mutators.change(eventValue) @@ -132,7 +156,10 @@ describe('useField hook',()=>{ rerender1() expect(result1.current.state.value).toEqual(eventValue) - const { result: result2, rerender: rerender2 } = renderHook(() => useField({ name: 'username', getValueFromEvent }), { wrapper: formWrapper }) + const { result: result2, rerender: rerender2 } = renderHook( + () => useField({ name: 'username', getValueFromEvent }), + { wrapper: formWrapper } + ) expect(result2.current.state.value).toEqual(undefined) act(() => { result2.current.mutators.change(eventValue) @@ -141,61 +168,83 @@ describe('useField hook',()=>{ expect(result2.current.state.value).toEqual('abc') }) - test('triggerType mutator onChange', async()=>{ + test('triggerType mutator onChange', async () => { const formWrapper = ({ children }) => { const form = useForm({}) - return {children} + return ( + {children} + ) } - const fieldProps = { name: 'username', required: true } - const { result: result1, rerender } = renderHook(() => useField(fieldProps), { wrapper: formWrapper }) - expect(result1.current.state.errors).toEqual('') + const fieldProps = { name: 'username', required: true } + const { result: result1, rerender } = renderHook( + () => useField(fieldProps), + { wrapper: formWrapper } + ) + expect(result1.current.state.errors).toEqual([]) expect(result1.current.state.value).toEqual(undefined) rerender() act(() => { result1.current.mutators.change('') }) - + // await waitForNextUpdate1() expect(result1.current.state.value).toEqual('') - expect(result1.current.state.errors).toEqual('') + expect(result1.current.state.errors).toEqual([]) - const { result: result2, waitForNextUpdate: waitForNextUpdate2 } = renderHook(() => useField({ ...fieldProps, triggerType: 'onChange' }), { wrapper: formWrapper }) - expect(result2.current.state.errors).toEqual('') + const { + result: result2, + waitForNextUpdate: waitForNextUpdate2 + } = renderHook(() => useField({ ...fieldProps, triggerType: 'onChange' }), { + wrapper: formWrapper + }) + expect(result2.current.state.errors).toEqual([]) expect(result2.current.state.value).toEqual(undefined) act(() => { result2.current.mutators.change('') }) - + await waitForNextUpdate2() expect(result2.current.state.value).toEqual('') - expect(result2.current.state.errors).toEqual('This field is required') + const { queryByText } = render(
    {result2.current.state.errors}
    ) + expect(queryByText('This field is required')).toBeVisible() }) - test('triggerType mutator onBlur', async()=>{ + test('triggerType mutator onBlur', async () => { const formWrapper = ({ children }) => { const form = useForm({}) - return {children} + return ( + {children} + ) } - const fieldProps = { name: 'username', required: true } - const { result: result1, rerender } = renderHook(() => useField(fieldProps), { wrapper: formWrapper }) - expect(result1.current.state.errors).toEqual('') + const fieldProps = { name: 'username', required: true } + const { result: result1, rerender } = renderHook( + () => useField(fieldProps), + { wrapper: formWrapper } + ) + expect(result1.current.state.errors).toEqual([]) rerender() act(() => { result1.current.mutators.blur() }) - - expect(result1.current.state.errors).toEqual('') - const { result: result2, waitForNextUpdate: waitForNextUpdate2 } = renderHook(() => useField({ ...fieldProps, triggerType: 'onBlur' }), { wrapper: formWrapper }) - expect(result2.current.state.errors).toEqual('') + expect(result1.current.state.errors).toEqual([]) + + const { + result: result2, + waitForNextUpdate: waitForNextUpdate2 + } = renderHook(() => useField({ ...fieldProps, triggerType: 'onBlur' }), { + wrapper: formWrapper + }) + expect(result2.current.state.errors).toEqual([]) act(() => { result2.current.mutators.blur() }) await waitForNextUpdate2() - expect(result2.current.state.errors).toEqual('This field is required') + const { queryByText } = render(
    {result2.current.state.errors}
    ) + expect(queryByText('This field is required')).toBeVisible() }) -}) \ No newline at end of file +}) diff --git a/packages/react/src/hooks/useField.ts b/packages/react/src/hooks/useField.ts index 861dc2e4a66..7b30d582755 100644 --- a/packages/react/src/hooks/useField.ts +++ b/packages/react/src/hooks/useField.ts @@ -1,4 +1,4 @@ -import { useMemo, useEffect, useRef, useContext } from 'react' +import { createElement, useMemo, useEffect, useRef, useContext } from 'react' import { each, isFn } from '@uform/shared' import { IFieldState, IForm, IField, IMutators } from '@uform/core' import { getValueFromEvent } from '../shared' @@ -104,7 +104,14 @@ export const useField = (options: IFieldStateUIProps): IFieldHook => { field: ref.current.field, state: { ...state, - errors: state.errors.join(', ') + errors: state.errors.map((message, index) => { + return createElement( + 'span', + { key: index }, + message, + state.errors.length - 1 > index ? ' ,' : '' + ) + }) }, mutators, props: state.props diff --git a/packages/react/src/shared.ts b/packages/react/src/shared.ts index 0bde589128a..d024428a1fc 100644 --- a/packages/react/src/shared.ts +++ b/packages/react/src/shared.ts @@ -20,6 +20,7 @@ export const createFormActions = (): IFormActions => { 'reset', 'hasChanged', 'validate', + 'clearErrors', 'setFormState', 'getFormState', 'setFieldState', @@ -42,6 +43,7 @@ export const createAsyncFormActions = (): IFormAsyncActions => 'submit', 'reset', 'hasChanged', + 'clearErrors', 'validate', 'setFormState', 'getFormState', From 19e26277ec2cba4b6935904c148f34ee7bc98add Mon Sep 17 00:00:00 2001 From: Janry Date: Tue, 26 Nov 2019 22:16:20 +0800 Subject: [PATCH 76/99] fix(@uform/antd): Fix deps (#435) --- packages/antd/package.json | 4 ++-- packages/next/package.json | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/antd/package.json b/packages/antd/package.json index 7577bab83fb..1e15b216d89 100644 --- a/packages/antd/package.json +++ b/packages/antd/package.json @@ -33,10 +33,10 @@ "@uform/react-schema-renderer": "^1.0.0-alpha.2", "@uform/react-shared-components": "^1.0.0-alpha.2", "@uform/shared": "^1.0.0-alpha.2", - "@uform/types": "^0.4.0", "classnames": "^2.2.6", - "moveto": "^1.7.4", "react-stikky": "^0.1.15", + "react-eva": "^1.0.0-alpha.0", + "rxjs": "^6.5.1", "styled-components": "^4.1.1" }, "publishConfig": { diff --git a/packages/next/package.json b/packages/next/package.json index 764a06cf420..12d583ea15f 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -31,9 +31,7 @@ "@uform/react-schema-renderer": "^1.0.0-alpha.2", "@uform/react-shared-components": "^1.0.0-alpha.2", "@uform/shared": "^1.0.0-alpha.2", - "@uform/types": "^0.4.0", "classnames": "^2.2.6", - "moveto": "^1.7.4", "react-eva": "^1.0.0-alpha.0", "react-stikky": "^0.1.15", "rxjs": "^6.5.1", From e2aa55ea2759c7939ead006bb8b5ba2c85c264eb Mon Sep 17 00:00:00 2001 From: Janry Date: Wed, 27 Nov 2019 01:07:50 +0800 Subject: [PATCH 77/99] fix(@uform/react): fix unmount (#436) * fix(@uform/react): fix unmount * fix(@uform/react): fix unmount * fix(@uform/react): update docs --- packages/react/src/hooks/useField.ts | 2 +- packages/react/src/hooks/useVirtualField.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/hooks/useField.ts b/packages/react/src/hooks/useField.ts index 7b30d582755..1afb1c94dd2 100644 --- a/packages/react/src/hooks/useField.ts +++ b/packages/react/src/hooks/useField.ts @@ -94,7 +94,7 @@ export const useField = (options: IFieldStateUIProps): IFieldHook => { ref.current.field.unsubscribe(ref.current.subscriberId) ref.current.field.setState((state: IFieldState) => { state.unmounted = true - }, true) + })//must notify,need to trigger remove value } }, []) diff --git a/packages/react/src/hooks/useVirtualField.ts b/packages/react/src/hooks/useVirtualField.ts index 964ae11c8fc..14f69c6e639 100644 --- a/packages/react/src/hooks/useVirtualField.ts +++ b/packages/react/src/hooks/useVirtualField.ts @@ -56,7 +56,7 @@ export const useVirtualField = ( ref.current.field.unsubscribe() ref.current.field.setState((state: IVirtualFieldState) => { state.unmounted = true - }, true) + })//must notify,need to trigger remove value } }, []) From 853e051fc905135a06ea5998f66959e546d12db1 Mon Sep 17 00:00:00 2001 From: dahuang Date: Wed, 27 Nov 2019 13:28:09 +0800 Subject: [PATCH 78/99] test(@uform/react): improve field and virtualField test cases (#438) --- packages/react/src/__tests__/field.spec.tsx | 141 ++++++++++++++-- packages/react/src/__tests__/vfield.spec.tsx | 159 +++++++++++++++++++ packages/react/src/types.ts | 5 +- 3 files changed, 294 insertions(+), 11 deletions(-) create mode 100644 packages/react/src/__tests__/vfield.spec.tsx diff --git a/packages/react/src/__tests__/field.spec.tsx b/packages/react/src/__tests__/field.spec.tsx index cfbcecec706..e25f2b46158 100644 --- a/packages/react/src/__tests__/field.spec.tsx +++ b/packages/react/src/__tests__/field.spec.tsx @@ -1,21 +1,142 @@ -describe('test all apis',()=>{ - //todo - test('basic',()=>{ - //todo +import React from 'react' +import { render, fireEvent, RenderResult } from '@testing-library/react' +import { + Form, + Field, + createFormActions, + createAsyncFormActions, + FormEffectHooks +} from '../index' +import { IFormActions, IFormAsyncActions } from '../types' + +const { onFieldValueChange$ } = FormEffectHooks + +const Radio = props => ( + + {({ state, mutators }) => ( +
    +
    show input
    + + Yes +
    + + No +
    +
    {state.errors}
    +
    {state.warnings}
    +
    + )} +
    +) + +const Input = props => ( + + {({ state, mutators }) => ( +
    + +
    {state.errors}
    +
    {state.warnings}
    +
    + )} +
    +) + +describe('test all apis', () => { + let actions: IFormActions + let asyncActions: IFormAsyncActions + let onSubmitHandler: any + let onResetHandler: any + let onValidateFailedHandler: any + let onChangeHandler: any + + const renderForm = (isAsync = false): RenderResult => + render( +
    { + // run effect after form mount + onFieldValueChange$('a1').subscribe(x => { + if (x.value === '0') { + setFieldState('a2', state => (state.visible = false)) + setFieldState('a3', state => (state.display = false)) + } else if (x.value === '1') { + setFieldState('a2', state => (state.visible = true)) + setFieldState('a3', state => (state.display = true)) + } + }) + }} + > + + + + + ) + + beforeAll(() => { + actions = createFormActions() + asyncActions = createAsyncFormActions() + onSubmitHandler = jest.fn() + onResetHandler = jest.fn() + onValidateFailedHandler = jest.fn() + onChangeHandler = jest.fn() + }) + + test('field visible and display', () => { + const { queryByTestId } = renderForm() + const radio1Ele = queryByTestId('radio1') + fireEvent.click(radio1Ele) + const inputA2 = queryByTestId('input-a2') + fireEvent.change(inputA2, { target: { value: '123' } }) + const inputA3 = queryByTestId('input-a3') + fireEvent.change(inputA3, { target: { value: '456' } }) + let formState = actions.getFormState() + expect(formState.values.a2).toEqual('123') + expect(formState.values.a3).toEqual('456') + const radio2Ele = queryByTestId('radio2') + fireEvent.click(radio2Ele) + formState = actions.getFormState() + expect(formState.values.a2).toBeUndefined() + expect(formState.values.a3).toEqual('456') }) }) -describe('major scenes',()=>{ +describe('major scenes', () => { //todo - test('basic',()=>{ + test('basic', () => { //todo }) }) - -describe('bugfix',()=>{ +describe('bugfix', () => { //todo - test('basic',()=>{ + test('basic', () => { //todo }) -}) \ No newline at end of file +}) diff --git a/packages/react/src/__tests__/vfield.spec.tsx b/packages/react/src/__tests__/vfield.spec.tsx new file mode 100644 index 00000000000..07dfa38e345 --- /dev/null +++ b/packages/react/src/__tests__/vfield.spec.tsx @@ -0,0 +1,159 @@ +import React from 'react' +import { render, fireEvent, RenderResult } from '@testing-library/react' +import { + Form, + Field, + VirtualField, + createFormActions, + createAsyncFormActions, + FormEffectHooks +} from '../index' +import { IFormActions, IFormAsyncActions } from '../types' + +const { onFieldValueChange$ } = FormEffectHooks + +const FieldBlock = props => ( + + {({ state }) => ( +
    + block-{props.name} + {props.children} +
    + )} +
    +) + +const Radio = props => ( + + {({ state, mutators }) => ( +
    +
    show input
    + + Yes +
    + + No +
    +
    {state.errors}
    +
    {state.warnings}
    +
    + )} +
    +) + +const Input = props => ( + + {({ state, mutators }) => ( +
    + +
    {state.errors}
    +
    {state.warnings}
    +
    + )} +
    +) + +describe('test all apis', () => { + let actions: IFormActions + let asyncActions: IFormAsyncActions + let onSubmitHandler: any + let onResetHandler: any + let onValidateFailedHandler: any + let onChangeHandler: any + + const renderForm = (isAsync = false): RenderResult => + render( +
    { + onFieldValueChange$('a1').subscribe(x => { + if (x.value === '0') { + setFieldState('b', state => (state.visible = false)) + setFieldState('c', state => (state.display = false)) + } else if (x.value === '1') { + setFieldState('b', state => (state.visible = true)) + setFieldState('c', state => (state.display = true)) + } + }) + }} + > + + + + + + + + + ) + + beforeAll(() => { + actions = createFormActions() + asyncActions = createAsyncFormActions() + onSubmitHandler = jest.fn() + onResetHandler = jest.fn() + onValidateFailedHandler = jest.fn() + onChangeHandler = jest.fn() + }) + + test('virtualField visible and display', () => { + const { queryByTestId } = renderForm() + const radio1Ele = queryByTestId('radio1') + fireEvent.click(radio1Ele) + const inputB1 = queryByTestId('input-b.b1') + fireEvent.change(inputB1, { target: { value: '123' } }) + const inputC1 = queryByTestId('input-c.c1') + fireEvent.change(inputC1, { target: { value: '456' } }) + + let formState = actions.getFormState() + expect(formState.values.b1).toEqual('123') + expect(formState.values.c1).toEqual('456') + expect(formState.values).toEqual({ a1: '1', b1: '123', c1: '456' }) + const radio2Ele = queryByTestId('radio2') + fireEvent.click(radio2Ele) + formState = actions.getFormState() + expect(formState.values.b1).toBeUndefined() + expect(formState.values).toEqual({ a1: '0', c1: '456' }) + }) +}) + +describe('major scenes', () => { + //todo + test('basic', () => { + //todo + }) +}) + +describe('bugfix', () => { + //todo + test('basic', () => { + //todo + }) +}) diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 186067331f2..1bc54a1153a 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -51,7 +51,10 @@ export interface IFormProps< onSubmit?: (values: Value) => void | Promise onReset?: () => void onValidateFailed?: (valideted: IFormValidateResult) => void - children?: React.ReactElement | ((form: IForm) => React.ReactElement) + children?: + | React.ReactElement + | React.ReactElement[] + | ((form: IForm) => React.ReactElement) useDirty?: boolean editable?: boolean | ((name: string) => boolean) validateFirst?: boolean From fd93386fe5e03fd89a438746d064758933539863 Mon Sep 17 00:00:00 2001 From: quirkyvar Date: Wed, 27 Nov 2019 14:14:03 +0800 Subject: [PATCH 79/99] fix(@uform/antd/next/react): doc (#437) --- packages/antd/README.md | 2956 +++++++++++++++- packages/antd/README.zh-cn.md | 2934 +++++++++++++++- packages/antd/src/components/Button.tsx | 7 +- .../antd/src/components/FormButtonGroup.tsx | 6 + packages/antd/src/types.ts | 6 +- packages/next/README.md | 2970 ++++++++++++++++- packages/next/README.zh-cn.md | 2966 +++++++++++++++- packages/next/src/components/Button.tsx | 7 +- .../next/src/components/FormButtonGroup.tsx | 6 + packages/next/src/types.ts | 6 +- .../src/components/SchemaMarkup.tsx | 5 +- packages/react/README.md | 216 ++ packages/react/README.zh-cn.md | 215 +- packages/react/src/hooks/useFormState.ts | 2 +- 14 files changed, 12287 insertions(+), 15 deletions(-) diff --git a/packages/antd/README.md b/packages/antd/README.md index c76e7635a8d..85a992c8da9 100644 --- a/packages/antd/README.md +++ b/packages/antd/README.md @@ -1,2 +1,2956 @@ # @uform/antd -> UForm Ant Design组件插件包 \ No newline at end of file + +### Install + +```bash +npm install --save @uform/antd +``` + +### Table Of Contents + + + +- [Quick-Start](#Quick-Start) +- [Components](#components) + - [``](#SchemaForm) + - [``](#SchemaMarkupField) + - [``](#Submit) + - [``](#Reset) + - [`(deprecated,please use )`](#) +- [Form List](#Array-Components) + - [`array`](#array) + - [`cards`](#cards) + - [`table`](#table) +- [Layout Components](#Layout-Components) + - [``](#FormCard) + - [``](#FormBlock) + - [``](#FormStep) + - [``](#FormLayout) + - [``](#FormItemGrid) + - [``](#FormTextBox) + - [``](#FormButtonGroup) + - [``](#TextButton) + - [``](#CircleButton) +- [Type of SchemaMarkupField](#Type-of-SchemaMarkupField) + - [`string`](#string) + - [`textarea`](#textarea) + - [`password`](#password) + - [`number`](#number) + - [`boolean`](#boolean) + - [`date`](#date) + - [`time`](#time) + - [`range`](#range) + - [`upload`](#upload) + - [`checkbox`](#checkbox) + - [`radio`](#radio) + - [`rating`](#rating) + - [`transfer`](#transfer) +- [API](#API) + - [`createFormActions`](#createFormActions) + - [`createAsyncFormActions`](#createAsyncFormActions) + - [`FormEffectHooks`](#FormEffectHooks) + - [`createEffectHook`](#createEffectHook) + - [`connect`](#connect) + - [`registerFormField`](#registerFormField) +- [Interfaces](#Interfaces) + - [`ButtonProps`](#ButtonProps) + - [`CardProps`](#CardProps) + - [`ICompatItemProps`](#ICompatItemProps) + - [`IFieldState`](#IFieldState) + - [`ISchemaFieldComponentProps`](#ISchemaFieldComponentProps) + - [`ISchemaVirtualFieldComponentProps`](#ISchemaVirtualFieldComponentProps) + - [`ISchemaFieldWrapper`](#ISchemaFieldWrapper) + - [`ISchemaFieldComponent`](#ISchemaFieldComponent) + - [`ISchemaVirtualFieldComponent`](#ISchemaVirtualFieldComponent) + - [`ISchemaFormRegistry`](#ISchemaFormRegistry) + - [`INextSchemaFieldProps`](#INextSchemaFieldProps) + - [`IPreviewTextProps`](#IPreviewTextProps) + - [`IMutators`](#IMutators) + - [`IFieldProps`](#IFieldProps) + - [`IConnectOptions`](#IConnectOptions) + + +### Quick-Start + +--- + +Example:develop with JSX + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import { Button } from 'antd' +import'antd/dist/antd.css' + +const actions = createFormActions() + +const App = () => { + return ( + + + + + + + + + + + + + + + + item.title + }} + title="transfer" + name="transfer" + + /> + + + + + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +Example:develop with JSON Schema + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import { Button } from 'antd' +import'antd/dist/antd.css' + +const actions = createFormActions() + +const App = () => { + const schema = { + type: 'object', + properties: { + radio: { + type: 'radio', + enum: ['1', '2', '3', '4'], + title: 'Radio' + }, + select: { + type: 'string', + enum: ['1', '2', '3', '4'], + title: 'Select', + required: true + }, + checkbox: { + type: 'checkbox', + enum: ['1', '2', '3', '4'], + title: 'Checkbox', + required: true + }, + textarea: { + type: 'string', + 'x-component': 'textarea', + title: 'TextArea' + }, + number: { + type: 'number', + title: 'number' + }, + boolean: { + type: 'boolean', + title: 'boolean' + }, + date: { + type: 'date', + title: 'date' + }, + daterange: { + type: 'daterange', + default: ['2018-12-19', '2018-12-19'], + title: 'daterange' + }, + year: { + type: 'year', + title: 'year' + }, + time: { + type: 'time', + title: 'time' + }, + upload: { + type: 'upload', + 'x-props': { + listType: 'card' + }, + title: 'upload(card)' + }, + upload2: { + type: 'upload', + 'x-props': { + listType: 'dragger' + }, + title: 'uplaod(dragger)' + }, + upload3: { + type: 'upload', + 'x-props': { + listType: 'text' + }, + title: 'upload(text)' + }, + range: { + type: 'range', + 'x-props': { + min: 0, + max: 1024, + marks: [0, 1024] + }, + title: 'range' + }, + transfer: { + type: 'transfer', + enum: [ + { + key: 1, + title: 'opt1' + }, + { + key: 2, + title: 'opt2' + } + ], + 'x-props': { + render: (item) => item.title + }, + title: 'transfer' + }, + rating: { + type: 'rating', + title: 'rating' + }, + layout_btb_group: { + type: 'object', + 'x-component': 'button-group', + 'x-component-props': { + offset:7, + sticky: true, + }, + properties: { + submit_btn: { + type: 'object', + 'x-component': 'submit', + 'x-component-props': { + children: 'Submit', + }, + }, + reset_btn: { + type: 'object', + 'x-component': 'reset', + 'x-component-props': { + children: 'Reset', + }, + }, + } + }, + } + } + return +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Components + +--- + +#### `` + +Base on `` of @uform/react-schema-renderer. Recommended for production environments. + + +```typescript +interface IAntdSchemaFormProps { + // render by schema + schema?: ISchema; + fields?: ISchemaFormRegistry['fields']; + virtualFields?: ISchemaFormRegistry['virtualFields']; + // pre-registered Form Component + formComponent?: ISchemaFormRegistry['formComponent']; + // pre-registered FormItem Component + formItemComponent?: ISchemaFormRegistry['formItemComponent']; + // layout setting + layout?: FormLayout; + form?: WrappedFormUtils; + // triggered by `htmlType="submit"` or action.submit + onSubmit?: React.FormEventHandler; + style?: React.CSSProperties; + className?: string; + // className of prefix + prefixCls?: string; + hideRequiredMark?: boolean; + // FormItem column settiing + wrapperCol?: ColProps; + // label column settiing + labelCol?: ColProps; + // it there a colon + colon?: boolean; + // alignment of label + labelAlign?: FormLabelAlign; + // is it inline + inline?: boolean + className?: string + style?: React.CSSProperties + // custom placeholder when preivew + previewPlaceholder?: string | ((props: IPreviewTextProps) => string); + // form state value + value?: Value; + // form state defaultValue + defaultValue?: DefaultValue; + // form state initialValues + initialValues?: DefaultValue; + // FormActions instance + actions?: FormActions; + // IFormEffect instance + effects?: IFormEffect; + // form instance + form?: IForm; + // Form change event callback + onChange?: (values: Value) => void; + // triggered by `htmlType="submit"` or actions.submit时 + onSubmit?: (values: Value) => void | Promise; + // triggered by or actions.reset + onReset?: () => void; + // Form verification failure event callback + onValidateFailed?: (valideted: IFormValidateResult) => void; + children?: React.ReactElement | ((form: IForm) => React.ReactElement); + // Whether to use the dirty check, the default will go immer accurate update + useDirty?: boolean; + // Is it editable, overall control in the Form dimension + editable?: boolean | ((name: string) => boolean); + // Whether to go pessimistic check, stop the subsequent check when the first check fails + validateFirst?: boolean; +} +``` + +**Usage** + +Example1: Sync value of a and a-mirror + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + registerFormField, + Field, + connect, + createFormActions +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() + +ReactDOM.render( + { + $('onFieldChange','aa').subscribe((fieldState)=>{ + actions.setFieldState('bb',state=>{ + state.value = fieldState.value + }) + }) + }}> + + + , + document.getElementById('root') +) +``` + + +Example:Layout + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + createFormActions, + FormLayout, + FormButtonGroup, + Submit, + Reset, +} from '@uform/antd' + +const actions = createFormActions() + +ReactDOM.render( +
    +
    Basic Layout
    + + + + + + + + SubmitReset​ + + +
    Inline Layout
    + + + + ​ + + SubmitReset​ + + +
    editable = false
    + + + + ​ + + SubmitReset​ + + +
    , + document.getElementById('root') +) +``` + +#### `` + +> Core components of @uform/antd, used to describe form fields + +```typescript +interface IMarkupSchemaFieldProps { + name?: string + /** base json schema spec**/ + title?: SchemaMessage + description?: SchemaMessage + default?: any + readOnly?: boolean + writeOnly?: boolean + type?: 'string' | 'object' | 'array' | 'number' | string + enum?: Array + const?: any + multipleOf?: number + maximum?: number + exclusiveMaximum?: number + minimum?: number + exclusiveMinimum?: number + maxLength?: number + minLength?: number + pattern?: string | RegExp + maxItems?: number + minItems?: number + uniqueItems?: boolean + maxProperties?: number + minProperties?: number + required?: string[] | boolean + format?: string + /** nested json schema spec **/ + properties?: { + [key: string]: ISchema + } + items?: ISchema | ISchema[] + additionalItems?: ISchema + patternProperties?: { + [key: string]: ISchema + } + additionalProperties?: ISchema + /** extend json schema specs */ + editable?: boolean + visible?: boolean + display?: boolean + ['x-props']?: { [name: string]: any } + ['x-index']?: number + ['x-rules']?: ValidatePatternRules + ['x-component']?: string + ['x-component-props']?: { [name: string]: any } + ['x-render']?: ( + props: T & { + renderComponent: () => React.ReactElement + } + ) => React.ReactElement + ['x-effect']?: ( + dispatch: (type: string, payload: any) => void, + option?: object + ) => { [key: string]: any } +} +``` + +##### Usage + + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + FormSlot, + Field, + createFormActions, + FormLayout, + FormButtonGroup, + Submit, + Reset, +} from '@uform/antd' + +const actions = createFormActions() + +ReactDOM.render( + +
    required
    + + +
    description
    + + +
    default value
    + + +
    readOnly
    + + +
    visible = false
    + + +
    display = false
    + + +
    editable = false
    + +
    , + document.getElementById('root') +) +``` + + +#### `` + +> Props of `` + +```typescript +interface ISubmitProps { + /** reset pops **/ + onSubmit?: ISchemaFormProps['onSubmit'] + showLoading?: boolean + /** nextBtnProps **/ + // type of btn + type?: 'primary' | 'secondary' | 'normal' + // size of btn + size?: 'small' | 'medium' | 'large' + // size of Icon + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // type of button when component = 'button' + htmlType?: 'submit' | 'reset' | 'button' + // typeof btn + component?: 'button' | 'a' + // Set the loading state of the button + loading?: boolean + // Whether it is a ghost button + ghost?: true | false | 'light' | 'dark' + // Whether it is a text button + text?: boolean + // Whether it is a warning button + warning?: boolean + // Whether it is disabled + disabled?: boolean + // Callback for button click + onClick?: (e: {}) => void + // Valid when Button component is set to 'a', which represents the URL of the linked page + href?: string + // Valid when Button component is set to 'a', which represents the way of open the linked document + target?: string +} +``` + +#### `` + +> Props of `` + +```typescript +interface IResetProps { + /** reset pops **/ + forceClear?: boolean + validate?: boolean + /** nextBtnProps **/ + // type of btn + type?: 'primary' | 'secondary' | 'normal' + // size of btn + size?: 'small' | 'medium' | 'large' + // size of Icon + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // type of button when component = 'button' + htmlType?: 'submit' | 'reset' | 'button' + // typeof btn + component?: 'button' | 'a' + // Set the loading state of the button + loading?: boolean + // Whether it is a ghost button + ghost?: true | false | 'light' | 'dark' + // Whether it is a text button + text?: boolean + // Whether it is a warning button + warning?: boolean + // Whether it is disabled + disabled?: boolean + // Callback for button click + onClick?: (e: {}) => void + // Valid when Button component is set to 'a', which represents the URL of the linked page + href?: string + // Valid when Button component is set to 'a', which represents the way of open the linked document + target?: string +} +``` + +#### `` + +> deprecated,please use [SchemaMarkupField](#SchemaMarkupField) + +### Array Components + +#### array + +```jsx +import React, { useState, useEffect } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/antd' +import'antd/dist/antd.css' +import Printer from '@uform/printer' + +const App = () => { + const [value, setValues] = useState({}) + useEffect(() => { + setTimeout(() => { + setValues({ + array: [{ array2: [{ aa: '123', bb: '321' }] }] + }) + }, 1000) + }, []) + return ( + + console.log(v)}> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Submit + Reset + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### cards + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/antd' +import'antd/dist/antd.css' +import Printer from '@uform/printer' + +const App = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### table + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/antd' +import'antd/dist/antd.css' +import Printer from '@uform/printer' + +const App = () => ( + + + + Hello worldasdasdasdasd + }, + operationsWidth: 300 + }} + > + + + + + + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +### Layout Components + + +#### `` + +> Props of ``, fully inherited from [CardProps](#CardProps)。 +> The only difference between FormCard [FormBlock](#FormBlock) is a border on the style + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { FormCard, SchemaMarkupField as Field } from '@uform/antd' +import'antd/dist/antd.css' + +const App = () => ( + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` , fully inherited from [CardProps](#CardProps) + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { FormBlock, SchemaMarkupField as Field } from '@uform/antd' +import'antd/dist/antd.css' + +const App = () => ( + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormStep { + dataSource: StepItemProps[] + /** next step props**/ + // current + current?: number + // direction of step + direction?: 'hoz' | 'ver' + // Content arrangement in horizontal layout + labelPlacement?: 'hoz' | 'ver' + // shape of step + shape?: 'circle' | 'arrow' | 'dot' + readOnly?: boolean + // Whether to activate animation + animation?: boolean + className?: string + // Custom StepItem render + itemRender?: (index: number, status: string) => React.ReactNode +} +``` + +**Usage** + +```jsx +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + FormEffectHooks, + createFormActions, + FormGridRow, + FormItemGrid, + FormGridCol, + FormPath, + FormLayout, + FormBlock, + FormCard, + FormTextBox, + FormStep +} from '@uform/antd' +import { Button } from 'antd' +import'antd/dist/antd.css' + +const { onFormInit$ } = FormEffectHooks + +const actions = createFormActions() + +let cache = {} + +export default () => ( + { + console.log('submit') + console.log(values) + }} + actions={actions} + labelCol={{ span: 8 }} + wrapperCol={{ span: 6 }} + validateFirst + effects={({ setFieldState, getFormGraph }) => { + onFormInit$().subscribe(() => { + setFieldState('col1', state => { + state.visible = false + }) + }) + }} + > + + + + + + + + + + + + Submit + + + + + + +) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormItemTopProps { + inline?: boolean + className?: string + style?: React.CSSProperties + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} +``` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/antd' +import { Button } from 'antd' +import Printer from '@uform/printer' +import'antd/dist/antd.css' +const App = () => ( + + + + + + + + + SubmitReset​ + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormItemGridProps { + cols?: Array + gutter?: number + /** next Form.Item props**/ + // prefix od FormItem + prefix?: string + + // label od FormItem + label?: React.ReactNode + + // label layout setting, eg, {span: 8, offset: 16} + labelCol?: {} + wrapperCol?: {} + + // Custom prompt information, if not set, it will be automatically generated according to the verification rules. + help?: React.ReactNode + + // Additional prompt information, similar to help, can be used when error messages and prompt copy are required at the same time. Behind the error message. + extra?: React.ReactNode + + // Check status, if not set, it will be generated automatically according to check rules + validateState?: 'error' | 'success' | 'loading' + + // Used in conjunction with the validateState property, whether to display the success / loading validation status icon. Currently only Input supports + hasFeedback?: boolean + + // Custom inline style + + style?: React.CSSProperties + + // node or function(values) + children?: React.ReactNode | (() => void) + + // The size of a single Item is customized, and takes precedence over the size of the Form, and when a component is used with an Item, the component itself does not set the size property. + size?: 'large' | 'small' | 'medium' + + // Position of the label + labelAlign?: 'top' | 'left' | 'inset' + + // alignment of labels + labelTextAlign?: 'left' | 'right' + + className?: string + + // [validation] required + required?: boolean + + // whether required asterisks are displayed + asterisk?: boolean + + // required custom error message + requiredMessage?: string + + // required Custom trigger method + requiredTrigger?: string | Array + + // [validation] min + min?: number + + // [validation] max + max?: number + + // min/max error message + minmaxMessage?: string + + // min/max custom trigger method + minmaxTrigger?: string | Array + + // [validation] min length of string / min length of array + minLength?: number + + // [validation] max length of string / max length of array + maxLength?: number + + // minLength/maxLength custom error message + minmaxLengthMessage?: string + + // minLength/maxLength custom trigger method + minmaxLengthTrigger?: string | Array + + // [validation] length of string / length of array + length?: number + + // length custom error message + lengthMessage?: string + + // length custom trigger method + lengthTrigger?: string | Array + + // Regular pattern + pattern?: any + + // pattern custom error message + patternMessage?: string + + // pattern custom trigger method + patternTrigger?: string | Array + + // [validation] regular pattern + format?: 'number' | 'email' | 'url' | 'tel' + + // format custom error message + formatMessage?: string + + // format custom trigger method + formatTrigger?: string | Array + + // [validation] custom validator + validator?: () => void + + // validator custom trigger method + validatorTrigger?: string | Array + + // Whether to automatically trigger validate when data is modified + autoValidate?: boolean +} +``` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/antd' +import { Button } from 'antd' +import Printer from '@uform/printer' +import'antd/dist/antd.css' + +const App = () => ( + + console.log(v)}> + + + + + + + + + + + + ​SubmitReset​ + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormTextBox { + text?: string + gutter?: number + title?: React.ReactText + description?: React.ReactText +} +``` + +**Usage** + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormTextBox, + FormCard, + FormLayout +} from '@uform/antd' +import { Button } from 'antd' +import Printer from '@uform/printer' +import'antd/dist/antd.css' + +const App = () => { + return ( + + console.log(v)}> + + + + + + + + + + + + ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormButtonGroupProps { + sticky?: boolean + style?: React.CSSProperties + itemStyle?: React.CSSProperties + className?: string + align?: 'left' | 'right' | 'start' | 'end' | 'top' | 'bottom' | 'center' + triggerDistance?: number + zIndex?: number + span?: ColSpanType + offset?: ColSpanType +} +``` + +**Usage** + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/antd' +import { Button } from 'antd' +import Printer from '@uform/printer' +import'antd/dist/antd.css' + +const App = () => { + const [state, setState] = useState({ editable: true }) + return ( + + console.log(v)}> +
    normal
    + + ​SubmitReset​ + +
    sticky
    + + ​Submit​ + + Reset​ + +
    +
    + ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of ``, fully inherited from [ButtonProps](#ButtonProps) + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { TextButton } from '@uform/antd' +import'antd/dist/antd.css' + +const App = () => ( + + content + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of ``, fully inherited from [ButtonProps](#ButtonProps) + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { CircleButton } from '@uform/antd' +import'antd/dist/antd.css' + +const App = () => ( + + ok + +) +ReactDOM.render(, document.getElementById('root')) +``` + + +### Type of SchemaMarkupField + +#### string + +* Schema Type : `string` +* Schema UI Component: Fusion-Next ``, ``, `` + +```typescript +interface IPasswordProps { + checkStrength: boolean + /** next input props **/ + // value + value?: string | number + + // default value + defaultValue?: string | number + + // callback triggered when value change + onChange?: (value: string, e: React.ChangeEvent) => void + + // callback triggered when keyboard is press + onKeyDown?: (e: React.KeyboardEvent, opts: {}) => void + + // Disabled + disabled?: boolean + + // Max length + maxLength?: number + + // Whether to show the maximum length style + hasLimitHint?: boolean + + // When maxLength is set, whether to truncate beyond the string + cutString?: boolean + + // readOnly + readOnly?: boolean + + // Automatically remove the leading and trailing blank characters when trigger onChange + trim?: boolean + + // placeholder + placeholder?: string + + // callback triggered when focus + onFocus?: () => void + + // callback triggered when blur + onBlur?: () => void + + // Custom string length calculation + getValueLength?: (value: string) => number + + className?: string + style?: React.CSSProperties + htmlType?: string + + // name of field + name?: string + + // state of field + state?: 'error' | 'loading' | 'success' + + // label + label?: React.ReactNode + + // whether to show clear + hasClear?: boolean + + // whether to show border + hasBorder?: boolean + + // size of field + size?: 'small' | 'medium' | 'large' + + // callback triggered when enter is press + onPressEnter?: () => void + + // Watermark (Icon type, shared with hasClear) + hint?: string + + // Append content before text + innerBefore?: React.ReactNode + + // Append content after text + innerAfter?: React.ReactNode + + // Append content before input + addonBefore?: React.ReactNode + + // Append content after input + addonAfter?: React.ReactNode + + // Append text before input + addonTextBefore?: React.ReactNode + + // Append text after input + addonTextAfter?: React.ReactNode + + // (Native supported by input) + autoComplete?: string + + // (Native supported by input) + autoFocus?: boolean +} +``` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### number + +* Schema Type : `number` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### boolean + +* Schema Type : `boolean` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### date + +* Schema Type : `date` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### time + +* Schema Type : `time` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### range + +* Schema Type : `range` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### upload + +* Schema Type : `upload` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### checkbox + +* Schema Type : `checkbox` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### radio + +* Schema Type : `radio` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### rating + +* Schema Type : `rating` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### transfer + +* Schema Type : `transfer` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + item.title + }} + x-component-props={{ + showSearch: true + }} + /> + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### API + +> Fully inherited from @uform/react, The specific API of @uform/antd is listed below. + +--- + +#### `createFormActions` + +> Return [IFormActions](#IFormActions) + +**Signature** + +```typescript +createFormActions(): IFormActions +``` + +**Usage** + +```typescript +import { createFormActions } from '@uform/antd' + +const actions = createFormActions() +console.log(actions.getFieldValue('username')) +``` + +#### `createAsyncFormActions` + +> Return [IFormAsyncActions](#IFormAsyncActions) + +**Signature** + +```typescript +createAsyncFormActions(): IFormAsyncActions +``` + +**Usage** + +```typescript +import { createAsyncFormActions } from '@uform/antd' + +const actions = createAsyncFormActions() +actions.getFieldValue('username').then(val => console.log(val)) +``` + +#### `FormEffectHooks` + +> Return all @uform/core lifeCycles hook which can be subscribe + +**Usage** + +```tsx +import { FormEffectHooks, Form } from '@uform/react' +const { + /** + * Form LifeCycle + **/ + // Form pre-initialization trigger + onFormWillInit$, + // Form initialization trigger + onFormInit$, + // Triggered when the form changes + onFormChange$, + // Triggered when the form event is triggered, used to monitor only manual operations + onFormInputChange$, + // Trigger when the form initial value changes + onFormInitialValueChange$, + // Triggered when the form is reset + onFormReset$, + // Triggered when the form is submitted + onFormSubmit$, + // Triggered when the form submission starts + onFormSubmitStart$, + // Triggered when the form submission ends + onFormSubmitEnd$, + // Triggered when the form is mounted + onFormMount$, + // Triggered when the form is unloaded + onFormUnmount$, + // Triggered when form validation begins + onFormValidateStart$, + // Triggered when the form validation ends + onFormValidateEnd$, + // Trigger when the form initial value changes + onFormValuesChange$, + /** + * FormGraph LifeCycle + **/ + // Triggered when the form observer tree changes + onFormGraphChange$, + /** + * Field LifeCycle + **/ + // Triggered when pre-initialized + onFieldWillInit$, + // Triggered when the field is initialized + onFieldInit$, + // Triggered when the field changes + onFieldChange$, + // Triggered when the field is mounted + onFieldMount$, + // Trigger when the field is unloaded + onFieldUnmount$, + // Triggered when the field event is triggered, used to monitor only manual operations + onFieldInputChange$, + // Triggered when the field value changes + onFieldValueChange$, + // Trigger when the initial value of the field changes + onFieldInitialValueChange$ +} = FormEffectHooks + +const App = () => { + return ( +
    { + onFormInit$().subscribe(() => { + console.log('initialized') + }) + }} + > + ... +
    + ) +} +``` + +#### createEffectHook + +> Custom your own hook by this api + +**Usage** + +```jsx +import SchemaForm, { createEffectHook, createFormActions } from '@uform/antd' + +const actions = createFormActions() +const diyHook1$ = createEffectHook('diy1') +const diyHook2$ = createEffectHook('diy2') + +const App = () => { + return ( + { + diyHook1$().subscribe(payload => { + console.log('diy1 hook triggered', payload) + }) + + diyHook2$().subscribe(payload => { + console.log('diy2 hook triggered', payload) + }) + }} + > + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### connect + +> Wrap field components with value / defaultValue / onChange of field, which make it esaily make custom field + +```typescript +type Connect = >(options?: IConnectOptions) => +(Target: T) => React.PureComponent +``` +**Usage** + +```typescript +import {registerFormField,connect} from '@uform/antd' + +registerFormField( + 'string', + connect()(props => ) +) +``` + +#### registerFormField + +```typescript +type registerFormField( + name : string, // name of field + component : React.ComponentType, // component of field + noMiddleware: boolean // whether use middleware +) +``` + +**Usage** + +```jsx + +import SchemaForm, { SchemaMarkupField as Field, registerFormField, connect, createFormActions } from '@uform/antd' + +registerFormField( + 'custom-string', + connect()(props => ) +) +const actions = createFormActions() + +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Interfaces + +> The Interfaces is fully inherited from @uform/react. The specific Interfaces of @uform/antd is listed below. +--- + +#### IForm + +> Form instance object API created by using createForm + + + +```typescript +interface IForm { +  /* +   * Form submission, if the callback parameter returns Promise, +   * Then the entire submission process will hold and load is true. +   * Wait for Promise resolve to trigger the form onFormSubmitEnd event while loading is false +   */ +   submit( +      onSubmit?: (values: IFormState['values']) => any | Promise +    ): Promise<{ +       Validated: IFormValidateResult +       Payload: any //onSubmit callback function return value +   }> +    +   /* +    * Clear the error message, you can pass the FormPathPattern to batch or precise control of the field to be cleared. +    * For example, clearErrors("*(aa,bb,cc)") +    */ +   clearErrors: (pattern?: FormPathPattern) => void +    +   /* +    * Get status changes, mainly used to determine which states in the current life cycle have changed in the form lifecycle hook. +    * For example, hasChanged(state,'value.aa') +    */ +   hasChanged(target: IFormState | IFieldState | IVirtualFieldState, path: FormPathPattern): boolean +    +   /* +    * Reset form +    */ +   reset(options?: { +     // Forced to empty +     forceClear?: boolean +     // Forced check +     validate?: boolean +     // Reset range for batch or precise control of the field to be reset +     selector?: FormPathPattern +   }): Promise +    +   /* +    * Validation form +    */ +   validate(path?: FormPathPattern, options?: { +     // Is it pessimistic check, if the current field encounters the first verification error, stop the subsequent verification process +     first?:boolean +   }): Promise +    +   /* +    * Set the form status +    */ +   setFormState( +     // Operation callback +     callback?: (state: IFormState) => any, +     // No trigger the event +     silent?: boolean +   ): void +    +   /* +    * Get form status +    */ +   getFormState( +     //transformer +     callback?: (state: IFormState) => any +   ): any +    +   /* +    * Set the field status +    */ +   setFieldState( +     // Field path +     path: FormPathPattern, +     // Operation callback +     callback?: (state: IFieldState) => void, +     // No trigger the event +     silent?: boolean +   ): void +    +   /* +    * Get the field status +    */ +   getFieldState( +     // Field path +     path: FormPathPattern, +     // Transformer +     callback?: (state: IFieldState) => any +   ): any +    +   /* +    * Registration field +    */ +   registerField(props: { +    // Node path +    path?: FormPathPattern +    // Data path +    name?: string +    // Field value +    value?: any +    // Field multi-value +    values?: any[] +    // Field initial value +    initialValue?: any +    // Field extension properties +    props?: any +    // Field check rule +    rules?: ValidatePatternRules[] +    // Field is required +    required?: boolean +    // Is the field editable? +    editable?: boolean +    // Whether the field is dirty check +    useDirty?: boolean +    // Field state calculation container, mainly used to extend the core linkage rules +    computeState?: (draft: IFieldState, prevState: IFieldState) => void +  }): IField +   +  /* +   * Register virtual fields +   */ +  registerVirtualField(props: { +    // Node path +    path?: FormPathPattern +    // Data path +    name?: string +    // Field extension properties +    props?: any +    // Whether the field is dirty check +    useDirty?: boolean +    // Field state calculation container, mainly used to extend the core linkage rules +    computeState?: (draft: IFieldState, prevState: IFieldState) => void +  }): IVirtualField +   +  /* +   * Create a field data operator, which will explain the returned API in detail later. +   */ +  createMutators(field: IField): IMutators +   +  /* +   * Get the form observer tree +   */ +  getFormGraph(): IFormGraph +   +  /* +   * Set the form observer tree +   */ +  setFormGraph(graph: IFormGraph): void +   +  /* +   * Listen to the form life cycle +   */ +  subscribe(callback?: ({ +    type, +    payload +  }: { +    type: string +    payload: any +  }) => void): number +   +  /* +   * Cancel the listening form life cycle +   */ +  unsubscribe(id: number): void +   +  /* +   * Trigger form custom life cycle +   */ +  notify: (type: string, payload?: T) => void +   +  /* +   * Set the field value +   */ +  setFieldValue(path?: FormPathPattern, value?: any): void +   +  /* +   * Get the field value +   */ +  getFieldValue(path?: FormPathPattern): any +   +  /* +   * Set the initial value of the field +   */ +  setFieldInitialValue(path?: FormPathPattern, value?: any): void +   +  /* +   * Get the initial value of the field +   */ +  getFieldInitialValue(path?: FormPathPattern): any +} +``` + +#### ButtonProps + +```typescript +interface ButtonProps { + href: string; + target?: string; + onClick?: React.MouseEventHandler; + htmlType?: ButtonHTMLType; + onClick?: React.MouseEventHandler; +} +``` + +#### CardProps + +```typescript +interface CardProps extends HTMLAttributesWeak, CommonProps { + prefixCls?: string; + title?: React.ReactNode; + extra?: React.ReactNode; + bordered?: boolean; + headStyle?: React.CSSProperties; + bodyStyle?: React.CSSProperties; + style?: React.CSSProperties; + loading?: boolean; + noHovering?: boolean; + hoverable?: boolean; + children?: React.ReactNode; + id?: string; + className?: string; + size?: CardSize; + type?: CardType; + cover?: React.ReactNode; + actions?: React.ReactNode[]; + tabList?: CardTabListType[]; + tabBarExtraContent?: React.ReactNode | null; + onTabChange?: (key: string) => void; + activeTabKey?: string; + defaultActiveTabKey?: string; +} +``` + +#### ICompatItemProps + +```typescript +interface ICompatItemProps + extends Exclude, + Partial { + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} +``` + + +#### IFieldState + +```typescript +interface IFieldState { + /**Read-only attribute**/ + // State name, FieldState + displayName?: string // Data path + name: string // Node path + path: string // Has been initialized + initialized: boolean // Is it in the original state, the state is true only when value===intialValues + pristine: boolean // Is it in a legal state, as long as the error length is greater than 0, the valid is false + valid: boolean // Is it illegal, as long as the error length is greater than 0, the valid is true + invalid: boolean // Is it in check state? + validating: boolean // Is it modified, if the value changes, the property is true, and will be true throughout the life of the field + modified: boolean // Is it touched? + touched: boolean // Is it activated, when the field triggers the onFocus event, it will be triggered to true, when onBlur is triggered, it is false + active: boolean // Have you ever visited, when the field triggers the onBlur event, it will be triggered to true + visited: boolean /** writable property**/ // Is it visible, note: if the state is false, then the value of the field will not be submitted, and the UI will not display + visible: boolean // Whether to show, note: if the state is false, then the value of the field will be submitted, the UI will not display, similar to the form hidden field + display: boolean // Is it editable? + editable: boolean // Is it in the loading state, note: if the field is in asynchronous verification, loading is true + loading: boolean // Field multi-parameter value, such as when the field onChange trigger, the event callback passed multi-parameter data, then the value of all parameters will be stored here + values: any[] // Field error message + errors: string[] // Field alert message + warnings: string[] // Field value, is equal to values[0] + value: any // Initial value + initialValue: any // Check the rules, the specific type description refers to the following documents + rules: ValidatePatternRules[] // Is it required? + required: boolean // Whether to mount + mounted: boolean // Whether to uninstall + unmounted: boolean // field extension properties + props: FieldProps +} +``` + + +#### ISchemaFieldComponentProps + +```typescript +interface ISchemaFieldComponentProps extends IFieldState { + // render by schema + schema: Schema + // mutators of + mutators: IMutators + // form instance + form: IForm + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + +#### ISchemaVirtualFieldComponentProps + +```typescript +interface ISchemaVirtualFieldComponentProps extends IVirtualFieldState { + // render by schema + schema: Schema + // form instance + form: IForm + children: React.ReactElement[] + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + +#### ISchemaFieldWrapper + +```typescript +interface ISchemaFieldWrapper { + (Traget: ISchemaFieldComponent): + | React.FC + | React.ClassicComponent +} +``` + +#### ISchemaFieldComponent + +```typescript +declare type ISchemaFieldComponent = ComponentWithStyleComponent< + ISchemaFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} +``` + +#### ISchemaVirtualFieldComponent + +```typescript +declare type ISchemaVirtualFieldComponent = ComponentWithStyleComponent< + ISchemaVirtualFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} +``` + +#### ISchemaFormRegistry + +```typescript +interface ISchemaFormRegistry { + fields: { + [key: string]: ISchemaFieldComponent + } + virtualFields: { + [key: string]: ISchemaVirtualFieldComponent + } + wrappers?: ISchemaFieldWrapper[] + formItemComponent: React.JSXElementConstructor + formComponent: string | React.JSXElementConstructor +} +``` + +#### INextSchemaFieldProps + +```typescript +interface INextSchemaFieldProps { + name?: string; + /** ISchema **/ + title?: SchemaMessage; + description?: SchemaMessage; + default?: any; + readOnly?: boolean; + writeOnly?: boolean; + type?: 'string' | 'object' | 'array' | 'number' | string; + enum?: Array; + const?: any; + multipleOf?: number; + maximum?: number; + exclusiveMaximum?: number; + minimum?: number; + exclusiveMinimum?: number; + maxLength?: number; + minLength?: number; + pattern?: string | RegExp; + maxItems?: number; + minItems?: number; + uniqueItems?: boolean; + maxProperties?: number; + minProperties?: number; + required?: string[] | boolean; + format?: string; + properties?: { + [key: string]: ISchema; + }; + items?: ISchema | ISchema[]; + additionalItems?: ISchema; + patternProperties?: { + [key: string]: ISchema; + }; + additionalProperties?: ISchema; + // Is it in the loading state, + editable?: boolean; + /** writable property**/ // Is it visible, note: if the state is false, then the value of the field will not be submitted, and the UI will not display + visible?: boolean; + // Whether to show, note: if the state is false, then the value of the field will be submitted, the UI will not display, similar to the form hidden field + display?: boolean; + ['x-props']?: { + [name: string]: any; + }; + ['x-index']?: number; + ['x-rules']?: ValidatePatternRules; + ['x-component']?: string; + ['x-component-props']?: { + [name: string]: any; + }; + ['x-render']?: (props: T & { + renderComponent: () => React.ReactElement; + }) => React.ReactElement; + ['x-effect']?: (dispatch: (type: string, payload: any) => void, option?: object) => { + [key: string]: any; + }; + +``` + +#### IPreviewTextProps + +```typescript +interface IPreviewTextProps { + className?: React.ReactText + dataSource?: any[] + value?: any + addonBefore?: React.ReactNode + innerBefore?: React.ReactNode + addonTextBefore?: React.ReactNode + addonTextAfter?: React.ReactNode + innerAfter?: React.ReactNode + addonAfter?: React.ReactNode +} + +``` + +#### IMutators + +> The instance API created by crewikiutators is mainly used to operate field data. + +```typescript +interface IMutators { + // Changing the field value and multi parameter condition will store all parameters in values + change(...values: any[]): any + // Get focus, trigger active state change + focus(): void + // Lose focus, trigger active / visited status change + blur(): void + // Trigger current field verifier + validate(): Promise + // Whether the value of the current field exists in the values property of form + exist(index?: number | string): Boolean + + /**Array operation method**/ + + // Append data + push(value?: any): any[] + // Pop up tail data + pop(): any[] + // Insert data + insert(index: number, value: any): any[] + // Delete data + remove(index: number | string): any + // Head insertion + unshift(value: any): any[] + // Head ejection + shift(): any[] + // Move element + move($from: number, $to: number): any[] + // Move down + moveDown(index: number): any[] + // Move up + moveUp(index: number): any[] +} +``` + +#### IFieldProps + +```typescript +interface IFieldProps { + name : string // Node path + path : Array // Data path + value : V // value + errors : Array // Field error message + editable : boolean | ((name:string) => boolean) // Is it editable? + locale : Locale // i18n locale + loading : boolean // Is it in the loading state, note: if the field is in asynchronous verification, loading is true + schemaPath : Array // schema path + getSchema : (path: string) => ISchema // get schema by path + renderField : (childKey: string, reactKey: string | number) => JSX.Element | string | null + renderComponent : React.FunctionComponent | undefined>, + getOrderProperties : () => Array<{schema: ISchema, key: number, path: string, name: string }>, + mutators : IMutators, + schema : ISchema +} + +``` + +#### IConnectOptions + +```typescript + +interface IConnectOptions { + // name of value property + valueName?: string + // name of event property + eventName?: string + // default props + defaultProps?: Partial + // In some case, the value of our event function is not the first parameter of the event callback, and further customization is required. + getValueFromEvent?: (props: IFieldProps['x-props'], event: Event, ...args: any[]) => any + // props transformer + getProps?: (connectProps: IConnectProps, fieldProps: IFieldProps) => IConnectProps + // component transformer + getComponent?: ( + target: T, + connectProps: IConnectProps, + fieldProps: IFieldProps + ) => T +} + +``` \ No newline at end of file diff --git a/packages/antd/README.zh-cn.md b/packages/antd/README.zh-cn.md index c76e7635a8d..46b5dbaefbb 100644 --- a/packages/antd/README.zh-cn.md +++ b/packages/antd/README.zh-cn.md @@ -1,2 +1,2934 @@ # @uform/antd -> UForm Ant Design组件插件包 \ No newline at end of file + +### 安装 + +```bash +npm install --save @uform/antd +``` + +### 目录 + + + +- [使用方式](#使用方式) + - [`快速开始`](#快速开始) +- [Components](#components) + - [``](#SchemaForm) + - [``](#SchemaMarkupField) + - [``](#Submit) + - [``](#Reset) + - [`(即将废弃,请使用)`](<#Field(即将废弃,请使用SchemaMarkupField)>) +- [表单List](#Array-Components) + - [`array`](#array) + - [`cards`](#cards) + - [`table`](#table) +- [布局组件](#Layout-Components) + - [``](#FormCard) + - [``](#FormBlock) + - [``](#FormStep) + - [``](#FormLayout) + - [``](#FormItemGrid) + - [``](#FormTextBox) + - [``](#FormButtonGroup) + - [``](#TextButton) + - [``](#CircleButton) +- [字段类型](#Type-of-SchemaMarkupField) + - [`string`](#string) + - [`textarea`](#textarea) + - [`password`](#password) + - [`number`](#number) + - [`boolean`](#boolean) + - [`date`](#date) + - [`time`](#time) + - [`range`](#range) + - [`upload`](#upload) + - [`checkbox`](#checkbox) + - [`radio`](#radio) + - [`rating`](#rating) + - [`transfer`](#transfer) +- [API](#API) + - [`createFormActions`](#createFormActions) + - [`createAsyncFormActions`](#createAsyncFormActions) + - [`FormEffectHooks`](#FormEffectHooks) + - [`createEffectHook`](#createEffectHook) + - [`connect`](#connect) + - [`registerFormField`](#registerFormField) +- [Interfaces](#Interfaces) + - [`ButtonProps`](#ButtonProps) + - [`CardProps`](#CardProps) + - [`ICompatItemProps`](#ICompatItemProps) + - [`IFieldState`](#IFieldState) + - [`ISchemaFieldComponentProps`](#ISchemaFieldComponentProps) + - [`ISchemaVirtualFieldComponentProps`](#ISchemaVirtualFieldComponentProps) + - [`ISchemaFieldWrapper`](#ISchemaFieldWrapper) + - [`ISchemaFieldComponent`](#ISchemaFieldComponent) + - [`ISchemaVirtualFieldComponent`](#ISchemaVirtualFieldComponent) + - [`ISchemaFormRegistry`](#ISchemaFormRegistry) + - [`INextSchemaFieldProps`](#INextSchemaFieldProps) + - [`IPreviewTextProps`](#IPreviewTextProps) + - [`IMutators`](#IMutators) + - [`IFieldProps`](#IFieldProps) + - [`IConnectOptions`](#IConnectOptions) + + +### 使用方式 + +--- + +#### 快速开始 + +例子:使用 JSX 开发 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import { Button } from 'antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() + +const App = () => { + return ( + + + + + + + + + + + + + + + + item.title + }} + title="穿梭框" + name="transfer" + /> + + + + + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +例子:使用 schema 来开发 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import { Button } from 'antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() + +const App = () => { + const schema = { + type: 'object', + properties: { + radio: { + type: 'radio', + enum: ['1', '2', '3', '4'], + title: 'Radio' + }, + select: { + type: 'string', + enum: ['1', '2', '3', '4'], + title: 'Select', + required: true + }, + checkbox: { + type: 'checkbox', + enum: ['1', '2', '3', '4'], + title: 'Checkbox', + required: true + }, + textarea: { + type: 'string', + 'x-component': 'textarea', + title: 'TextArea' + }, + number: { + type: 'number', + title: '数字选择' + }, + boolean: { + type: 'boolean', + title: '开关选择' + }, + date: { + type: 'date', + title: '日期选择' + }, + daterange: { + type: 'daterange', + default: ['2018-12-19', '2018-12-19'], + title: '日期范围' + }, + year: { + type: 'year', + title: '年份' + }, + time: { + type: 'time', + title: '时间' + }, + upload: { + type: 'upload', + 'x-props': { + listType: 'card' + }, + title: '卡片上传文件' + }, + upload2: { + type: 'upload', + 'x-props': { + listType: 'dragger' + }, + title: '拖拽上传文件' + }, + upload3: { + type: 'upload', + 'x-props': { + listType: 'text' + }, + title: '普通上传文件' + }, + range: { + type: 'range', + 'x-props': { + min: 0, + max: 1024, + marks: [0, 1024] + }, + title: '范围选择' + }, + transfer: { + type: 'transfer', + enum: [ + { + key: 1, + title: '选项1' + }, + { + key: 2, + title: '选项2' + } + ], + 'x-props': { + render: (item) => item.title + }, + title: '穿梭框' + }, + rating: { + type: 'rating', + title: '等级' + }, + layout_btb_group: { + type: 'object', + 'x-component': 'button-group', + 'x-component-props': { + offset:7, + sticky: true, + }, + properties: { + submit_btn: { + type: 'object', + 'x-component': 'submit', + 'x-component-props': { + children: '提交', + }, + }, + reset_btn: { + type: 'object', + 'x-component': 'reset', + 'x-component-props': { + children: '重置', + }, + }, + } + }, + } + } + return +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Components + +--- + +#### `` + +基于@uform/react 的核心组件SchemaForm进一步扩展出来的SchemaForm组件,推荐生产环境下使用 + +```typescript +interface IAntdSchemaFormProps { + // 通过schema渲染 + schema?: ISchema; + fields?: ISchemaFormRegistry['fields']; + virtualFields?: ISchemaFormRegistry['virtualFields']; + // 全局注册Form渲染组件 + formComponent?: ISchemaFormRegistry['formComponent']; + // 全局注册FormItem渲染组件 + formItemComponent?: ISchemaFormRegistry['formItemComponent']; + // 布局设置 + layout?: FormLayout; + prefixCls?: string; + // 隐藏required的星标 + hideRequiredMark?: boolean; + colon?: boolean; + // 标签的位置 + labelAlign?: FormLabelAlign; + // 内联表单 + inline?: boolean + // 扩展class + className?: string + style?: React.CSSProperties + // label 布局控制 + labelCol?: number | { span: number; offset?: number } + // FormItem 布局控制 + wrapperCol?: number | { span: number; offset?: number } + // 自定义 placeholder + previewPlaceholder?: string | ((props: IPreviewTextProps) => string); + // 全局value + value?: Value; + // 全局defaultValue + defaultValue?: DefaultValue; + // 全局initialValues + initialValues?: DefaultValue; + // FormActions实例 + actions?: FormActions; + // IFormEffect实例 + effects?: IFormEffect; + // 表单实例 + form?: IForm; + // 表单变化回调 + onChange?: (values: Value) => void; + // form内有 `htmlType="submit"` 或 actions.submit时 触发 + onSubmit?: (values: Value) => void | Promise; + // form内有 或 actions.reset时 触发 + onReset?: () => void; + // 校验失败时触发 + onValidateFailed?: (valideted: IFormValidateResult) => void; + children?: React.ReactElement | ((form: IForm) => React.ReactElement); + //是否使用脏检查,默认会走immer精确更新 + useDirty?: boolean; + // 是否可编辑 + editable?: boolean | ((name: string) => boolean); + // 是否走悲观校验,遇到第一个校验失败就停止后续校验 + validateFirst?: boolean; +} +``` + +**用法** + +例子1: 将a-mirror的值设置为a的值。 + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + connect, + createFormActions +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() + +ReactDOM.render( + { + $('onFieldChange','a').subscribe((fieldState)=>{ + actions.setFieldState('a-mirror',state=>{ + state.value = fieldState.value + }) + }) + }}> + + + , + document.getElementById('root') +) +``` + + +例子2: 布局 + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + createFormActions, + FormLayout, + FormButtonGroup, + Submit, + Reset, +} from '@uform/antd' + +const actions = createFormActions() + +ReactDOM.render( +
    +
    常规布局
    + + + + + + + + 提交重置​ + + +
    Inline Layout
    + + + + ​ + + 提交重置​ + + +
    editable = false
    + + + + ​ + + 提交重置​ + + +
    , + document.getElementById('root') +) +``` + +#### `` + +> @uform/antd 的核心组件,用于描述表单字段 + +```typescript +interface IMarkupSchemaFieldProps { + name?: string + /** base json schema spec**/ + title?: SchemaMessage + description?: SchemaMessage + default?: any + readOnly?: boolean + writeOnly?: boolean + type?: 'string' | 'object' | 'array' | 'number' | string + enum?: Array + const?: any + multipleOf?: number + maximum?: number + exclusiveMaximum?: number + minimum?: number + exclusiveMinimum?: number + maxLength?: number + minLength?: number + pattern?: string | RegExp + maxItems?: number + minItems?: number + uniqueItems?: boolean + maxProperties?: number + minProperties?: number + required?: string[] | boolean + format?: string + /** nested json schema spec **/ + properties?: { + [key: string]: ISchema + } + items?: ISchema | ISchema[] + additionalItems?: ISchema + patternProperties?: { + [key: string]: ISchema + } + additionalProperties?: ISchema + /** extend json schema specs */ + editable?: boolean + visible?: boolean + display?: boolean + ['x-props']?: { [name: string]: any } + ['x-index']?: number + ['x-rules']?: ValidatePatternRules + ['x-component']?: string + ['x-component-props']?: { [name: string]: any } + ['x-render']?: ( + props: T & { + renderComponent: () => React.ReactElement + } + ) => React.ReactElement + ['x-effect']?: ( + dispatch: (type: string, payload: any) => void, + option?: object + ) => { [key: string]: any } +} +``` + +##### 用法 + + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + FormSlot, + Field, + createFormActions, + FormLayout, + FormButtonGroup, + Submit, + Reset, +} from '@uform/antd' + +const actions = createFormActions() + +ReactDOM.render( + +
    required
    + + +
    description
    + + +
    default value
    + + +
    readOnly
    + + +
    visible = false
    + + +
    display = false
    + + +
    editable = false
    + +
    , + document.getElementById('root') +) +``` + + +#### `` + +> Submit 组件 Props + +```typescript +interface ISubmitProps { + /** reset pops **/ + onSubmit?: ISchemaFormProps['onSubmit'] + showLoading?: boolean + /** nextBtnProps **/ + // 按钮的类型 + type?: 'primary' | 'secondary' | 'normal' + // 按钮的尺寸 + size?: 'small' | 'medium' | 'large' + // 按钮中 Icon 的尺寸,用于替代 Icon 的默认大小 + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // 当 component = 'button' 时,设置 button 标签的 type 值 + htmlType?: 'submit' | 'reset' | 'button' + // 设置标签类型 + component?: 'button' | 'a' + // 设置按钮的载入状态 + loading?: boolean + // 是否为幽灵按钮 + ghost?: true | false | 'light' | 'dark' + // 是否为文本按钮 + text?: boolean + // 是否为警告按钮 + warning?: boolean + // 是否禁用 + disabled?: boolean + // 点击按钮的回调 + onClick?: (e: {}) => void + // 在Button组件使用component属性值为a时有效,代表链接页面的URL + href?: string + // 在Button组件使用component属性值为a时有效,代表何处打开链接文档 + target?: string +} +``` + +#### `` + +> Reset 组件 Props + +```typescript +interface IResetProps { + /** reset pops **/ + forceClear?: boolean + validate?: boolean + /** nextBtnProps **/ + // 按钮的类型 + type?: 'primary' | 'secondary' | 'normal' + // 按钮的尺寸 + size?: 'small' | 'medium' | 'large' + // 按钮中 Icon 的尺寸,用于替代 Icon 的默认大小 + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // 当 component = 'button' 时,设置 button 标签的 type 值 + htmlType?: 'submit' | 'reset' | 'button' + // 设置标签类型 + component?: 'button' | 'a' + // 设置按钮的载入状态 + loading?: boolean + // 是否为幽灵按钮 + ghost?: true | false | 'light' | 'dark' + // 是否为文本按钮 + text?: boolean + // 是否为警告按钮 + warning?: boolean + // 是否禁用 + disabled?: boolean + // 点击按钮的回调 + onClick?: (e: {}) => void + // 在Button组件使用component属性值为a时有效,代表链接页面的URL + href?: string + // 在Button组件使用component属性值为a时有效,代表何处打开链接文档 + target?: string +} +``` + +### Array Components + +#### array + +```jsx +import React, { useState, useEffect } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/antd' +import 'antd/dist/antd.css' +import Printer from '@uform/printer' + +const App = () => { + const [value, setValues] = useState({}) + useEffect(() => { + setTimeout(() => { + setValues({ + array: [{ array2: [{ aa: '123', bb: '321' }] }] + }) + }, 1000) + }, []) + return ( + + console.log(v)}> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 提交 + 重置 + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### cards + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/antd' +import 'antd/dist/antd.css' +import Printer from '@uform/printer' + +const App = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### table + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/antd' +import 'antd/dist/antd.css' +import Printer from '@uform/printer' + +const App = () => ( + + + + Hello worldasdasdasdasd + }, + operationsWidth: 300 + }} + > + + + + + + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +### Layout Components + + +#### `` + +> FormCard 组件 Props, 完全继承自 [CardProps](#CardProps)。 +> FormCard与[FormBlock](#FormBlock) 唯一区别是样式上是否有框 + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { FormCard, SchemaMarkupField as Field } from '@uform/antd' +import 'antd/dist/antd.css' + +const App = () => ( + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormBlock 组件 Props, 完全继承自 [CardProps](#CardProps) + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { FormBlock, SchemaMarkupField as Field } from '@uform/antd' +import 'antd/dist/antd.css' + +const App = () => ( + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormStep 组件 Props + +```typescript +interface IFormStep { + dataSource: StepItemProps[] + /** next step props**/ + // 当前步骤 + current?: number + // 展示方向 + direction?: 'hoz' | 'ver' + // 横向布局时的内容排列 + labelPlacement?: 'hoz' | 'ver' + // 类型 + shape?: 'circle' | 'arrow' | 'dot' + // 是否只读模式 + readOnly?: boolean + // 是否开启动效 + animation?: boolean + // 自定义样式名 + className?: string + // StepItem 的自定义渲染 + itemRender?: (index: number, status: string) => React.ReactNode +} +``` + +**用法** + +```jsx +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + FormEffectHooks, + createFormActions, + FormGridRow, + FormItemGrid, + FormGridCol, + FormPath, + FormLayout, + FormBlock, + FormCard, + FormTextBox, + FormStep +} from '@uform/antd' +import { Button } from 'antd' +import 'antd/dist/antd.css' + +const { onFormInit$ } = FormEffectHooks + +const actions = createFormActions() + +let cache = {} + +export default () => ( + { + console.log('提交') + console.log(values) + }} + actions={actions} + labelCol={{ span: 8 }} + wrapperCol={{ span: 6 }} + validateFirst + effects={({ setFieldState, getFormGraph }) => { + onFormInit$().subscribe(() => { + setFieldState('col1', state => { + state.visible = false + }) + }) + }} + > + + + + + + + + + + + + 提交 + + + + + + +) +``` + +#### `` + +> FormLayout 组件 Props + +```typescript +interface IFormItemTopProps { + inline?: boolean + className?: string + style?: React.CSSProperties + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} +``` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/antd' +import { Button } from 'antd' +import Printer from '@uform/printer' +import 'antd/dist/antd.css' +const App = () => ( + + + + + + + + + 提交重置​ + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormItemGrid 组件 Props + +```typescript +interface IFormItemGridProps { + cols?: Array + gutter?: number + /** next Form.Item props**/ + // 样式前缀 + prefix?: string + + // label 标签的文本 + label?: React.ReactNode + + // label 标签布局,通 `` 组件,设置 span offset 值,如 {span: 8, offset: 16},该项仅在垂直表单有效 + labelCol?: {} + + // 需要为输入控件设置布局样式时,使用该属性,用法同 labelCol + wrapperCol?: {} + + // 自定义提示信息,如不设置,则会根据校验规则自动生成. + help?: React.ReactNode + + // 额外的提示信息,和 help 类似,当需要错误信息和提示文案同时出现时,可以使用这个。 位于错误信息后面 + extra?: React.ReactNode + + // 校验状态,如不设置,则会根据校验规则自动生成 + validateState?: 'error' | 'success' | 'loading' + + // 配合 validateState 属性使用,是否展示 success/loading 的校验状态图标, 目前只有Input支持 + hasFeedback?: boolean + + // 自定义内联样式 + style?: React.CSSProperties + + // node 或者 function(values) + children?: React.ReactNode | (() => void) + + // 单个 Item 的 size 自定义,优先级高于 Form 的 size, 并且当组件与 Item 一起使用时,组件自身设置 size 属性无效。 + size?: 'large' | 'small' | 'medium' + + // 标签的位置 + labelAlign?: 'top' | 'left' | 'inset' + + // 标签的左右对齐方式 + labelTextAlign?: 'left' | 'right' + + // 扩展class + className?: string + + // [表单校验] 不能为空 + required?: boolean + + // required 的星号是否显示 + asterisk?: boolean + + // required 自定义错误信息 + requiredMessage?: string + + // required 自定义触发方式 + requiredTrigger?: string | Array + + // [表单校验] 最小值 + min?: number + + // [表单校验] 最大值 + max?: number + + // min/max 自定义错误信息 + minmaxMessage?: string + + // min/max 自定义触发方式 + minmaxTrigger?: string | Array + + // [表单校验] 字符串最小长度 / 数组最小个数 + minLength?: number + + // [表单校验] 字符串最大长度 / 数组最大个数 + maxLength?: number + + // minLength/maxLength 自定义错误信息 + minmaxLengthMessage?: string + + // minLength/maxLength 自定义触发方式 + minmaxLengthTrigger?: string | Array + + // [表单校验] 字符串精确长度 / 数组精确个数 + length?: number + + // length 自定义错误信息 + lengthMessage?: string + + // length 自定义触发方式 + lengthTrigger?: string | Array + + // 正则校验 + pattern?: any + + // pattern 自定义错误信息 + patternMessage?: string + + // pattern 自定义触发方式 + patternTrigger?: string | Array + + // [表单校验] 四种常用的 pattern + format?: 'number' | 'email' | 'url' | 'tel' + + // format 自定义错误信息 + formatMessage?: string + + // format 自定义触发方式 + formatTrigger?: string | Array + + // [表单校验] 自定义校验函数 + validator?: () => void + + // validator 自定义触发方式 + validatorTrigger?: string | Array + + // 是否修改数据时自动触发校验 + autoValidate?: boolean +} +``` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/antd' +import { Button } from 'antd' +import Printer from '@uform/printer' +import 'antd/dist/antd.css' + +const App = () => ( + + console.log(v)}> + + + + + + + + + + + + ​提交重置​ + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormTextBox 组件 Props + +```typescript +interface IFormTextBox { + text?: string + gutter?: number + title?: React.ReactText + description?: React.ReactText +} +``` + +**用法** + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormTextBox, + FormCard, + FormLayout +} from '@uform/antd' +import { Button } from 'antd' +import Printer from '@uform/printer' +import 'antd/dist/antd.css' + +const App = () => { + return ( + + console.log(v)}> + + + + + + + + + + + + ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormButtonGroup 组件 Props + +```typescript +interface IFormButtonGroupProps { + sticky?: boolean + style?: React.CSSProperties + itemStyle?: React.CSSProperties + className?: string + align?: 'left' | 'right' | 'start' | 'end' | 'top' | 'bottom' | 'center' + triggerDistance?: number + zIndex?: number + span?: ColSpanType + offset?: ColSpanType +} +``` + +**用法** + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/antd' +import { Button } from 'antd' +import Printer from '@uform/printer' +import 'antd/dist/antd.css' + +const App = () => { + const [state, setState] = useState({ editable: true }) + return ( + + console.log(v)}> +
    normal
    + + ​提交重置​ + +
    sticky
    + + ​提交​ + + 重置​ + +
    +
    + ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> TextButton 组件 Props, 完全继承自 [ButtonProps](#ButtonProps) + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { TextButton } from '@uform/antd' +import 'antd/dist/antd.css' + +const App = () => ( + + content + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> CircleButton 组件 Props, 完全继承自 [ButtonProps](#ButtonProps) + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { CircleButton } from '@uform/antd' +import 'antd/dist/antd.css' + +const App = () => ( + + ok + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `(即将废弃,请使用SchemaMarkupField)` + +> 即将废弃,请使用[SchemaMarkupField](#SchemaMarkupField) + +### Type of SchemaMarkupField + +#### string + +* Schema Type : `string` +* Schema UI Component: Fusion-Next ``, ``, `` + +```typescript +interface IPasswordProps { + checkStrength: boolean + /** next input props **/ + // 当前值 + value?: string | number + + // 初始化值 + defaultValue?: string | number + + // 发生改变的时候触发的回调 + onChange?: (value: string, e: React.ChangeEvent) => void + + // 键盘按下的时候触发的回调 + onKeyDown?: (e: React.KeyboardEvent, opts: {}) => void + + // 禁用状态 + disabled?: boolean + + // 最大长度 + maxLength?: number + + // 是否展现最大长度样式 + hasLimitHint?: boolean + + // 当设置了maxLength时,是否截断超出字符串 + cutString?: boolean + + // 只读 + readOnly?: boolean + + // onChange返回会自动去除头尾空字符 + trim?: boolean + + // 输入提示 + placeholder?: string + + // 获取焦点时候触发的回调 + onFocus?: () => void + + // 失去焦点时候触发的回调 + onBlur?: () => void + + // 自定义字符串计算长度方式 + getValueLength?: (value: string) => number + + // 自定义class + className?: string + + // 自定义内联样式 + style?: React.CSSProperties + + // 原生type + htmlType?: string + + // name + name?: string + + // 状态 + state?: 'error' | 'loading' | 'success' + + // label + label?: React.ReactNode + + // 是否出现clear按钮 + hasClear?: boolean + + // 是否有边框 + hasBorder?: boolean + + // 尺寸 + size?: 'small' | 'medium' | 'large' + + // 按下回车的回调 + onPressEnter?: () => void + + // 水印 (Icon的type类型,和hasClear占用一个地方) + hint?: string + + // 文字前附加内容 + innerBefore?: React.ReactNode + + // 文字后附加内容 + innerAfter?: React.ReactNode + + // 输入框前附加内容 + addonBefore?: React.ReactNode + + // 输入框后附加内容 + addonAfter?: React.ReactNode + + // 输入框前附加文字 + addonTextBefore?: React.ReactNode + + // 输入框后附加文字 + addonTextAfter?: React.ReactNode + + // (原生input支持) + autoComplete?: string + + // 自动聚焦(原生input支持) + autoFocus?: boolean +} +``` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### number + +* Schema Type : `number` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### boolean + +* Schema Type : `boolean` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### date + +* Schema Type : `date` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### time + +* Schema Type : `time` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### range + +* Schema Type : `range` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### upload + +* Schema Type : `upload` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### checkbox + +* Schema Type : `checkbox` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### radio + +* Schema Type : `radio` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### rating + +* Schema Type : `rating` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### transfer + +* Schema Type : `transfer` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + item.title + }} + x-component-props={{ + showSearch: true + }} + /> + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### API + +> 整体完全继承@uform/react, 下面只列举@uform/antd 的特有 API + +--- + +#### `createFormActions` + +> 创建一个 [IFormActions](#IFormActions) 实例 + +**签名** + +```typescript +createFormActions(): IFormActions +``` + +**用法** + +```typescript +import { createFormActions } from '@uform/antd' + +const actions = createFormActions() +console.log(actions.getFieldValue('username')) +``` + +#### `createAsyncFormActions` + +> 创建一个 [IFormAsyncActions](#IFormAsyncActions) 实例,成员方法 同[IFormActions](#IFormActions), +> 但是调用 API 返回的结果是异步的(promise)。 + +**签名** + +```typescript +createAsyncFormActions(): IFormAsyncActions +``` + +**用法** + +```typescript +import { createAsyncFormActions } from '@uform/antd' + +const actions = createAsyncFormActions() +actions.getFieldValue('username').then(val => console.log(val)) +``` + +#### `FormEffectHooks` + +> 返回包含所有 UForm 生命周期的钩子函数,可以被监听消费 + +**用法** + +```typescript +import SchemaForm, { FormEffectHooks } from '@uform/antd' +const { + /** + * Form LifeCycle + **/ + onFormWillInit$, // 表单预初始化触发 + onFormInit$, // 表单初始化触发 + onFormChange$, // 表单变化时触发 + onFormInputChange$, // 表单事件触发时触发,用于只监控人工操作 + onFormInitialValueChange$, // 表单初始值变化时触发 + onFormReset$, // 表单重置时触发 + onFormSubmit$, // 表单提交时触发 + onFormSubmitStart$, // 表单提交开始时触发 + onFormSubmitEnd$, // 表单提交结束时触发 + onFormMount$, // 表单挂载时触发 + onFormUnmount$, // 表单卸载时触发 + onFormValidateStart$, // 表单校验开始时触发 + onFormValidateEnd$, //表单校验结束时触发 + onFormValuesChange$, // 表单值变化时触发 + /** + * FormGraph LifeCycle + **/ + onFormGraphChange$, // 表单观察者树变化时触发 + /** + * Field LifeCycle + **/ + onFieldWillInit$, // 字段预初始化时触发 + onFieldInit$, // 字段初始化时触发 + onFieldChange$, // 字段变化时触发 + onFieldMount$, // 字段挂载时触发 + onFieldUnmount$, // 字段卸载时触发 + onFieldInputChange$, // 字段事件触发时触发,用于只监控人工操作 + onFieldValueChange$, // 字段值变化时触发 + onFieldInitialValueChange$ // 字段初始值变化时触发 +} = FormEffectHooks + +const App = () => { + return ( + { + onFormInit$().subscribe(() => { + console.log('初始化') + }) + }} + > + ... + + ) +} +``` + +#### createEffectHook + +> 自定义 hook + +**Usage** + +```jsx +import SchemaForm, { createEffectHook, createFormActions } from '@uform/antd' + +const actions = createFormActions() +const diyHook1$ = createEffectHook('diy1') +const diyHook2$ = createEffectHook('diy2') + +const App = () => { + return ( + { + diyHook1$().subscribe(payload => { + console.log('diy1 hook triggered', payload) + }) + + diyHook2$().subscribe(payload => { + console.log('diy2 hook triggered', payload) + }) + }} + > + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### connect + +> 包装字段组件,让字段组件只需要支持value/defaultValue/onChange属性即可快速接入表单 + +```typescript +type Connect = >(options?: IConnectOptions) => +(Target: T) => React.PureComponent +``` +**用法** + +```typescript +import {registerFormField,connect} from '@uform/antd' + +registerFormField( + 'string', + connect()(props => ) +) +``` + +#### registerFormField + +```typescript +type registerFormField( + name : string, //类型名称 + component : React.ComponentType, //类型组件 + noMiddleware: boolean //是否被middleware包装 +) +``` + +**用法** + +```jsx + +import SchemaForm, { SchemaMarkupField as Field, registerFormField, connect, createFormActions } from '@uform/antd' + +registerFormField( + 'custom-string', + connect()(props => ) +) +const actions = createFormActions() + +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Interfaces + +> 整体完全继承@uform/react, 下面只列举@uform/antd 的特有的 Interfaces + +--- + +#### IForm + +> 通过 createForm 创建出来的 Form 实例对象 API + +```typescript +interface IForm { + /* + * 表单提交,如果回调参数返回Promise, + * 那么整个提交流程会hold住,同时loading为true, + * 等待Promise resolve才触发表单onFormSubmitEnd事件,同时loading为false + */ + submit( + onSubmit?: (values: IFormState['values']) => any | Promise + ): Promise<{ + validated: IFormValidateResult + payload: any //onSubmit回调函数返回值 + }> + + /* + * 清空错误消息,可以通过传FormPathPattern来批量或精确控制要清空的字段, + * 比如clearErrors("*(aa,bb,cc)") + */ + clearErrors: (pattern?: FormPathPattern) => void + + /* + * 获取状态变化情况,主要用于在表单生命周期钩子内判断当前生命周期中有哪些状态发生了变化, + * 比如hasChanged(state,'value.aa') + */ + hasChanged( + target: IFormState | IFieldState | IVirtualFieldState, + path: FormPathPattern + ): boolean + + /* + * 重置表单 + */ + reset(options?: { + //强制清空 + forceClear?: boolean + //强制校验 + validate?: boolean + //重置范围,用于批量或者精确控制要重置的字段 + selector?: FormPathPattern + }): Promise + + /* + * 校验表单 + */ + validate( + path?: FormPathPattern, + options?: { + //是否悲观校验,如果当前字段遇到第一个校验错误则停止后续校验流程 + first?: boolean + } + ): Promise + + /* + * 设置表单状态 + */ + setFormState( + //操作回调 + callback?: (state: IFormState) => any, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取表单状态 + */ + getFormState( + //transformer + callback?: (state: IFormState) => any + ): any + + /* + * 设置字段状态 + */ + setFieldState( + //字段路径 + path: FormPathPattern, + //操作回调 + callback?: (state: IFieldState) => void, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取字段状态 + */ + getFieldState( + //字段路径 + path: FormPathPattern, + //transformer + callback?: (state: IFieldState) => any + ): any + + /* + * 注册字段 + */ + registerField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段值 + value?: any + //字段多参值 + values?: any[] + //字段初始值 + initialValue?: any + //字段扩展属性 + props?: any + //字段校验规则 + rules?: ValidatePatternRules[] + //字段是否必填 + required?: boolean + //字段是否可编辑 + editable?: boolean + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IField + + /* + * 注册虚拟字段 + */ + registerVirtualField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段扩展属性 + props?: any + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IVirtualField + + /* + * 创建字段数据操作器,后面会详细解释返回的API + */ + createMutators(field: IField): IMutators + + /* + * 获取表单观察者树 + */ + getFormGraph(): IFormGraph + + /* + * 设置表单观察者树 + */ + setFormGraph(graph: IFormGraph): void + + /* + * 监听表单生命周期 + */ + subscribe( + callback?: ({ type, payload }: { type: string; payload: any }) => void + ): number + + /* + * 取消监听表单生命周期 + */ + unsubscribe(id: number): void + + /* + * 触发表单自定义生命周期 + */ + notify: (type: string, payload?: T) => void + + /* + * 设置字段值 + */ + setFieldValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段值 + */ + getFieldValue(path?: FormPathPattern): any + + /* + * 设置字段初始值 + */ + setFieldInitialValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段初始值 + */ + getFieldInitialValue(path?: FormPathPattern): any +} +``` + +#### ButtonProps + +```typescript +interface ButtonProps { + href: string; + target?: string; + onClick?: React.MouseEventHandler; + htmlType?: ButtonHTMLType; + onClick?: React.MouseEventHandler; +} +``` + +#### CardProps + +```typescript +interface CardProps extends HTMLAttributesWeak, CommonProps { + prefixCls?: string; + title?: React.ReactNode; + extra?: React.ReactNode; + bordered?: boolean; + headStyle?: React.CSSProperties; + bodyStyle?: React.CSSProperties; + style?: React.CSSProperties; + loading?: boolean; + noHovering?: boolean; + hoverable?: boolean; + children?: React.ReactNode; + id?: string; + className?: string; + size?: CardSize; + type?: CardType; + cover?: React.ReactNode; + actions?: React.ReactNode[]; + tabList?: CardTabListType[]; + tabBarExtraContent?: React.ReactNode | null; + onTabChange?: (key: string) => void; + activeTabKey?: string; + defaultActiveTabKey?: string; +} +``` + +#### ICompatItemProps + +```typescript +interface ICompatItemProps + extends Exclude, + Partial { + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} +``` + + +#### IFieldState + +```typescript +interface IFieldState { + /**只读属性**/ + + //状态名称,FieldState + displayName?: string + //数据路径 + name: string + //节点路径 + path: string + //是否已经初始化 + initialized: boolean + //是否处于原始态,只有value===intialValues时的时候该状态为true + pristine: boolean + //是否处于合法态,只要errors长度大于0的时候valid为false + valid: boolean + //是否处于非法态,只要errors长度大于0的时候valid为true + invalid: boolean + //是否处于校验态 + validating: boolean + //是否被修改,如果值发生变化,该属性为true,同时在整个字段的生命周期内都会为true + modified: boolean + //是否被触碰 + touched: boolean + //是否被激活,字段触发onFocus事件的时候,它会被触发为true,触发onBlur时,为false + active: boolean + //是否访问过,字段触发onBlur事件的时候,它会被触发为true + visited: boolean + + /**可写属性**/ + + //是否可见,注意:该状态如果为false,那么字段的值不会被提交,同时UI不会显示 + visible: boolean + //是否展示,注意:该状态如果为false,那么字段的值会提交,UI不会展示,类似于表单隐藏域 + display: boolean + //是否可编辑 + editable: boolean + //是否处于loading状态,注意:如果字段处于异步校验时,loading为true + loading: boolean + //字段多参值,比如字段onChange触发时,给事件回调传了多参数据,那么这里会存储所有参数的值 + values: any[] + //字段错误消息 + errors: string[] + //字段告警消息 + warnings: string[] + //字段值,与values[0]是恒定相等 + value: any + //初始值 + initialValue: any + //校验规则,具体类型描述参考后面文档 + rules: ValidatePatternRules[] + //是否必填 + required: boolean + //是否挂载 + mounted: boolean + //是否卸载 + unmounted: boolean + //字段扩展属性 + props: FieldProps +} +``` + + +#### ISchemaFieldComponentProps + +```typescript +interface ISchemaFieldComponentProps extends IFieldState { + schema: Schema + mutators: IMutators + form: IForm + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + +#### ISchemaVirtualFieldComponentProps + +```typescript +interface ISchemaVirtualFieldComponentProps extends IVirtualFieldState { + schema: Schema + form: IForm + children: React.ReactElement[] + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + +#### ISchemaFieldWrapper + +```typescript +interface ISchemaFieldWrapper { + (Traget: ISchemaFieldComponent): + | React.FC + | React.ClassicComponent +} +``` + +#### ISchemaFieldComponent + +```typescript +declare type ISchemaFieldComponent = ComponentWithStyleComponent< + ISchemaFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} +``` + +#### ISchemaVirtualFieldComponent + +```typescript +declare type ISchemaVirtualFieldComponent = ComponentWithStyleComponent< + ISchemaVirtualFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} +``` + +#### ISchemaFormRegistry + +```typescript +interface ISchemaFormRegistry { + fields: { + [key: string]: ISchemaFieldComponent + } + virtualFields: { + [key: string]: ISchemaVirtualFieldComponent + } + wrappers?: ISchemaFieldWrapper[] + formItemComponent: React.JSXElementConstructor + formComponent: string | React.JSXElementConstructor +} +``` + +#### INextSchemaFieldProps + +```typescript +interface INextSchemaFieldProps { + name?: string; + /** ISchema **/ + title?: SchemaMessage; + description?: SchemaMessage; + default?: any; + readOnly?: boolean; + writeOnly?: boolean; + type?: 'string' | 'object' | 'array' | 'number' | string; + enum?: Array; + const?: any; + multipleOf?: number; + maximum?: number; + exclusiveMaximum?: number; + minimum?: number; + exclusiveMinimum?: number; + maxLength?: number; + minLength?: number; + pattern?: string | RegExp; + maxItems?: number; + minItems?: number; + uniqueItems?: boolean; + maxProperties?: number; + minProperties?: number; + required?: string[] | boolean; + format?: string; + properties?: { + [key: string]: ISchema; + }; + items?: ISchema | ISchema[]; + additionalItems?: ISchema; + patternProperties?: { + [key: string]: ISchema; + }; + additionalProperties?: ISchema; + editable?: boolean; + visible?: boolean; + display?: boolean; + ['x-props']?: { + [name: string]: any; + }; + ['x-index']?: number; + ['x-rules']?: ValidatePatternRules; + ['x-component']?: string; + ['x-component-props']?: { + [name: string]: any; + }; + ['x-render']?: (props: T & { + renderComponent: () => React.ReactElement; + }) => React.ReactElement; + ['x-effect']?: (dispatch: (type: string, payload: any) => void, option?: object) => { + [key: string]: any; + }; + +``` + +#### IPreviewTextProps + +```typescript +interface IPreviewTextProps { + className?: React.ReactText + dataSource?: any[] + value?: any + addonBefore?: React.ReactNode + innerBefore?: React.ReactNode + addonTextBefore?: React.ReactNode + addonTextAfter?: React.ReactNode + innerAfter?: React.ReactNode + addonAfter?: React.ReactNode +} + +``` + +#### IMutators + +```typescript +interface IMutators { + change: (value: V)=> void,//改变当前字段值 + dispatch: (name: string, payload : any) => void,//触发effect事件 + errors: (errors: string | Array, ...formatValues: Array) => void,//设置当前字段的错误消息 + push(value: V),//对当前字段的值做push操作 + pop(),//对当前字段的值做pop操作 + insert(index: number,value: V),//对当前字段的值做insert操作 + remove(name : string),//对当前字段的值做remove操作 + unshift(value : V),//对当前字段值做unshift操作 + shift(),//对当前字段值做shift操作 + move(fromIndex: number, toIndex: number)//对当前字段值做move操作 +} +``` + +#### IFieldProps + +```typescript +interface IFieldProps { + name : string //字段数据路径 + path : Array //字段数组数据路径 + value : V //字段值 + errors : Array //字段错误消息集合 + editable : boolean | ((name:string) => boolean) //字段是否可编辑 + locale : Locale //国际化文案对象 + loading : boolean //是否处于加载态 + schemaPath : Array //schema path,考虑到有些schema其实是不占数据路径的,所以这个路径是真实路径 + getSchema : (path: string) => ISchema //根据路径获取schema + renderField : (childKey: string, reactKey: string | number) => JSX.Element | string | null //根据childKey渲染当前字段的子字段 + renderComponent : React.FunctionComponent | undefined>,//渲染当前字段的组件,对于x-render来说,可以借助它快速实现渲染包装功能 + getOrderProperties : () => Array<{schema: ISchema, key: number, path: string, name: string }>,//根据properties里字段的x-index值求出排序后的properties + mutators : Mutators,//数据操作对象 + schema : ISchema +} + +``` + +```typescript + +interface IConnectOptions { + //控制表单组件 + valueName?: string + //事件名称 + eventName?: string + //默认props + defaultProps?: Partial + //取值函数,有些场景我们的事件函数取值并不是事件回调的第一个参数,需要做进一步的定制 + getValueFromEvent?: (props: IFieldProps['x-props'], event: Event, ...args: any[]) => any + //字段组件props transformer + getProps?: (connectProps: IConnectProps, fieldProps: IFieldProps) => IConnectProps + //字段组件component transformer + getComponent?: ( + target: T, + connectProps: IConnectProps, + fieldProps: IFieldProps + ) => T +} + +``` \ No newline at end of file diff --git a/packages/antd/src/components/Button.tsx b/packages/antd/src/components/Button.tsx index 5d07468e830..d8f707ce804 100644 --- a/packages/antd/src/components/Button.tsx +++ b/packages/antd/src/components/Button.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { FormSpy, LifeCycleTypes } from '@uform/react-schema-renderer' +import { FormSpy, LifeCycleTypes, createVirtualBox } from '@uform/react-schema-renderer' import { Button } from 'antd' import { ButtonProps } from 'antd/lib/button' import { ISubmitProps, IResetProps } from '../types' @@ -85,3 +85,8 @@ export const Reset: React.FC = ({ ) } + +createVirtualBox('reset', Reset) +createVirtualBox('text-button', TextButton) +createVirtualBox('submit', Submit) +createVirtualBox('circle-button', CircleButton) diff --git a/packages/antd/src/components/FormButtonGroup.tsx b/packages/antd/src/components/FormButtonGroup.tsx index 7899c572d23..850e2a222a0 100644 --- a/packages/antd/src/components/FormButtonGroup.tsx +++ b/packages/antd/src/components/FormButtonGroup.tsx @@ -5,6 +5,7 @@ import cls from 'classnames' import styled from 'styled-components' import { useFormItem } from '../compat/context' import { IFormButtonGroupProps } from '../types' +import { createVirtualBox } from '@uform/react-schema-renderer' export interface IOffset { top: number | string @@ -153,3 +154,8 @@ export const FormButtonGroup = styled( } } ` + +createVirtualBox>( + 'button-group', + FormButtonGroup, +) \ No newline at end of file diff --git a/packages/antd/src/types.ts b/packages/antd/src/types.ts index 581c913bcd9..933ca9e03c5 100644 --- a/packages/antd/src/types.ts +++ b/packages/antd/src/types.ts @@ -14,10 +14,10 @@ import { StyledComponent } from 'styled-components' type ColSpanType = number | string -export type IAntdSchemaFormProps = ISchemaFormProps & - FormProps & +export type IAntdSchemaFormProps = FormProps & IFormItemTopProps & - PreviewTextConfigProps + PreviewTextConfigProps & + ISchemaFormProps export type IAntdSchemaFieldProps = IMarkupSchemaFieldProps diff --git a/packages/next/README.md b/packages/next/README.md index 35f5c209246..a55b5804149 100644 --- a/packages/next/README.md +++ b/packages/next/README.md @@ -1,2 +1,2970 @@ # @uform/next -> UForm Fusion Next 组件插件包 + +### Install + +```bash +npm install --save @uform/next +``` + +### Table Of Contents + + + +- [Quick-Start](#Quick-Start) +- [Components](#components) + - [``](#SchemaForm) + - [``](#SchemaMarkupField) + - [``](#Submit) + - [``](#Reset) + - [`(deprecated,please use )`](#) +- [Form List](#Array-Components) + - [`array`](#array) + - [`cards`](#cards) + - [`table`](#table) +- [Layout Components](#Layout-Components) + - [``](#FormCard) + - [``](#FormBlock) + - [``](#FormStep) + - [``](#FormLayout) + - [``](#FormItemGrid) + - [``](#FormTextBox) + - [``](#FormButtonGroup) + - [``](#TextButton) + - [``](#CircleButton) +- [Type of SchemaMarkupField](#Type-of-SchemaMarkupField) + - [`string`](#string) + - [`textarea`](#textarea) + - [`password`](#password) + - [`number`](#number) + - [`boolean`](#boolean) + - [`date`](#date) + - [`time`](#time) + - [`range`](#range) + - [`upload`](#upload) + - [`checkbox`](#checkbox) + - [`radio`](#radio) + - [`rating`](#rating) + - [`transfer`](#transfer) +- [API](#API) + - [`createFormActions`](#createFormActions) + - [`createAsyncFormActions`](#createAsyncFormActions) + - [`FormEffectHooks`](#FormEffectHooks) + - [`createEffectHook`](#createEffectHook) + - [`connect`](#connect) + - [`registerFormField`](#registerFormField) +- [Interfaces](#Interfaces) + - [`ButtonProps`](#ButtonProps) + - [`CardProps`](#CardProps) + - [`ICompatItemProps`](#ICompatItemProps) + - [`IFieldState`](#IFieldState) + - [`ISchemaFieldComponentProps`](#ISchemaFieldComponentProps) + - [`ISchemaVirtualFieldComponentProps`](#ISchemaVirtualFieldComponentProps) + - [`ISchemaFieldWrapper`](#ISchemaFieldWrapper) + - [`ISchemaFieldComponent`](#ISchemaFieldComponent) + - [`ISchemaVirtualFieldComponent`](#ISchemaVirtualFieldComponent) + - [`ISchemaFormRegistry`](#ISchemaFormRegistry) + - [`INextSchemaFieldProps`](#INextSchemaFieldProps) + - [`IPreviewTextProps`](#IPreviewTextProps) + - [`IMutators`](#IMutators) + - [`IFieldProps`](#IFieldProps) + - [`IConnectOptions`](#IConnectOptions) + + +### Quick-Start + +--- + +Example:develop with JSX + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import { Button } from '@alifd/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() + +const App = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +Example:develop with JSON Schema + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import { Button } from '@alifd/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() + +const App = () => { + const schema = { + type: 'object', + properties: { + radio: { + type: 'radio', + enum: ['1', '2', '3', '4'], + title: 'Radio' + }, + select: { + type: 'string', + enum: ['1', '2', '3', '4'], + title: 'Select', + required: true + }, + checkbox: { + type: 'checkbox', + enum: ['1', '2', '3', '4'], + title: 'Checkbox', + required: true + }, + textarea: { + type: 'string', + 'x-component': 'textarea', + title: 'TextArea' + }, + number: { + type: 'number', + title: 'number' + }, + boolean: { + type: 'boolean', + title: 'boolean' + }, + date: { + type: 'date', + title: 'date' + }, + daterange: { + type: 'daterange', + default: ['2018-12-19', '2018-12-19'], + title: 'daterange' + }, + year: { + type: 'year', + title: 'year' + }, + time: { + type: 'time', + title: 'time' + }, + upload: { + type: 'upload', + 'x-props': { + listType: 'card' + }, + title: 'upload(card)' + }, + upload2: { + type: 'upload', + 'x-props': { + listType: 'dragger' + }, + title: 'uplaod(dragger)' + }, + upload3: { + type: 'upload', + 'x-props': { + listType: 'text' + }, + title: 'upload(text)' + }, + range: { + type: 'range', + 'x-props': { + min: 0, + max: 1024, + marks: [0, 1024] + }, + title: 'range' + }, + transfer: { + type: 'transfer', + enum: [ + { + value: 1, + label: 'opt1' + }, + { + value: 2, + label: 'opt2' + } + ], + title: 'transfer' + }, + rating: { + type: 'rating', + title: 'rating' + }, + layout_btb_group: { + type: 'object', + 'x-component': 'button-group', + 'x-component-props': { + offset:7, + sticky: true, + }, + properties: { + submit_btn: { + type: 'object', + 'x-component': 'submit', + 'x-component-props': { + children: 'Submit', + }, + }, + reset_btn: { + type: 'object', + 'x-component': 'reset', + 'x-component-props': { + children: 'Reset', + }, + }, + } + }, + } + } + return +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Components + +--- + +#### `` + +Base on `` of @uform/react-schema-renderer. Recommended for production environments. + +```typescript +interface INextSchemaFormProps { + // render by schema + schema?: ISchema; + fields?: ISchemaFormRegistry['fields']; + virtualFields?: ISchemaFormRegistry['virtualFields']; + // pre-registered Form Component + formComponent?: ISchemaFormRegistry['formComponent']; + // pre-registered FormItem Component + formItemComponent?: ISchemaFormRegistry['formItemComponent']; + // label column settiing + labelCol?: number | { span: number; offset?: number } + // FormItem column settiing + wrapperCol?: number | { span: number; offset?: number } + // custom placeholder when preivew + previewPlaceholder?: string | ((props: IPreviewTextProps) => string); + // prefix + prefix?: string; + // is it inline + inline?: boolean; + // The size of a single Item is customized, and takes precedence over the size of the Form, and when a component is used with an Item, the component itself does not set the size property. + size?: 'large' | 'medium' | 'small'; + // position of label + labelAlign?: 'top' | 'left' | 'inset'; + // aligment of label + labelTextAlign?: 'left' | 'right'; + // labelCol of FormItem + labelCol?: {}; + // wrapperCol of FormItem + wrapperCol?: {}; + children?: any; + className?: string; + style?: React.CSSProperties; + // type of component + component?: string | (() => void); + // form state value + value?: Value; + // form state defaultValue + defaultValue?: DefaultValue; + // form state initialValues + initialValues?: DefaultValue; + // FormActions instance + actions?: FormActions; + // IFormEffect instance + effects?: IFormEffect; + // form instance + form?: IForm; + // Form change event callback + onChange?: (values: Value) => void; + // triggered by `htmlType="submit"` or actions.submit时 + onSubmit?: (values: Value) => void | Promise; + // triggered by or actions.reset + onReset?: () => void; + // Form verification failure event callback + onValidateFailed?: (valideted: IFormValidateResult) => void; + children?: React.ReactElement | ((form: IForm) => React.ReactElement); + // Whether to use the dirty check, the default will go immer accurate update + useDirty?: boolean; + // Is it editable, overall control in the Form dimension + editable?: boolean | ((name: string) => boolean); + // Whether to go pessimistic check, stop the subsequent check when the first check fails + validateFirst?: boolean; +} +``` + +**Usage** + +Example1: Sync value of a and a-mirror + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + registerFormField, + Field, + connect, + createFormActions +} from '@uform/next' + +const actions = createFormActions() + +registerFormField( + 'string', + connect()(props => ) +) + +ReactDOM.render( + { + $('onFieldChange','a').subscribe((fieldState)=>{ + actions.setFieldState('a-mirror',state=>{ + state.value = fieldState.value + }) + }) + }}> + + + , + document.getElementById('root') +) +``` + + +Example:Layout + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + createFormActions, + FormLayout, + FormButtonGroup, + Submit, + Reset, +} from '@uform/next' + +const actions = createFormActions() + +ReactDOM.render( +
    +
    Basic Layout
    + + + + + + + + SubmitReset​ + + +
    Inline Layout
    + + + + ​ + + SubmitReset​ + + +
    editable = false
    + + + + ​ + + SubmitReset​ + + +
    , + document.getElementById('root') +) +``` + +#### `` + +> Core components of @uform/next, used to describe form fields + +```typescript +interface IMarkupSchemaFieldProps { + name?: string + /** base json schema spec**/ + title?: SchemaMessage + description?: SchemaMessage + default?: any + readOnly?: boolean + writeOnly?: boolean + type?: 'string' | 'object' | 'array' | 'number' | string + enum?: Array + const?: any + multipleOf?: number + maximum?: number + exclusiveMaximum?: number + minimum?: number + exclusiveMinimum?: number + maxLength?: number + minLength?: number + pattern?: string | RegExp + maxItems?: number + minItems?: number + uniqueItems?: boolean + maxProperties?: number + minProperties?: number + required?: string[] | boolean + format?: string + /** nested json schema spec **/ + properties?: { + [key: string]: ISchema + } + items?: ISchema | ISchema[] + additionalItems?: ISchema + patternProperties?: { + [key: string]: ISchema + } + additionalProperties?: ISchema + /** extend json schema specs */ + editable?: boolean + visible?: boolean + display?: boolean + ['x-props']?: { [name: string]: any } + ['x-index']?: number + ['x-rules']?: ValidatePatternRules + ['x-component']?: string + ['x-component-props']?: { [name: string]: any } + ['x-render']?: ( + props: T & { + renderComponent: () => React.ReactElement + } + ) => React.ReactElement + ['x-effect']?: ( + dispatch: (type: string, payload: any) => void, + option?: object + ) => { [key: string]: any } +} +``` + +##### Usage + + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + FormSlot, + Field, + createFormActions, + FormLayout, + FormButtonGroup, + Submit, + Reset, +} from '@uform/next' + +const actions = createFormActions() + +ReactDOM.render( + +
    required
    + + +
    description
    + + +
    default value
    + + +
    readOnly
    + + +
    visible = false
    + + +
    display = false
    + + +
    editable = false
    + +
    , + document.getElementById('root') +) +``` + + +#### `` + +> Props of `` + +```typescript +interface ISubmitProps { + /** reset pops **/ + onSubmit?: ISchemaFormProps['onSubmit'] + showLoading?: boolean + /** nextBtnProps **/ + // type of btn + type?: 'primary' | 'secondary' | 'normal' + // size of btn + size?: 'small' | 'medium' | 'large' + // size of Icon + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // type of button when component = 'button' + htmlType?: 'submit' | 'reset' | 'button' + // typeof btn + component?: 'button' | 'a' + // Set the loading state of the button + loading?: boolean + // Whether it is a ghost button + ghost?: true | false | 'light' | 'dark' + // Whether it is a text button + text?: boolean + // Whether it is a warning button + warning?: boolean + // Whether it is disabled + disabled?: boolean + // Callback for button click + onClick?: (e: {}) => void + // Valid when Button component is set to 'a', which represents the URL of the linked page + href?: string + // Valid when Button component is set to 'a', which represents the way of open the linked document + target?: string +} +``` + +#### `` + +> Props of `` + +```typescript +interface IResetProps { + /** reset pops **/ + forceClear?: boolean + validate?: boolean + /** nextBtnProps **/ + // type of btn + type?: 'primary' | 'secondary' | 'normal' + // size of btn + size?: 'small' | 'medium' | 'large' + // size of Icon + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // type of button when component = 'button' + htmlType?: 'submit' | 'reset' | 'button' + // typeof btn + component?: 'button' | 'a' + // Set the loading state of the button + loading?: boolean + // Whether it is a ghost button + ghost?: true | false | 'light' | 'dark' + // Whether it is a text button + text?: boolean + // Whether it is a warning button + warning?: boolean + // Whether it is disabled + disabled?: boolean + // Callback for button click + onClick?: (e: {}) => void + // Valid when Button component is set to 'a', which represents the URL of the linked page + href?: string + // Valid when Button component is set to 'a', which represents the way of open the linked document + target?: string +} +``` + +#### `` + +> deprecated,please use [SchemaMarkupField](#SchemaMarkupField) + +### Array Components + +#### array + +```jsx +import React, { useState, useEffect } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/next' +import '@alifd/next/dist/next.css' +import Printer from '@uform/printer' + +const App = () => { + const [value, setValues] = useState({}) + useEffect(() => { + setTimeout(() => { + setValues({ + array: [{ array2: [{ aa: '123', bb: '321' }] }] + }) + }, 1000) + }, []) + return ( + + console.log(v)}> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Submit + Reset + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### cards + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/next' +import '@alifd/next/dist/next.css' +import Printer from '@uform/printer' + +const App = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### table + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/next' +import '@alifd/next/dist/next.css' +import Printer from '@uform/printer' + +const App = () => ( + + + + Hello worldasdasdasdasd + }, + operationsWidth: 300 + }} + > + + + + + + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +### Layout Components + + +#### `` + +> Props of ``, fully inherited from [CardProps](#CardProps)。 +> The only difference between FormCard [FormBlock](#FormBlock) is a border on the style + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { FormCard, SchemaMarkupField as Field } from '@uform/next' +import '@alifd/next/dist/next.css' + +const App = () => ( + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` , fully inherited from [CardProps](#CardProps) + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { FormBlock, SchemaMarkupField as Field } from '@uform/next' +import '@alifd/next/dist/next.css' + +const App = () => ( + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormStep { + dataSource: StepItemProps[] + /** next step props**/ + // current + current?: number + // direction of step + direction?: 'hoz' | 'ver' + // Content arrangement in horizontal layout + labelPlacement?: 'hoz' | 'ver' + // shape of step + shape?: 'circle' | 'arrow' | 'dot' + readOnly?: boolean + // Whether to activate animation + animation?: boolean + className?: string + // Custom StepItem render + itemRender?: (index: number, status: string) => React.ReactNode +} +``` + +**Usage** + +```jsx +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + FormEffectHooks, + createFormActions, + FormGridRow, + FormItemGrid, + FormGridCol, + FormPath, + FormLayout, + FormBlock, + FormCard, + FormTextBox, + FormStep +} from '@uform/next' +import { Button } from '@alifd/next' +import '@alifd/next/dist/next.css' + +const { onFormInit$ } = FormEffectHooks + +const actions = createFormActions() + +let cache = {} + +export default () => ( + { + console.log('submit') + console.log(values) + }} + actions={actions} + labelCol={{ span: 8 }} + wrapperCol={{ span: 6 }} + validateFirst + effects={({ setFieldState, getFormGraph }) => { + onFormInit$().subscribe(() => { + setFieldState('col1', state => { + state.visible = false + }) + }) + }} + > + + + + + + + + + + + + Submit + + + + + + +) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormItemTopProps { + inline?: boolean + className?: string + style?: React.CSSProperties + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} +``` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/next' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' +const App = () => ( + + + + + + + + + SubmitReset​ + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormItemGridProps { + cols?: Array + gutter?: number + /** next Form.Item props**/ + // prefix od FormItem + prefix?: string + + // label od FormItem + label?: React.ReactNode + + // label layout setting, eg, {span: 8, offset: 16} + labelCol?: {} + wrapperCol?: {} + + // Custom prompt information, if not set, it will be automatically generated according to the verification rules. + help?: React.ReactNode + + // Additional prompt information, similar to help, can be used when error messages and prompt copy are required at the same time. Behind the error message. + extra?: React.ReactNode + + // Check status, if not set, it will be generated automatically according to check rules + validateState?: 'error' | 'success' | 'loading' + + // Used in conjunction with the validateState property, whether to display the success / loading validation status icon. Currently only Input supports + hasFeedback?: boolean + + // Custom inline style + + style?: React.CSSProperties + + // node or function(values) + children?: React.ReactNode | (() => void) + + // The size of a single Item is customized, and takes precedence over the size of the Form, and when a component is used with an Item, the component itself does not set the size property. + size?: 'large' | 'small' | 'medium' + + // Position of the label + labelAlign?: 'top' | 'left' | 'inset' + + // alignment of labels + labelTextAlign?: 'left' | 'right' + + className?: string + + // [validation] required + required?: boolean + + // whether required asterisks are displayed + asterisk?: boolean + + // required custom error message + requiredMessage?: string + + // required Custom trigger method + requiredTrigger?: string | Array + + // [validation] min + min?: number + + // [validation] max + max?: number + + // min/max error message + minmaxMessage?: string + + // min/max custom trigger method + minmaxTrigger?: string | Array + + // [validation] min length of string / min length of array + minLength?: number + + // [validation] max length of string / max length of array + maxLength?: number + + // minLength/maxLength custom error message + minmaxLengthMessage?: string + + // minLength/maxLength custom trigger method + minmaxLengthTrigger?: string | Array + + // [validation] length of string / length of array + length?: number + + // length custom error message + lengthMessage?: string + + // length custom trigger method + lengthTrigger?: string | Array + + // Regular pattern + pattern?: any + + // pattern custom error message + patternMessage?: string + + // pattern custom trigger method + patternTrigger?: string | Array + + // [validation] regular pattern + format?: 'number' | 'email' | 'url' | 'tel' + + // format custom error message + formatMessage?: string + + // format custom trigger method + formatTrigger?: string | Array + + // [validation] custom validator + validator?: () => void + + // validator custom trigger method + validatorTrigger?: string | Array + + // Whether to automatically trigger validate when data is modified + autoValidate?: boolean +} +``` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/next' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' + +const App = () => ( + + console.log(v)}> + + + + + + + + + + + + ​SubmitReset​ + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormTextBox { + text?: string + gutter?: number + title?: React.ReactText + description?: React.ReactText +} +``` + +**Usage** + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormTextBox, + FormCard, + FormLayout +} from '@uform/next' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' + +const App = () => { + return ( + + console.log(v)}> + + + + + + + + + + + + ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormButtonGroupProps { + sticky?: boolean + style?: React.CSSProperties + itemStyle?: React.CSSProperties + className?: string + align?: 'left' | 'right' | 'start' | 'end' | 'top' | 'bottom' | 'center' + triggerDistance?: number + zIndex?: number + span?: ColSpanType + offset?: ColSpanType +} +``` + +**Usage** + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/next' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' + +const App = () => { + const [state, setState] = useState({ editable: true }) + return ( + + console.log(v)}> +
    normal
    + + ​SubmitReset​ + +
    sticky
    + + ​Submit​ + + Reset​ + +
    +
    + ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of ``, fully inherited from [ButtonProps](#ButtonProps) + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { TextButton } from '@uform/next' +import '@alifd/next/dist/next.css' + +const App = () => ( + + content + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of ``, fully inherited from [ButtonProps](#ButtonProps) + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { CircleButton } from '@uform/next' +import '@alifd/next/dist/next.css' + +const App = () => ( + + ok + +) +ReactDOM.render(, document.getElementById('root')) +``` + + +### Type of SchemaMarkupField + +#### string + +* Schema Type : `string` +* Schema UI Component: Fusion-Next ``, ``, `` + +```typescript +interface IPasswordProps { + checkStrength: boolean + /** next input props **/ + // value + value?: string | number + + // default value + defaultValue?: string | number + + // callback triggered when value change + onChange?: (value: string, e: React.ChangeEvent) => void + + // callback triggered when keyboard is press + onKeyDown?: (e: React.KeyboardEvent, opts: {}) => void + + // Disabled + disabled?: boolean + + // Max length + maxLength?: number + + // Whether to show the maximum length style + hasLimitHint?: boolean + + // When maxLength is set, whether to truncate beyond the string + cutString?: boolean + + // readOnly + readOnly?: boolean + + // Automatically remove the leading and trailing blank characters when trigger onChange + trim?: boolean + + // placeholder + placeholder?: string + + // callback triggered when focus + onFocus?: () => void + + // callback triggered when blur + onBlur?: () => void + + // Custom string length calculation + getValueLength?: (value: string) => number + + className?: string + style?: React.CSSProperties + htmlType?: string + + // name of field + name?: string + + // state of field + state?: 'error' | 'loading' | 'success' + + // label + label?: React.ReactNode + + // whether to show clear + hasClear?: boolean + + // whether to show border + hasBorder?: boolean + + // size of field + size?: 'small' | 'medium' | 'large' + + // callback triggered when enter is press + onPressEnter?: () => void + + // Watermark (Icon type, shared with hasClear) + hint?: string + + // Append content before text + innerBefore?: React.ReactNode + + // Append content after text + innerAfter?: React.ReactNode + + // Append content before input + addonBefore?: React.ReactNode + + // Append content after input + addonAfter?: React.ReactNode + + // Append text before input + addonTextBefore?: React.ReactNode + + // Append text after input + addonTextAfter?: React.ReactNode + + // (Native supported by input) + autoComplete?: string + + // (Native supported by input) + autoFocus?: boolean +} +``` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### number + +* Schema Type : `number` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### boolean + +* Schema Type : `boolean` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### date + +* Schema Type : `date` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### time + +* Schema Type : `time` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### range + +* Schema Type : `range` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### upload + +* Schema Type : `upload` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### checkbox + +* Schema Type : `checkbox` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### radio + +* Schema Type : `radio` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### rating + +* Schema Type : `rating` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### transfer + +* Schema Type : `transfer` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### API + +> Fully inherited from @uform/react, The specific API of @uform/next is listed below. + +--- + +#### `createFormActions` + +> Return [IFormActions](#IFormActions) + +**Signature** + +```typescript +createFormActions(): IFormActions +``` + +**Usage** + +```typescript +import { createFormActions } from '@uform/next' + +const actions = createFormActions() +console.log(actions.getFieldValue('username')) +``` + +#### `createAsyncFormActions` + +> Return [IFormAsyncActions](#IFormAsyncActions) + +**Signature** + +```typescript +createAsyncFormActions(): IFormAsyncActions +``` + +**Usage** + +```typescript +import { createAsyncFormActions } from '@uform/next' + +const actions = createAsyncFormActions() +actions.getFieldValue('username').then(val => console.log(val)) +``` + +#### `FormEffectHooks` + +> Return all @uform/core lifeCycles hook which can be subscribe + +**Usage** + +```tsx +import { FormEffectHooks, Form } from '@uform/react' +const { + /** + * Form LifeCycle + **/ + // Form pre-initialization trigger + onFormWillInit$, + // Form initialization trigger + onFormInit$, + // Triggered when the form changes + onFormChange$, + // Triggered when the form event is triggered, used to monitor only manual operations + onFormInputChange$, + // Trigger when the form initial value changes + onFormInitialValueChange$, + // Triggered when the form is reset + onFormReset$, + // Triggered when the form is submitted + onFormSubmit$, + // Triggered when the form submission starts + onFormSubmitStart$, + // Triggered when the form submission ends + onFormSubmitEnd$, + // Triggered when the form is mounted + onFormMount$, + // Triggered when the form is unloaded + onFormUnmount$, + // Triggered when form validation begins + onFormValidateStart$, + // Triggered when the form validation ends + onFormValidateEnd$, + // Trigger when the form initial value changes + onFormValuesChange$, + /** + * FormGraph LifeCycle + **/ + // Triggered when the form observer tree changes + onFormGraphChange$, + /** + * Field LifeCycle + **/ + // Triggered when pre-initialized + onFieldWillInit$, + // Triggered when the field is initialized + onFieldInit$, + // Triggered when the field changes + onFieldChange$, + // Triggered when the field is mounted + onFieldMount$, + // Trigger when the field is unloaded + onFieldUnmount$, + // Triggered when the field event is triggered, used to monitor only manual operations + onFieldInputChange$, + // Triggered when the field value changes + onFieldValueChange$, + // Trigger when the initial value of the field changes + onFieldInitialValueChange$ +} = FormEffectHooks + +const App = () => { + return ( +
    { + onFormInit$().subscribe(() => { + console.log('initialized') + }) + }} + > + ... +
    + ) +} +``` + +#### createEffectHook + +> Custom your own hook by this api + +**Usage** + +```jsx +import SchemaForm, { createEffectHook, createFormActions } from '@uform/next' + +const actions = createFormActions() +const diyHook1$ = createEffectHook('diy1') +const diyHook2$ = createEffectHook('diy2') + +const App = () => { + return ( + { + diyHook1$().subscribe(payload => { + console.log('diy1 hook triggered', payload) + }) + + diyHook2$().subscribe(payload => { + console.log('diy2 hook triggered', payload) + }) + }} + > + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### connect + +> Wrap field components with value / defaultValue / onChange of field, which make it esaily make custom field + +```typescript +type Connect = >(options?: IConnectOptions) => +(Target: T) => React.PureComponent +``` +**Usage** + +```typescript +import {registerFormField,connect} from '@uform/next' + +registerFormField( + 'string', + connect()(props => ) +) +``` + +#### registerFormField + +```typescript +type registerFormField( + name : string, // name of field + component : React.ComponentType, // component of field + noMiddleware: boolean // whether use middleware +) +``` + +**Usage** + +```jsx + +import SchemaForm, { SchemaMarkupField as Field, registerFormField, connect, createFormActions } from '@uform/next' + +registerFormField( + 'custom-string', + connect()(props => ) +) +const actions = createFormActions() + +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Interfaces + +> The Interfaces is fully inherited from @uform/react. The specific Interfaces of @uform/next is listed below. +--- + +#### IForm + +> Form instance object API created by using createForm + + + +```typescript +interface IForm { +  /* +   * Form submission, if the callback parameter returns Promise, +   * Then the entire submission process will hold and load is true. +   * Wait for Promise resolve to trigger the form onFormSubmitEnd event while loading is false +   */ +   submit( +      onSubmit?: (values: IFormState['values']) => any | Promise +    ): Promise<{ +       Validated: IFormValidateResult +       Payload: any //onSubmit callback function return value +   }> +    +   /* +    * Clear the error message, you can pass the FormPathPattern to batch or precise control of the field to be cleared. +    * For example, clearErrors("*(aa,bb,cc)") +    */ +   clearErrors: (pattern?: FormPathPattern) => void +    +   /* +    * Get status changes, mainly used to determine which states in the current life cycle have changed in the form lifecycle hook. +    * For example, hasChanged(state,'value.aa') +    */ +   hasChanged(target: IFormState | IFieldState | IVirtualFieldState, path: FormPathPattern): boolean +    +   /* +    * Reset form +    */ +   reset(options?: { +     // Forced to empty +     forceClear?: boolean +     // Forced check +     validate?: boolean +     // Reset range for batch or precise control of the field to be reset +     selector?: FormPathPattern +   }): Promise +    +   /* +    * Validation form +    */ +   validate(path?: FormPathPattern, options?: { +     // Is it pessimistic check, if the current field encounters the first verification error, stop the subsequent verification process +     first?:boolean +   }): Promise +    +   /* +    * Set the form status +    */ +   setFormState( +     // Operation callback +     callback?: (state: IFormState) => any, +     // No trigger the event +     silent?: boolean +   ): void +    +   /* +    * Get form status +    */ +   getFormState( +     //transformer +     callback?: (state: IFormState) => any +   ): any +    +   /* +    * Set the field status +    */ +   setFieldState( +     // Field path +     path: FormPathPattern, +     // Operation callback +     callback?: (state: IFieldState) => void, +     // No trigger the event +     silent?: boolean +   ): void +    +   /* +    * Get the field status +    */ +   getFieldState( +     // Field path +     path: FormPathPattern, +     // Transformer +     callback?: (state: IFieldState) => any +   ): any +    +   /* +    * Registration field +    */ +   registerField(props: { +    // Node path +    path?: FormPathPattern +    // Data path +    name?: string +    // Field value +    value?: any +    // Field multi-value +    values?: any[] +    // Field initial value +    initialValue?: any +    // Field extension properties +    props?: any +    // Field check rule +    rules?: ValidatePatternRules[] +    // Field is required +    required?: boolean +    // Is the field editable? +    editable?: boolean +    // Whether the field is dirty check +    useDirty?: boolean +    // Field state calculation container, mainly used to extend the core linkage rules +    computeState?: (draft: IFieldState, prevState: IFieldState) => void +  }): IField +   +  /* +   * Register virtual fields +   */ +  registerVirtualField(props: { +    // Node path +    path?: FormPathPattern +    // Data path +    name?: string +    // Field extension properties +    props?: any +    // Whether the field is dirty check +    useDirty?: boolean +    // Field state calculation container, mainly used to extend the core linkage rules +    computeState?: (draft: IFieldState, prevState: IFieldState) => void +  }): IVirtualField +   +  /* +   * Create a field data operator, which will explain the returned API in detail later. +   */ +  createMutators(field: IField): IMutators +   +  /* +   * Get the form observer tree +   */ +  getFormGraph(): IFormGraph +   +  /* +   * Set the form observer tree +   */ +  setFormGraph(graph: IFormGraph): void +   +  /* +   * Listen to the form life cycle +   */ +  subscribe(callback?: ({ +    type, +    payload +  }: { +    type: string +    payload: any +  }) => void): number +   +  /* +   * Cancel the listening form life cycle +   */ +  unsubscribe(id: number): void +   +  /* +   * Trigger form custom life cycle +   */ +  notify: (type: string, payload?: T) => void +   +  /* +   * Set the field value +   */ +  setFieldValue(path?: FormPathPattern, value?: any): void +   +  /* +   * Get the field value +   */ +  getFieldValue(path?: FormPathPattern): any +   +  /* +   * Set the initial value of the field +   */ +  setFieldInitialValue(path?: FormPathPattern, value?: any): void +   +  /* +   * Get the initial value of the field +   */ +  getFieldInitialValue(path?: FormPathPattern): any +} +``` + +#### ButtonProps + +```typescript +interface ButtonProps { + /** reset pops **/ + onSubmit?: ISchemaFormProps['onSubmit'] + showLoading?: boolean + /** nextBtnProps **/ + // type of btn + type?: 'primary' | 'secondary' | 'normal' + // size of btn + size?: 'small' | 'medium' | 'large' + // size of Icon + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // type of button when component = 'button' + htmlType?: 'submit' | 'reset' | 'button' + // typeof btn + component?: 'button' | 'a' + // Set the loading state of the button + loading?: boolean + // Whether it is a ghost button + ghost?: true | false | 'light' | 'dark' + // Whether it is a text button + text?: boolean + // Whether it is a warning button + warning?: boolean + // Whether it is disabled + disabled?: boolean + // Callback for button click + onClick?: (e: {}) => void + // Valid when Button component is set to 'a', which represents the URL of the linked page + href?: string + // Valid when Button component is set to 'a', which represents the way of open the linked document + target?: string +} +``` + +#### CardProps + +```typescript +interface CardProps extends HTMLAttributesWeak, CommonProps { + // media in card + media?: React.ReactNode + + // title of the card + title?: React.ReactNode + + // subTitle of the card + subTitle?: React.ReactNode + + // action button of the card + actions?: React.ReactNode + + // whether to show bullet of title + showTitleBullet?: boolean + + // whether to show divider of head + showHeadDivider?: boolean + contentHeight?: string | number + + // extra content of card + extra?: React.ReactNode + + // whether to set free mode, title, subtitle will be invalid when this options turns on + free?: boolean +} +``` + +#### ICompatItemProps + +```typescript +interface ICompatItemProps + extends Exclude, + Partial { + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} +``` + + +#### IFieldState + +```typescript +interface IFieldState { + /**Read-only attribute**/ + // State name, FieldState + displayName?: string // Data path + name: string // Node path + path: string // Has been initialized + initialized: boolean // Is it in the original state, the state is true only when value===intialValues + pristine: boolean // Is it in a legal state, as long as the error length is greater than 0, the valid is false + valid: boolean // Is it illegal, as long as the error length is greater than 0, the valid is true + invalid: boolean // Is it in check state? + validating: boolean // Is it modified, if the value changes, the property is true, and will be true throughout the life of the field + modified: boolean // Is it touched? + touched: boolean // Is it activated, when the field triggers the onFocus event, it will be triggered to true, when onBlur is triggered, it is false + active: boolean // Have you ever visited, when the field triggers the onBlur event, it will be triggered to true + visited: boolean /** writable property**/ // Is it visible, note: if the state is false, then the value of the field will not be submitted, and the UI will not display + visible: boolean // Whether to show, note: if the state is false, then the value of the field will be submitted, the UI will not display, similar to the form hidden field + display: boolean // Is it editable? + editable: boolean // Is it in the loading state, note: if the field is in asynchronous verification, loading is true + loading: boolean // Field multi-parameter value, such as when the field onChange trigger, the event callback passed multi-parameter data, then the value of all parameters will be stored here + values: any[] // Field error message + errors: string[] // Field alert message + warnings: string[] // Field value, is equal to values[0] + value: any // Initial value + initialValue: any // Check the rules, the specific type description refers to the following documents + rules: ValidatePatternRules[] // Is it required? + required: boolean // Whether to mount + mounted: boolean // Whether to uninstall + unmounted: boolean // field extension properties + props: FieldProps +} +``` + + +#### ISchemaFieldComponentProps + +```typescript +interface ISchemaFieldComponentProps extends IFieldState { + schema: Schema + mutators: IMutators + form: IForm + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + +#### ISchemaVirtualFieldComponentProps + +```typescript +interface ISchemaVirtualFieldComponentProps extends IVirtualFieldState { + schema: Schema + form: IForm + children: React.ReactElement[] + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + +#### ISchemaFieldWrapper + +```typescript +interface ISchemaFieldWrapper { + (Traget: ISchemaFieldComponent): + | React.FC + | React.ClassicComponent +} +``` + +#### ISchemaFieldComponent + +```typescript +declare type ISchemaFieldComponent = ComponentWithStyleComponent< + ISchemaFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} +``` + +#### ISchemaVirtualFieldComponent + +```typescript +declare type ISchemaVirtualFieldComponent = ComponentWithStyleComponent< + ISchemaVirtualFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} +``` + +#### ISchemaFormRegistry + +```typescript +interface ISchemaFormRegistry { + fields: { + [key: string]: ISchemaFieldComponent + } + virtualFields: { + [key: string]: ISchemaVirtualFieldComponent + } + wrappers?: ISchemaFieldWrapper[] + formItemComponent: React.JSXElementConstructor + formComponent: string | React.JSXElementConstructor +} +``` + +#### INextSchemaFieldProps + +```typescript +interface INextSchemaFieldProps { + name?: string; + /** ISchema **/ + title?: SchemaMessage; + description?: SchemaMessage; + default?: any; + readOnly?: boolean; + writeOnly?: boolean; + type?: 'string' | 'object' | 'array' | 'number' | string; + enum?: Array; + const?: any; + multipleOf?: number; + maximum?: number; + exclusiveMaximum?: number; + minimum?: number; + exclusiveMinimum?: number; + maxLength?: number; + minLength?: number; + pattern?: string | RegExp; + maxItems?: number; + minItems?: number; + uniqueItems?: boolean; + maxProperties?: number; + minProperties?: number; + required?: string[] | boolean; + format?: string; + properties?: { + [key: string]: ISchema; + }; + items?: ISchema | ISchema[]; + additionalItems?: ISchema; + patternProperties?: { + [key: string]: ISchema; + }; + additionalProperties?: ISchema; + editable?: boolean; + visible?: boolean; + display?: boolean; + ['x-props']?: { + [name: string]: any; + }; + ['x-index']?: number; + ['x-rules']?: ValidatePatternRules; + ['x-component']?: string; + ['x-component-props']?: { + [name: string]: any; + }; + ['x-render']?: (props: T & { + renderComponent: () => React.ReactElement; + }) => React.ReactElement; + ['x-effect']?: (dispatch: (type: string, payload: any) => void, option?: object) => { + [key: string]: any; + }; + +``` + +#### IPreviewTextProps + +```typescript +interface IPreviewTextProps { + className?: React.ReactText + dataSource?: any[] + value?: any + addonBefore?: React.ReactNode + innerBefore?: React.ReactNode + addonTextBefore?: React.ReactNode + addonTextAfter?: React.ReactNode + innerAfter?: React.ReactNode + addonAfter?: React.ReactNode +} + +``` + + +#### IMutators + +> The instance API created by crewikiutators is mainly used to operate field data. + +```typescript +interface IMutators { + // Changing the field value and multi parameter condition will store all parameters in values + change(...values: any[]): any + // Get focus, trigger active state change + focus(): void + // Lose focus, trigger active / visited status change + blur(): void + // Trigger current field verifier + validate(): Promise + // Whether the value of the current field exists in the values property of form + exist(index?: number | string): Boolean + + /**Array operation method**/ + + // Append data + push(value?: any): any[] + // Pop up tail data + pop(): any[] + // Insert data + insert(index: number, value: any): any[] + // Delete data + remove(index: number | string): any + // Head insertion + unshift(value: any): any[] + // Head ejection + shift(): any[] + // Move element + move($from: number, $to: number): any[] + // Move down + moveDown(index: number): any[] + // Move up + moveUp(index: number): any[] +} +``` + +#### IFieldProps + +```typescript +interface IFieldProps { + name : string // Node path + path : Array // Data path + value : V // value + errors : Array // Field error message + editable : boolean | ((name:string) => boolean) // Is it editable? + locale : Locale // i18n locale + loading : boolean // Is it in the loading state, note: if the field is in asynchronous verification, loading is true + schemaPath : Array // schema path + getSchema : (path: string) => ISchema // get schema by path + renderField : (childKey: string, reactKey: string | number) => JSX.Element | string | null + renderComponent : React.FunctionComponent | undefined>, + getOrderProperties : () => Array<{schema: ISchema, key: number, path: string, name: string }>, + mutators : IMutators, + schema : ISchema +} + +``` + +#### IConnectOptions + +```typescript + +interface IConnectOptions { + // name of value property + valueName?: string + // name of event property + eventName?: string + // default props + defaultProps?: Partial + // In some case, the value of our event function is not the first parameter of the event callback, and further customization is required. + getValueFromEvent?: (props: IFieldProps['x-props'], event: Event, ...args: any[]) => any + // props transformer + getProps?: (connectProps: IConnectProps, fieldProps: IFieldProps) => IConnectProps + // component transformer + getComponent?: ( + target: T, + connectProps: IConnectProps, + fieldProps: IFieldProps + ) => T +} + +``` \ No newline at end of file diff --git a/packages/next/README.zh-cn.md b/packages/next/README.zh-cn.md index 35f5c209246..62a45464f58 100644 --- a/packages/next/README.zh-cn.md +++ b/packages/next/README.zh-cn.md @@ -1,2 +1,2966 @@ # @uform/next -> UForm Fusion Next 组件插件包 + +### 安装 + +```bash +npm install --save @uform/next +``` + +### 目录 + + + +- [使用方式](#使用方式) + - [`快速开始`](#快速开始) +- [Components](#components) + - [``](#SchemaForm) + - [``](#SchemaMarkupField) + - [``](#Submit) + - [``](#Reset) + - [`(即将废弃,请使用)`](<#Field(即将废弃,请使用SchemaMarkupField)>) +- [表单List](#Array-Components) + - [`array`](#array) + - [`cards`](#cards) + - [`table`](#table) +- [布局组件](#Layout-Components) + - [``](#FormCard) + - [``](#FormBlock) + - [``](#FormStep) + - [``](#FormLayout) + - [``](#FormItemGrid) + - [``](#FormTextBox) + - [``](#FormButtonGroup) + - [``](#TextButton) + - [``](#CircleButton) +- [字段类型](#Type-of-SchemaMarkupField) + - [`string`](#string) + - [`textarea`](#textarea) + - [`password`](#password) + - [`number`](#number) + - [`boolean`](#boolean) + - [`date`](#date) + - [`time`](#time) + - [`range`](#range) + - [`upload`](#upload) + - [`checkbox`](#checkbox) + - [`radio`](#radio) + - [`rating`](#rating) + - [`transfer`](#transfer) +- [API](#API) + - [`createFormActions`](#createFormActions) + - [`createAsyncFormActions`](#createAsyncFormActions) + - [`FormEffectHooks`](#FormEffectHooks) + - [`createEffectHook`](#createEffectHook) + - [`connect`](#connect) + - [`registerFormField`](#registerFormField) +- [Interfaces](#Interfaces) + - [`ButtonProps`](#ButtonProps) + - [`CardProps`](#CardProps) + - [`ICompatItemProps`](#ICompatItemProps) + - [`IFieldState`](#IFieldState) + - [`ISchemaFieldComponentProps`](#ISchemaFieldComponentProps) + - [`ISchemaVirtualFieldComponentProps`](#ISchemaVirtualFieldComponentProps) + - [`ISchemaFieldWrapper`](#ISchemaFieldWrapper) + - [`ISchemaFieldComponent`](#ISchemaFieldComponent) + - [`ISchemaVirtualFieldComponent`](#ISchemaVirtualFieldComponent) + - [`ISchemaFormRegistry`](#ISchemaFormRegistry) + - [`INextSchemaFieldProps`](#INextSchemaFieldProps) + - [`IPreviewTextProps`](#IPreviewTextProps) + - [`IMutators`](#IMutators) + - [`IFieldProps`](#IFieldProps) + - [`IConnectOptions`](#IConnectOptions) + + +### 使用方式 + +--- + +#### 快速开始 + +例子:使用 JSX 开发 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import { Button } from '@alifd/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() + +const App = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +例子:使用 schema 来开发 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import { Button } from '@alifd/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() + +const App = () => { + const schema = { + type: 'object', + properties: { + radio: { + type: 'radio', + enum: ['1', '2', '3', '4'], + title: 'Radio' + }, + select: { + type: 'string', + enum: ['1', '2', '3', '4'], + title: 'Select', + required: true + }, + checkbox: { + type: 'checkbox', + enum: ['1', '2', '3', '4'], + title: 'Checkbox', + required: true + }, + textarea: { + type: 'string', + 'x-component': 'textarea', + title: 'TextArea' + }, + number: { + type: 'number', + title: '数字选择' + }, + boolean: { + type: 'boolean', + title: '开关选择' + }, + date: { + type: 'date', + title: '日期选择' + }, + daterange: { + type: 'daterange', + default: ['2018-12-19', '2018-12-19'], + title: '日期范围' + }, + year: { + type: 'year', + title: '年份' + }, + time: { + type: 'time', + title: '时间' + }, + upload: { + type: 'upload', + 'x-props': { + listType: 'card' + }, + title: '卡片上传文件' + }, + upload2: { + type: 'upload', + 'x-props': { + listType: 'dragger' + }, + title: '拖拽上传文件' + }, + upload3: { + type: 'upload', + 'x-props': { + listType: 'text' + }, + title: '普通上传文件' + }, + range: { + type: 'range', + 'x-props': { + min: 0, + max: 1024, + marks: [0, 1024] + }, + title: '范围选择' + }, + transfer: { + type: 'transfer', + enum: [ + { + value: 1, + label: '选项1' + }, + { + value: 2, + label: '选项2' + } + ], + title: '穿梭框' + }, + rating: { + type: 'rating', + title: '等级' + }, + layout_btb_group: { + type: 'object', + 'x-component': 'button-group', + 'x-component-props': { + offset:7, + sticky: true, + }, + properties: { + submit_btn: { + type: 'object', + 'x-component': 'submit', + 'x-component-props': { + children: '提交', + }, + }, + reset_btn: { + type: 'object', + 'x-component': 'reset', + 'x-component-props': { + children: '重置', + }, + }, + } + }, + } + } + return +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Components + +--- + +#### `` + +基于@uform/react 的核心组件SchemaForm进一步扩展出来的SchemaForm组件,推荐生产环境下使用 + +```typescript +interface INextSchemaFormProps { + // 通过schema渲染 + schema?: ISchema; + fields?: ISchemaFormRegistry['fields']; + virtualFields?: ISchemaFormRegistry['virtualFields']; + // 全局注册Form渲染组件 + formComponent?: ISchemaFormRegistry['formComponent']; + // 全局注册FormItem渲染组件 + formItemComponent?: ISchemaFormRegistry['formItemComponent']; + // label布局控制 + labelCol?: number | { span: number; offset?: number } + // FormItem布局控制 + wrapperCol?: number | { span: number; offset?: number } + // 自定义预览placeholder + previewPlaceholder?: string | ((props: IPreviewTextProps) => string); + // 样式前缀 + prefix?: string; + // 内联表单 + inline?: boolean; + // 单个 Item 的 size 自定义,优先级高于 Form 的 size, 并且当组件与 Item 一起使用时,组件自身设置 size 属性无效。 + size?: 'large' | 'medium' | 'small'; + // 标签的位置 + labelAlign?: 'top' | 'left' | 'inset'; + // 标签的左右对齐方式 + labelTextAlign?: 'left' | 'right'; + // 控制第一级 Item 的 labelCol + labelCol?: {}; + // 控制第一级 Item 的 wrapperCol + wrapperCol?: {}; + // 扩展class + className?: string; + // 自定义内联样式 + style?: React.CSSProperties; + // 设置标签类型 + component?: string | (() => void); + // 全局value + value?: Value; + // 全局defaultValue + defaultValue?: DefaultValue; + // 全局initialValues + initialValues?: DefaultValue; + // FormActions实例 + actions?: FormActions; + // IFormEffect实例 + effects?: IFormEffect; + // 表单实例 + form?: IForm; + // 表单变化回调 + onChange?: (values: Value) => void; + // form内有 `htmlType="submit"` 或 actions.submit时 触发 + onSubmit?: (values: Value) => void | Promise; + // form内有 或 actions.reset时 触发 + onReset?: () => void; + // 校验失败时触发 + onValidateFailed?: (valideted: IFormValidateResult) => void; + children?: React.ReactElement | ((form: IForm) => React.ReactElement); + // 是否使用脏检查,默认会走immer精确更新 + useDirty?: boolean; + // 是否可编辑 + editable?: boolean | ((name: string) => boolean); + // 是否走悲观校验,遇到第一个校验失败就停止后续校验 + validateFirst?: boolean; +} +``` + +**用法** + +例子1: 将a-mirror的值设置为a的值。 + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + registerFormField, + Field, + connect, + createFormActions +} from '@uform/next' + +const actions = createFormActions() + +registerFormField( + 'string', + connect()(props => ) +) + +ReactDOM.render( + { + $('onFieldChange','a').subscribe((fieldState)=>{ + actions.setFieldState('a-mirror',state=>{ + state.value = fieldState.value + }) + }) + }}> + + + , + document.getElementById('root') +) +``` + + +例子2: 布局 + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + createFormActions, + FormLayout, + FormButtonGroup, + Submit, + Reset, +} from '@uform/next' + +const actions = createFormActions() + +ReactDOM.render( +
    +
    常规布局
    + + + + + + + + 提交重置​ + + +
    Inline Layout
    + + + + ​ + + 提交重置​ + + +
    editable = false
    + + + + ​ + + 提交重置​ + + +
    , + document.getElementById('root') +) +``` + +#### `` + +> @uform/next 的核心组件,用于描述表单字段 + +```typescript +interface IMarkupSchemaFieldProps { + name?: string + /** base json schema spec**/ + title?: SchemaMessage + description?: SchemaMessage + default?: any + readOnly?: boolean + writeOnly?: boolean + type?: 'string' | 'object' | 'array' | 'number' | string + enum?: Array + const?: any + multipleOf?: number + maximum?: number + exclusiveMaximum?: number + minimum?: number + exclusiveMinimum?: number + maxLength?: number + minLength?: number + pattern?: string | RegExp + maxItems?: number + minItems?: number + uniqueItems?: boolean + maxProperties?: number + minProperties?: number + required?: string[] | boolean + format?: string + /** nested json schema spec **/ + properties?: { + [key: string]: ISchema + } + items?: ISchema | ISchema[] + additionalItems?: ISchema + patternProperties?: { + [key: string]: ISchema + } + additionalProperties?: ISchema + /** extend json schema specs */ + editable?: boolean + visible?: boolean + display?: boolean + ['x-props']?: { [name: string]: any } + ['x-index']?: number + ['x-rules']?: ValidatePatternRules + ['x-component']?: string + ['x-component-props']?: { [name: string]: any } + ['x-render']?: ( + props: T & { + renderComponent: () => React.ReactElement + } + ) => React.ReactElement + ['x-effect']?: ( + dispatch: (type: string, payload: any) => void, + option?: object + ) => { [key: string]: any } +} +``` + +##### 用法 + + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + FormSlot, + Field, + createFormActions, + FormLayout, + FormButtonGroup, + Submit, + Reset, +} from '@uform/next' + +const actions = createFormActions() + +ReactDOM.render( + +
    required
    + + +
    description
    + + +
    default value
    + + +
    readOnly
    + + +
    visible = false
    + + +
    display = false
    + + +
    editable = false
    + +
    , + document.getElementById('root') +) +``` + + +#### `` + +> Submit 组件 Props + +```typescript +interface ISubmitProps { + /** reset pops **/ + onSubmit?: ISchemaFormProps['onSubmit'] + showLoading?: boolean + /** nextBtnProps **/ + // 按钮的类型 + type?: 'primary' | 'secondary' | 'normal' + // 按钮的尺寸 + size?: 'small' | 'medium' | 'large' + // 按钮中 Icon 的尺寸,用于替代 Icon 的默认大小 + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // 当 component = 'button' 时,设置 button 标签的 type 值 + htmlType?: 'submit' | 'reset' | 'button' + // 设置标签类型 + component?: 'button' | 'a' + // 设置按钮的载入状态 + loading?: boolean + // 是否为幽灵按钮 + ghost?: true | false | 'light' | 'dark' + // 是否为文本按钮 + text?: boolean + // 是否为警告按钮 + warning?: boolean + // 是否禁用 + disabled?: boolean + // 点击按钮的回调 + onClick?: (e: {}) => void + // 在Button组件使用component属性值为a时有效,代表链接页面的URL + href?: string + // 在Button组件使用component属性值为a时有效,代表何处打开链接文档 + target?: string +} +``` + +#### `` + +> Reset 组件 Props + +```typescript +interface IResetProps { + /** reset pops **/ + forceClear?: boolean + validate?: boolean + /** nextBtnProps **/ + // 按钮的类型 + type?: 'primary' | 'secondary' | 'normal' + // 按钮的尺寸 + size?: 'small' | 'medium' | 'large' + // 按钮中 Icon 的尺寸,用于替代 Icon 的默认大小 + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // 当 component = 'button' 时,设置 button 标签的 type 值 + htmlType?: 'submit' | 'reset' | 'button' + // 设置标签类型 + component?: 'button' | 'a' + // 设置按钮的载入状态 + loading?: boolean + // 是否为幽灵按钮 + ghost?: true | false | 'light' | 'dark' + // 是否为文本按钮 + text?: boolean + // 是否为警告按钮 + warning?: boolean + // 是否禁用 + disabled?: boolean + // 点击按钮的回调 + onClick?: (e: {}) => void + // 在Button组件使用component属性值为a时有效,代表链接页面的URL + href?: string + // 在Button组件使用component属性值为a时有效,代表何处打开链接文档 + target?: string +} +``` + +### Array Components + +#### array + +```jsx +import React, { useState, useEffect } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/next' +import '@alifd/next/dist/next.css' +import Printer from '@uform/printer' + +const App = () => { + const [value, setValues] = useState({}) + useEffect(() => { + setTimeout(() => { + setValues({ + array: [{ array2: [{ aa: '123', bb: '321' }] }] + }) + }, 1000) + }, []) + return ( + + console.log(v)}> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 提交 + 重置 + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### cards + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/next' +import '@alifd/next/dist/next.css' +import Printer from '@uform/printer' + +const App = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### table + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/next' +import '@alifd/next/dist/next.css' +import Printer from '@uform/printer' + +const App = () => ( + + + + Hello worldasdasdasdasd + }, + operationsWidth: 300 + }} + > + + + + + + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +### Layout Components + + +#### `` + +> FormCard 组件 Props, 完全继承自 [CardProps](#CardProps)。 +> FormCard与[FormBlock](#FormBlock) 唯一区别是样式上是否有框 + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { FormCard, SchemaMarkupField as Field } from '@uform/next' +import '@alifd/next/dist/next.css' + +const App = () => ( + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormBlock 组件 Props, 完全继承自 [CardProps](#CardProps) + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { FormBlock, SchemaMarkupField as Field } from '@uform/next' +import '@alifd/next/dist/next.css' + +const App = () => ( + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormStep 组件 Props + +```typescript +interface IFormStep { + dataSource: StepItemProps[] + /** next step props**/ + // 当前步骤 + current?: number + // 展示方向 + direction?: 'hoz' | 'ver' + // 横向布局时的内容排列 + labelPlacement?: 'hoz' | 'ver' + // 类型 + shape?: 'circle' | 'arrow' | 'dot' + // 是否只读模式 + readOnly?: boolean + // 是否开启动效 + animation?: boolean + // 自定义样式名 + className?: string + // StepItem 的自定义渲染 + itemRender?: (index: number, status: string) => React.ReactNode +} +``` + +**用法** + +```jsx +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + FormEffectHooks, + createFormActions, + FormGridRow, + FormItemGrid, + FormGridCol, + FormPath, + FormLayout, + FormBlock, + FormCard, + FormTextBox, + FormStep +} from '@uform/next' +import { Button } from '@alifd/next' +import '@alifd/next/dist/next.css' + +const { onFormInit$ } = FormEffectHooks + +const actions = createFormActions() + +let cache = {} + +export default () => ( + { + console.log('提交') + console.log(values) + }} + actions={actions} + labelCol={{ span: 8 }} + wrapperCol={{ span: 6 }} + validateFirst + effects={({ setFieldState, getFormGraph }) => { + onFormInit$().subscribe(() => { + setFieldState('col1', state => { + state.visible = false + }) + }) + }} + > + + + + + + + + + + + + 提交 + + + + + + +) +``` + +#### `` + +> FormLayout 组件 Props + +```typescript +interface IFormItemTopProps { + inline?: boolean + className?: string + style?: React.CSSProperties + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} +``` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/next' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' +const App = () => ( + + + + + + + + + 提交重置​ + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormItemGrid 组件 Props + +```typescript +interface IFormItemGridProps { + cols?: Array + gutter?: number + /** next Form.Item props**/ + // 样式前缀 + prefix?: string + + // label 标签的文本 + label?: React.ReactNode + + // label 标签布局,通 `` 组件,设置 span offset 值,如 {span: 8, offset: 16},该项仅在垂直表单有效 + labelCol?: {} + + // 需要为输入控件设置布局样式时,使用该属性,用法同 labelCol + wrapperCol?: {} + + // 自定义提示信息,如不设置,则会根据校验规则自动生成. + help?: React.ReactNode + + // 额外的提示信息,和 help 类似,当需要错误信息和提示文案同时出现时,可以使用这个。 位于错误信息后面 + extra?: React.ReactNode + + // 校验状态,如不设置,则会根据校验规则自动生成 + validateState?: 'error' | 'success' | 'loading' + + // 配合 validateState 属性使用,是否展示 success/loading 的校验状态图标, 目前只有Input支持 + hasFeedback?: boolean + + // 自定义内联样式 + style?: React.CSSProperties + + // node 或者 function(values) + children?: React.ReactNode | (() => void) + + // 单个 Item 的 size 自定义,优先级高于 Form 的 size, 并且当组件与 Item 一起使用时,组件自身设置 size 属性无效。 + size?: 'large' | 'small' | 'medium' + + // 标签的位置 + labelAlign?: 'top' | 'left' | 'inset' + + // 标签的左右对齐方式 + labelTextAlign?: 'left' | 'right' + + // 扩展class + className?: string + + // [表单校验] 不能为空 + required?: boolean + + // required 的星号是否显示 + asterisk?: boolean + + // required 自定义错误信息 + requiredMessage?: string + + // required 自定义触发方式 + requiredTrigger?: string | Array + + // [表单校验] 最小值 + min?: number + + // [表单校验] 最大值 + max?: number + + // min/max 自定义错误信息 + minmaxMessage?: string + + // min/max 自定义触发方式 + minmaxTrigger?: string | Array + + // [表单校验] 字符串最小长度 / 数组最小个数 + minLength?: number + + // [表单校验] 字符串最大长度 / 数组最大个数 + maxLength?: number + + // minLength/maxLength 自定义错误信息 + minmaxLengthMessage?: string + + // minLength/maxLength 自定义触发方式 + minmaxLengthTrigger?: string | Array + + // [表单校验] 字符串精确长度 / 数组精确个数 + length?: number + + // length 自定义错误信息 + lengthMessage?: string + + // length 自定义触发方式 + lengthTrigger?: string | Array + + // 正则校验 + pattern?: any + + // pattern 自定义错误信息 + patternMessage?: string + + // pattern 自定义触发方式 + patternTrigger?: string | Array + + // [表单校验] 四种常用的 pattern + format?: 'number' | 'email' | 'url' | 'tel' + + // format 自定义错误信息 + formatMessage?: string + + // format 自定义触发方式 + formatTrigger?: string | Array + + // [表单校验] 自定义校验函数 + validator?: () => void + + // validator 自定义触发方式 + validatorTrigger?: string | Array + + // 是否修改数据时自动触发校验 + autoValidate?: boolean +} +``` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/next' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' + +const App = () => ( + + console.log(v)}> + + + + + + + + + + + + ​提交重置​ + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormTextBox 组件 Props + +```typescript +interface IFormTextBox { + text?: string + gutter?: number + title?: React.ReactText + description?: React.ReactText +} +``` + +**用法** + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormTextBox, + FormCard, + FormLayout +} from '@uform/next' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' + +const App = () => { + return ( + + console.log(v)}> + + + + + + + + + + + + ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormButtonGroup 组件 Props + +```typescript +interface IFormButtonGroupProps { + sticky?: boolean + style?: React.CSSProperties + itemStyle?: React.CSSProperties + className?: string + align?: 'left' | 'right' | 'start' | 'end' | 'top' | 'bottom' | 'center' + triggerDistance?: number + zIndex?: number + span?: ColSpanType + offset?: ColSpanType +} +``` + +**用法** + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/next' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' + +const App = () => { + const [state, setState] = useState({ editable: true }) + return ( + + console.log(v)}> +
    normal
    + + ​提交重置​ + +
    sticky
    + + ​提交​ + + 重置​ + +
    +
    + ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> TextButton 组件 Props, 完全继承自 [ButtonProps](#ButtonProps) + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { TextButton } from '@uform/next' +import '@alifd/next/dist/next.css' + +const App = () => ( + + content + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> CircleButton 组件 Props, 完全继承自 [ButtonProps](#ButtonProps) + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { CircleButton } from '@uform/next' +import '@alifd/next/dist/next.css' + +const App = () => ( + + ok + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `(即将废弃,请使用SchemaMarkupField)` + +> 即将废弃,请使用[SchemaMarkupField](#SchemaMarkupField) + +### Type of SchemaMarkupField + +#### string + +* Schema Type : `string` +* Schema UI Component: Fusion-Next ``, ``, `` + +```typescript +interface IPasswordProps { + checkStrength: boolean + /** next input props **/ + // 当前值 + value?: string | number + + // 初始化值 + defaultValue?: string | number + + // 发生改变的时候触发的回调 + onChange?: (value: string, e: React.ChangeEvent) => void + + // 键盘按下的时候触发的回调 + onKeyDown?: (e: React.KeyboardEvent, opts: {}) => void + + // 禁用状态 + disabled?: boolean + + // 最大长度 + maxLength?: number + + // 是否展现最大长度样式 + hasLimitHint?: boolean + + // 当设置了maxLength时,是否截断超出字符串 + cutString?: boolean + + // 只读 + readOnly?: boolean + + // onChange返回会自动去除头尾空字符 + trim?: boolean + + // 输入提示 + placeholder?: string + + // 获取焦点时候触发的回调 + onFocus?: () => void + + // 失去焦点时候触发的回调 + onBlur?: () => void + + // 自定义字符串计算长度方式 + getValueLength?: (value: string) => number + + // 自定义class + className?: string + + // 自定义内联样式 + style?: React.CSSProperties + + // 原生type + htmlType?: string + + // name + name?: string + + // 状态 + state?: 'error' | 'loading' | 'success' + + // label + label?: React.ReactNode + + // 是否出现clear按钮 + hasClear?: boolean + + // 是否有边框 + hasBorder?: boolean + + // 尺寸 + size?: 'small' | 'medium' | 'large' + + // 按下回车的回调 + onPressEnter?: () => void + + // 水印 (Icon的type类型,和hasClear占用一个地方) + hint?: string + + // 文字前附加内容 + innerBefore?: React.ReactNode + + // 文字后附加内容 + innerAfter?: React.ReactNode + + // 输入框前附加内容 + addonBefore?: React.ReactNode + + // 输入框后附加内容 + addonAfter?: React.ReactNode + + // 输入框前附加文字 + addonTextBefore?: React.ReactNode + + // 输入框后附加文字 + addonTextAfter?: React.ReactNode + + // (原生input支持) + autoComplete?: string + + // 自动聚焦(原生input支持) + autoFocus?: boolean +} +``` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### number + +* Schema Type : `number` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### boolean + +* Schema Type : `boolean` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### date + +* Schema Type : `date` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### time + +* Schema Type : `time` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### range + +* Schema Type : `range` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### upload + +* Schema Type : `upload` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### checkbox + +* Schema Type : `checkbox` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### radio + +* Schema Type : `radio` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### rating + +* Schema Type : `rating` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### transfer + +* Schema Type : `transfer` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### API + +> 整体完全继承@uform/react, 下面只列举@uform/next 的特有 API + +--- + +#### `createFormActions` + +> 创建一个 [IFormActions](#IFormActions) 实例 + +**签名** + +```typescript +createFormActions(): IFormActions +``` + +**用法** + +```typescript +import { createFormActions } from '@uform/next' + +const actions = createFormActions() +console.log(actions.getFieldValue('username')) +``` + +#### `createAsyncFormActions` + +> 创建一个 [IFormAsyncActions](#IFormAsyncActions) 实例,成员方法 同[IFormActions](#IFormActions), +> 但是调用 API 返回的结果是异步的(promise)。 + +**签名** + +```typescript +createAsyncFormActions(): IFormAsyncActions +``` + +**用法** + +```typescript +import { createAsyncFormActions } from '@uform/next' + +const actions = createAsyncFormActions() +actions.getFieldValue('username').then(val => console.log(val)) +``` + +#### `FormEffectHooks` + +> 返回包含所有 UForm 生命周期的钩子函数,可以被监听消费 + +**用法** + +```typescript +import SchemaForm, { FormEffectHooks } from '@uform/next' +const { + /** + * Form LifeCycle + **/ + onFormWillInit$, // 表单预初始化触发 + onFormInit$, // 表单初始化触发 + onFormChange$, // 表单变化时触发 + onFormInputChange$, // 表单事件触发时触发,用于只监控人工操作 + onFormInitialValueChange$, // 表单初始值变化时触发 + onFormReset$, // 表单重置时触发 + onFormSubmit$, // 表单提交时触发 + onFormSubmitStart$, // 表单提交开始时触发 + onFormSubmitEnd$, // 表单提交结束时触发 + onFormMount$, // 表单挂载时触发 + onFormUnmount$, // 表单卸载时触发 + onFormValidateStart$, // 表单校验开始时触发 + onFormValidateEnd$, //表单校验结束时触发 + onFormValuesChange$, // 表单值变化时触发 + /** + * FormGraph LifeCycle + **/ + onFormGraphChange$, // 表单观察者树变化时触发 + /** + * Field LifeCycle + **/ + onFieldWillInit$, // 字段预初始化时触发 + onFieldInit$, // 字段初始化时触发 + onFieldChange$, // 字段变化时触发 + onFieldMount$, // 字段挂载时触发 + onFieldUnmount$, // 字段卸载时触发 + onFieldInputChange$, // 字段事件触发时触发,用于只监控人工操作 + onFieldValueChange$, // 字段值变化时触发 + onFieldInitialValueChange$ // 字段初始值变化时触发 +} = FormEffectHooks + +const App = () => { + return ( + { + onFormInit$().subscribe(() => { + console.log('初始化') + }) + }} + > + ... + + ) +} +``` + +#### createEffectHook + +> 自定义 hook + +**Usage** + +```jsx +import SchemaForm, { createEffectHook, createFormActions } from '@uform/next' + +const actions = createFormActions() +const diyHook1$ = createEffectHook('diy1') +const diyHook2$ = createEffectHook('diy2') + +const App = () => { + return ( + { + diyHook1$().subscribe(payload => { + console.log('diy1 hook triggered', payload) + }) + + diyHook2$().subscribe(payload => { + console.log('diy2 hook triggered', payload) + }) + }} + > + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### connect + +> 包装字段组件,让字段组件只需要支持value/defaultValue/onChange属性即可快速接入表单 + +```typescript +type Connect = >(options?: IConnectOptions) => +(Target: T) => React.PureComponent +``` +**用法** + +```typescript +import {registerFormField,connect} from '@uform/next' + +registerFormField( + 'string', + connect()(props => ) +) +``` + +#### registerFormField + +```typescript +type registerFormField( + name : string, //类型名称 + component : React.ComponentType, //类型组件 + noMiddleware: boolean //是否被middleware包装 +) +``` + +**用法** + +```jsx + +import SchemaForm, { SchemaMarkupField as Field, registerFormField, connect, createFormActions } from '@uform/next' + +registerFormField( + 'custom-string', + connect()(props => ) +) +const actions = createFormActions() + +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Interfaces + +> 整体完全继承@uform/react, 下面只列举@uform/next 的特有的 Interfaces + +--- + +#### IForm + +> 通过 createForm 创建出来的 Form 实例对象 API + +```typescript +interface IForm { + /* + * 表单提交,如果回调参数返回Promise, + * 那么整个提交流程会hold住,同时loading为true, + * 等待Promise resolve才触发表单onFormSubmitEnd事件,同时loading为false + */ + submit( + onSubmit?: (values: IFormState['values']) => any | Promise + ): Promise<{ + validated: IFormValidateResult + payload: any //onSubmit回调函数返回值 + }> + + /* + * 清空错误消息,可以通过传FormPathPattern来批量或精确控制要清空的字段, + * 比如clearErrors("*(aa,bb,cc)") + */ + clearErrors: (pattern?: FormPathPattern) => void + + /* + * 获取状态变化情况,主要用于在表单生命周期钩子内判断当前生命周期中有哪些状态发生了变化, + * 比如hasChanged(state,'value.aa') + */ + hasChanged( + target: IFormState | IFieldState | IVirtualFieldState, + path: FormPathPattern + ): boolean + + /* + * 重置表单 + */ + reset(options?: { + //强制清空 + forceClear?: boolean + //强制校验 + validate?: boolean + //重置范围,用于批量或者精确控制要重置的字段 + selector?: FormPathPattern + }): Promise + + /* + * 校验表单 + */ + validate( + path?: FormPathPattern, + options?: { + //是否悲观校验,如果当前字段遇到第一个校验错误则停止后续校验流程 + first?: boolean + } + ): Promise + + /* + * 设置表单状态 + */ + setFormState( + //操作回调 + callback?: (state: IFormState) => any, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取表单状态 + */ + getFormState( + //transformer + callback?: (state: IFormState) => any + ): any + + /* + * 设置字段状态 + */ + setFieldState( + //字段路径 + path: FormPathPattern, + //操作回调 + callback?: (state: IFieldState) => void, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取字段状态 + */ + getFieldState( + //字段路径 + path: FormPathPattern, + //transformer + callback?: (state: IFieldState) => any + ): any + + /* + * 注册字段 + */ + registerField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段值 + value?: any + //字段多参值 + values?: any[] + //字段初始值 + initialValue?: any + //字段扩展属性 + props?: any + //字段校验规则 + rules?: ValidatePatternRules[] + //字段是否必填 + required?: boolean + //字段是否可编辑 + editable?: boolean + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IField + + /* + * 注册虚拟字段 + */ + registerVirtualField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段扩展属性 + props?: any + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IVirtualField + + /* + * 创建字段数据操作器,后面会详细解释返回的API + */ + createMutators(field: IField): IMutators + + /* + * 获取表单观察者树 + */ + getFormGraph(): IFormGraph + + /* + * 设置表单观察者树 + */ + setFormGraph(graph: IFormGraph): void + + /* + * 监听表单生命周期 + */ + subscribe( + callback?: ({ type, payload }: { type: string; payload: any }) => void + ): number + + /* + * 取消监听表单生命周期 + */ + unsubscribe(id: number): void + + /* + * 触发表单自定义生命周期 + */ + notify: (type: string, payload?: T) => void + + /* + * 设置字段值 + */ + setFieldValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段值 + */ + getFieldValue(path?: FormPathPattern): any + + /* + * 设置字段初始值 + */ + setFieldInitialValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段初始值 + */ + getFieldInitialValue(path?: FormPathPattern): any +} +``` + +#### ButtonProps + +```typescript +interface ButtonProps { + /** reset pops **/ + onSubmit?: ISchemaFormProps['onSubmit'] + showLoading?: boolean + /** nextBtnProps **/ + // 按钮的类型 + type?: 'primary' | 'secondary' | 'normal' + // 按钮的尺寸 + size?: 'small' | 'medium' | 'large' + // 按钮中 Icon 的尺寸,用于替代 Icon 的默认大小 + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // 当 component = 'button' 时,设置 button 标签的 type 值 + htmlType?: 'submit' | 'reset' | 'button' + // 设置标签类型 + component?: 'button' | 'a' + // 设置按钮的载入状态 + loading?: boolean + // 是否为幽灵按钮 + ghost?: true | false | 'light' | 'dark' + // 是否为文本按钮 + text?: boolean + // 是否为警告按钮 + warning?: boolean + // 是否禁用 + disabled?: boolean + // 点击按钮的回调 + onClick?: (e: {}) => void + // 在Button组件使用component属性值为a时有效,代表链接页面的URL + href?: string + // 在Button组件使用component属性值为a时有效,代表何处打开链接文档 + target?: string +} +``` + +#### CardProps + +```typescript +interface CardProps extends HTMLAttributesWeak, CommonProps { + // 卡片的上的图片 / 视频 + media?: React.ReactNode + + // 卡片的标题 + title?: React.ReactNode + + // 卡片的副标题 + subTitle?: React.ReactNode + + // 卡片操作组,位置在卡片底部 + actions?: React.ReactNode + + // 是否显示标题的项目符号 + showTitleBullet?: boolean + + // 是否展示头部的分隔线 + showHeadDivider?: boolean + + // 内容区域的固定高度 + contentHeight?: string | number + + // 标题区域的用户自定义内容 + extra?: React.ReactNode + + // 是否开启自由模式,开启后card 将使用子组件配合使用, 设置此项后 title, subtitle, 等等属性都将失效 + free?: boolean +} +``` + +#### ICompatItemProps + +```typescript +interface ICompatItemProps + extends Exclude, + Partial { + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} +``` + + +#### IFieldState + +```typescript +interface IFieldState { + /**只读属性**/ + + //状态名称,FieldState + displayName?: string + //数据路径 + name: string + //节点路径 + path: string + //是否已经初始化 + initialized: boolean + //是否处于原始态,只有value===intialValues时的时候该状态为true + pristine: boolean + //是否处于合法态,只要errors长度大于0的时候valid为false + valid: boolean + //是否处于非法态,只要errors长度大于0的时候valid为true + invalid: boolean + //是否处于校验态 + validating: boolean + //是否被修改,如果值发生变化,该属性为true,同时在整个字段的生命周期内都会为true + modified: boolean + //是否被触碰 + touched: boolean + //是否被激活,字段触发onFocus事件的时候,它会被触发为true,触发onBlur时,为false + active: boolean + //是否访问过,字段触发onBlur事件的时候,它会被触发为true + visited: boolean + + /**可写属性**/ + + //是否可见,注意:该状态如果为false,那么字段的值不会被提交,同时UI不会显示 + visible: boolean + //是否展示,注意:该状态如果为false,那么字段的值会提交,UI不会展示,类似于表单隐藏域 + display: boolean + //是否可编辑 + editable: boolean + //是否处于loading状态,注意:如果字段处于异步校验时,loading为true + loading: boolean + //字段多参值,比如字段onChange触发时,给事件回调传了多参数据,那么这里会存储所有参数的值 + values: any[] + //字段错误消息 + errors: string[] + //字段告警消息 + warnings: string[] + //字段值,与values[0]是恒定相等 + value: any + //初始值 + initialValue: any + //校验规则,具体类型描述参考后面文档 + rules: ValidatePatternRules[] + //是否必填 + required: boolean + //是否挂载 + mounted: boolean + //是否卸载 + unmounted: boolean + //字段扩展属性 + props: FieldProps +} +``` + + +#### ISchemaFieldComponentProps + +```typescript +interface ISchemaFieldComponentProps extends IFieldState { + schema: Schema + mutators: IMutators + form: IForm + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + +#### ISchemaVirtualFieldComponentProps + +```typescript +interface ISchemaVirtualFieldComponentProps extends IVirtualFieldState { + schema: Schema + form: IForm + children: React.ReactElement[] + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + +#### ISchemaFieldWrapper + +```typescript +interface ISchemaFieldWrapper { + (Traget: ISchemaFieldComponent): + | React.FC + | React.ClassicComponent +} +``` + +#### ISchemaFieldComponent + +```typescript +declare type ISchemaFieldComponent = ComponentWithStyleComponent< + ISchemaFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} +``` + +#### ISchemaVirtualFieldComponent + +```typescript +declare type ISchemaVirtualFieldComponent = ComponentWithStyleComponent< + ISchemaVirtualFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} +``` + +#### ISchemaFormRegistry + +```typescript +interface ISchemaFormRegistry { + fields: { + [key: string]: ISchemaFieldComponent + } + virtualFields: { + [key: string]: ISchemaVirtualFieldComponent + } + wrappers?: ISchemaFieldWrapper[] + formItemComponent: React.JSXElementConstructor + formComponent: string | React.JSXElementConstructor +} +``` + +#### INextSchemaFieldProps + +```typescript +interface INextSchemaFieldProps { + name?: string; + /** ISchema **/ + title?: SchemaMessage; + description?: SchemaMessage; + default?: any; + readOnly?: boolean; + writeOnly?: boolean; + type?: 'string' | 'object' | 'array' | 'number' | string; + enum?: Array; + const?: any; + multipleOf?: number; + maximum?: number; + exclusiveMaximum?: number; + minimum?: number; + exclusiveMinimum?: number; + maxLength?: number; + minLength?: number; + pattern?: string | RegExp; + maxItems?: number; + minItems?: number; + uniqueItems?: boolean; + maxProperties?: number; + minProperties?: number; + required?: string[] | boolean; + format?: string; + properties?: { + [key: string]: ISchema; + }; + items?: ISchema | ISchema[]; + additionalItems?: ISchema; + patternProperties?: { + [key: string]: ISchema; + }; + additionalProperties?: ISchema; + editable?: boolean; + visible?: boolean; + display?: boolean; + ['x-props']?: { + [name: string]: any; + }; + ['x-index']?: number; + ['x-rules']?: ValidatePatternRules; + ['x-component']?: string; + ['x-component-props']?: { + [name: string]: any; + }; + ['x-render']?: (props: T & { + renderComponent: () => React.ReactElement; + }) => React.ReactElement; + ['x-effect']?: (dispatch: (type: string, payload: any) => void, option?: object) => { + [key: string]: any; + }; + +``` + +#### IPreviewTextProps + +```typescript +interface IPreviewTextProps { + className?: React.ReactText + dataSource?: any[] + value?: any + addonBefore?: React.ReactNode + innerBefore?: React.ReactNode + addonTextBefore?: React.ReactNode + addonTextAfter?: React.ReactNode + innerAfter?: React.ReactNode + addonAfter?: React.ReactNode +} + +``` + +#### IMutators + +```typescript +interface IMutators { + change: (value: V)=> void,//改变当前字段值 + dispatch: (name: string, payload : any) => void,//触发effect事件 + errors: (errors: string | Array, ...formatValues: Array) => void,//设置当前字段的错误消息 + push(value: V),//对当前字段的值做push操作 + pop(),//对当前字段的值做pop操作 + insert(index: number,value: V),//对当前字段的值做insert操作 + remove(name : string),//对当前字段的值做remove操作 + unshift(value : V),//对当前字段值做unshift操作 + shift(),//对当前字段值做shift操作 + move(fromIndex: number, toIndex: number)//对当前字段值做move操作 +} +``` + +#### IFieldProps + +```typescript +interface IFieldProps { + name : string //字段数据路径 + path : Array //字段数组数据路径 + value : V //字段值 + errors : Array //字段错误消息集合 + editable : boolean | ((name:string) => boolean) //字段是否可编辑 + locale : Locale //国际化文案对象 + loading : boolean //是否处于加载态 + schemaPath : Array //schema path,考虑到有些schema其实是不占数据路径的,所以这个路径是真实路径 + getSchema : (path: string) => ISchema //根据路径获取schema + renderField : (childKey: string, reactKey: string | number) => JSX.Element | string | null //根据childKey渲染当前字段的子字段 + renderComponent : React.FunctionComponent | undefined>,//渲染当前字段的组件,对于x-render来说,可以借助它快速实现渲染包装功能 + getOrderProperties : () => Array<{schema: ISchema, key: number, path: string, name: string }>,//根据properties里字段的x-index值求出排序后的properties + mutators : Mutators,//数据操作对象 + schema : ISchema +} + +``` + +```typescript + +interface IConnectOptions { + //控制表单组件 + valueName?: string + //事件名称 + eventName?: string + //默认props + defaultProps?: Partial + //取值函数,有些场景我们的事件函数取值并不是事件回调的第一个参数,需要做进一步的定制 + getValueFromEvent?: (props: IFieldProps['x-props'], event: Event, ...args: any[]) => any + //字段组件props transformer + getProps?: (connectProps: IConnectProps, fieldProps: IFieldProps) => IConnectProps + //字段组件component transformer + getComponent?: ( + target: T, + connectProps: IConnectProps, + fieldProps: IFieldProps + ) => T +} + +``` \ No newline at end of file diff --git a/packages/next/src/components/Button.tsx b/packages/next/src/components/Button.tsx index 9a3b67750b4..1b167eebd3e 100644 --- a/packages/next/src/components/Button.tsx +++ b/packages/next/src/components/Button.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { FormSpy, LifeCycleTypes } from '@uform/react-schema-renderer' +import { FormSpy, LifeCycleTypes, createVirtualBox } from '@uform/react-schema-renderer' import { Button } from '@alifd/next' import { ButtonProps } from '@alifd/next/types/button' import { ISubmitProps, IResetProps } from '../types' @@ -100,3 +100,8 @@ export const Reset: React.FC = ({ ) } + +createVirtualBox('reset', Reset) +createVirtualBox('text-button', TextButton) +createVirtualBox('submit', Submit) +createVirtualBox('circle-button', CircleButton) diff --git a/packages/next/src/components/FormButtonGroup.tsx b/packages/next/src/components/FormButtonGroup.tsx index 2faea71c2a4..f63d23eb80c 100644 --- a/packages/next/src/components/FormButtonGroup.tsx +++ b/packages/next/src/components/FormButtonGroup.tsx @@ -5,6 +5,7 @@ import cls from 'classnames' import styled from 'styled-components' import { useFormItem } from '../compat/context' import { IFormButtonGroupProps } from '../types' +import { createVirtualBox } from '@uform/react-schema-renderer' const { Row, Col } = Grid @@ -155,3 +156,8 @@ export const FormButtonGroup = styled( } } ` + +createVirtualBox>( + 'button-group', + FormButtonGroup, +) \ No newline at end of file diff --git a/packages/next/src/types.ts b/packages/next/src/types.ts index 9caa9688002..be1312f0060 100644 --- a/packages/next/src/types.ts +++ b/packages/next/src/types.ts @@ -11,10 +11,10 @@ import { StyledComponent } from 'styled-components' type ColSpanType = number | string -export type INextSchemaFormProps = ISchemaFormProps & - FormProps & +export type INextSchemaFormProps = FormProps & IFormItemTopProps & - PreviewTextConfigProps + PreviewTextConfigProps & + ISchemaFormProps export type INextSchemaFieldProps = IMarkupSchemaFieldProps diff --git a/packages/react-schema-renderer/src/components/SchemaMarkup.tsx b/packages/react-schema-renderer/src/components/SchemaMarkup.tsx index c61e25819de..f8d5c1df104 100644 --- a/packages/react-schema-renderer/src/components/SchemaMarkup.tsx +++ b/packages/react-schema-renderer/src/components/SchemaMarkup.tsx @@ -78,7 +78,10 @@ export function createVirtualBox( component ? ({ schema, children }) => { const props = schema.getExtendsComponentProps() - return React.createElement(component, props, children) + return React.createElement(component, { + children, + ...props, + }) } : () => ) diff --git a/packages/react/README.md b/packages/react/README.md index 40d16020d9d..5e2329e4b38 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -51,6 +51,8 @@ npm install --save @uform/react - [`(deprecated,pls using )`](#formconsumerdeprecated,pls-using-formspy) - [Hook](#Hook) - [`useFormEffects`](#useFormEffects) + - [`useFormState`](#useFormState) + - [`useFieldState`](#useFieldState) - [`useForm`](#useForm) - [`useField`](#useField) - [`useVirtualField`](#useVirtualField) @@ -79,6 +81,8 @@ npm install --save @uform/react - [`ValidateDescription`](#ValidateDescription) - [`ValidateArrayRules`](#ValidateArrayRules) - [`ValidatePatternRules`](#ValidatePatternRules) + - [`IFieldAPI`](#IFieldAPI) + - [`IVirtualFieldAPI`](#IVirtualFieldAPI) ### Usage @@ -1943,6 +1947,12 @@ interface IFormConsumerProps { > Implement local effects by using useFormEffects. Same effect as the example of [Linkage](#Linkage) +**Signature** + +```typescript +(effects: IFormEffect): void +``` + ```jsx import React from 'react' import ReactDOM from 'react-dom' @@ -2030,6 +2040,184 @@ const App = () => { ReactDOM.render(, document.getElementById('root')) ``` +#### `useFormState` + +> 使用 useFormState 为自定义组件提供FormState扩展和管理能力 + +**签名** + +```typescript +(defaultState: T): [state: IFormState, setFormState: (state?: IFormState) => void] +``` + +**用法** + +```jsx +import React, { useRef } from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, VirtualField, + createFormActions, createEffectHook, + useForm, + useFormState, + useFormEffects, + useFieldState, + LifeCycleTypes +} from '@uform/react' + +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + +const actions = createFormActions() +const FormFragment = (props) => { + const [formState, setFormState ] = useFormState({ extendVar: 0 }) + const { extendVar } = formState + + return
    + +
    count: {extendVar}
    +
    +} + +const App = () => { + return ( +
    + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### `useFieldState` + +> Manage state of custom field by using `useFieldState` + +**Signature** + +```typescript +(defaultState: T): [state: IFieldState, setFieldState: (state?: IFieldState) => void] +``` + +```jsx +import React, { useRef } from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, VirtualField, + createFormActions, createEffectHook, + useForm, + useFormEffects, + useFieldState, + LifeCycleTypes +} from '@uform/react' + +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + +const changeTab$ = createEffectHook('changeTab') +const actions = createFormActions() +const TabFragment = (props) => { + const [fieldState, setLocalFieldState ] = useFieldState({ current: 0 }) + const { current } = fieldState + const { children, dataSource, form } = props + const ref = useRef(current) + + const update = (cur) => { + form.notify('changeTab', cur) + setLocalFieldState({ + current: cur + }) + } + + useFormEffects(($, { setFieldState }) => { + dataSource.forEach((item, itemIdx) => { + setFieldState(item.name, state => { + state.display = itemIdx === current + }) + }) + + changeTab$().subscribe((idx) => { + dataSource.forEach((item, itemIdx) => { + setFieldState(item.name, state => { + state.display = itemIdx === idx + }) + }) + }) + }) + + ref.current = current + const btns = dataSource.map((item, idx) => { + console.log('current', current, ref.current) + const focusStyle = idx === current ? { color: '#fff', background: 'blue' } : {} + return + }) + + return btns +} + +const FormTab = (props) => { + return + {({ form }) => { + return + }} + +} + +const App = () => { + return ( +
    + +
    + + +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + #### useForm > get [IForm](#IForm) instance @@ -2274,6 +2462,12 @@ const App = () => { > Custom your own hook by this api +**Signature** + +```typescript +(type: string): Observable +``` + **Usage** ```jsx @@ -3036,3 +3230,25 @@ declare type ValidatePatternRules = | ValidateDescription | ValidateArrayRules ``` + + +#### IFieldAPI + +```typescript +interface IFieldAPI { + state: IFieldState + form: IForm + props: {} + mutators: IMutators +} +``` + +#### IVirtualFieldAPI + +```typescript +interface IVirtualFieldAPI { + state: IFieldState + form: IForm + props: {} +} +``` \ No newline at end of file diff --git a/packages/react/README.zh-cn.md b/packages/react/README.zh-cn.md index 11b110db0d2..7484ab00c1f 100644 --- a/packages/react/README.zh-cn.md +++ b/packages/react/README.zh-cn.md @@ -49,6 +49,8 @@ npm install --save @uform/react - [`(即将废弃,请使用)`](<#FormConsumer(即将废弃,请使用FormSpy)>) - [Hook](#Hook) - [`useFormEffects`](#useFormEffects) + - [`useFormState`](#useFormState) + - [`useFieldState`](#useFieldState) - [`useForm`](#useForm) - [`useField`](#useField) - [`useVirtualField`](#useVirtualField) @@ -77,6 +79,8 @@ npm install --save @uform/react - [`ValidateDescription`](#ValidateDescription) - [`ValidateArrayRules`](#ValidateArrayRules) - [`ValidatePatternRules`](#ValidatePatternRules) + - [`IFieldAPI`](#IFieldAPI) + - [`IVirtualFieldAPI`](#IVirtualFieldAPI) ### 使用方式 @@ -1983,6 +1987,12 @@ interface IFormConsumerProps { > 使用 useFormEffects 可以实现局部effect的表单组件,效果同:[简单联动](#简单联动) +**签名** + +```typescript +(effects: IFormEffect): void +``` + ```jsx import React from 'react' import ReactDOM from 'react-dom' @@ -2070,6 +2080,181 @@ const App = () => { ReactDOM.render(, document.getElementById('root')) ``` +#### `useFormState` + +> 使用 useFormState 为自定义组件提供FormState扩展和管理能力 + +**签名** + +```typescript +(defaultState: T): [state: IFormState, setFormState: (state?: IFormState) => void] +``` + +**用法** + +```jsx +import React, { useRef } from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, VirtualField, + createFormActions, createEffectHook, + useForm, + useFormState, + useFormEffects, + useFieldState, + LifeCycleTypes +} from '@uform/react' + +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + +const actions = createFormActions() +const FormFragment = (props) => { + const [formState, setFormState ] = useFormState({ extendVar: 0 }) + const { extendVar } = formState + + return
    + +
    count: {extendVar}
    +
    +} + +const App = () => { + return ( +
    + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### `useFieldState` + +> 使用 useFieldState 为自定义组件提供状态管理能力 + +**签名** + +```typescript +(defaultState: T): [state: IFieldState, setFieldState: (state?: IFieldState) => void] +``` + +```jsx +import React, { useRef } from 'react' +import ReactDOM from 'react-dom' +import { Form, Field, VirtualField, + createFormActions, createEffectHook, + useForm, + useFormEffects, + useFieldState, + LifeCycleTypes +} from '@uform/react' + +const InputField = props => ( + + {({ state, mutators }) => { + const loading = state.props.loading + return + { props.label && } + { loading ? ' loading... ' : } + {state.errors} + {state.warnings} + + }} + +) + +const changeTab$ = createEffectHook('changeTab') +const actions = createFormActions() +const TabFragment = (props) => { + const [fieldState, setLocalFieldState ] = useFieldState({ current: 0 }) + const { current } = fieldState + const { children, dataSource, form } = props + + const update = (cur) => { + form.notify('changeTab', cur) + setLocalFieldState({ + current: cur + }) + } + + useFormEffects(($, { setFieldState }) => { + dataSource.forEach((item, itemIdx) => { + setFieldState(item.name, state => { + state.display = itemIdx === current + }) + }) + + changeTab$().subscribe((idx) => { + dataSource.forEach((item, itemIdx) => { + setFieldState(item.name, state => { + state.display = itemIdx === idx + }) + }) + }) + }) + + const btns = dataSource.map((item, idx) => { + const focusStyle = idx === current ? { color: '#fff', background: 'blue' } : {} + return + }) + + return btns +} + +const FormTab = (props) => { + return + {({ form }) => { + return + }} + +} + +const App = () => { + return ( +
    + +
    + + +
    + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + #### useForm > 获取一个 [IForm](#IForm) 实例 @@ -2293,7 +2478,13 @@ const App = () => { > 自定义hook -**Usage** +**签名** + +```typescript +(type: string): Observable +``` + +**用法** ```jsx import { Form, createEffectHook, createFormActions } from '@uform/react' @@ -3104,3 +3295,25 @@ declare type ValidatePatternRules = | ValidateDescription | ValidateArrayRules ``` + + +#### IFieldAPI + +```typescript +interface IFieldAPI { + state: IFieldState + form: IForm + props: {} + mutators: IMutators +} +``` + +#### IVirtualFieldAPI + +```typescript +interface IVirtualFieldAPI { + state: IFieldState + form: IForm + props: {} +} +``` \ No newline at end of file diff --git a/packages/react/src/hooks/useFormState.ts b/packages/react/src/hooks/useFormState.ts index 3d246aa1bb1..618e8b1840c 100644 --- a/packages/react/src/hooks/useFormState.ts +++ b/packages/react/src/hooks/useFormState.ts @@ -5,7 +5,7 @@ import FormContext from '../context' export const useFormState = (defaultState: T) => { const forceUpdate = useForceUpdate() - const ref = useRef<{ state: IFormState; subscribeId: number }>() + const ref = useRef<{ state?: IFormState; subscribeId?: number }>({}) const form = useContext(FormContext) ref.current.subscribeId = useMemo(() => { form.setFormState(state => { From 15b6b43e382fa6a7982f67146e3a9a5e807fadf2 Mon Sep 17 00:00:00 2001 From: Janry Date: Wed, 27 Nov 2019 14:54:24 +0800 Subject: [PATCH 80/99] feat(@uform/next): update next features (#439) * feat(@uform/next): update next features * fix(@uform/antd): fix typings --- docs/Examples/next/Sample.md | 13 ++++++++++++- package.json | 2 +- packages/antd/src/types.ts | 2 +- packages/next/package.json | 4 ++-- packages/next/src/compat/FormItem.tsx | 7 +++---- packages/next/src/shared.ts | 4 +++- packages/next/src/types.ts | 2 +- scripts/doc-renderer.js | 10 ---------- scripts/docs.js | 2 +- 9 files changed, 24 insertions(+), 22 deletions(-) diff --git a/docs/Examples/next/Sample.md b/docs/Examples/next/Sample.md index 22c7bb01801..f620129b629 100644 --- a/docs/Examples/next/Sample.md +++ b/docs/Examples/next/Sample.md @@ -77,6 +77,14 @@ ReactDOM.render( title="TextArea" name="textarea" x-component="textarea" + x-rules={value => { + return value > 20 + ? { + type: 'warning', + message: '这是个警告信息' + } + : '' + }} /> @@ -115,7 +123,10 @@ ReactDOM.render( /> diff --git a/package.json b/package.json index cb9f4d0a911..a0e8747e959 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@types/react": "16.8.23" }, "devDependencies": { - "@alifd/next": "^1.14.2", + "@alifd/next": "^1.19.1", "@babel/cli": "^7.2.0", "@babel/core": "^7.3.3", "@babel/plugin-proposal-class-properties": "^7.2.3", diff --git a/packages/antd/src/types.ts b/packages/antd/src/types.ts index 933ca9e03c5..a37bdaf7f74 100644 --- a/packages/antd/src/types.ts +++ b/packages/antd/src/types.ts @@ -45,7 +45,7 @@ export type IFormItemTopProps = React.PropsWithChildren< > export interface ICompatItemProps - extends Exclude, + extends Omit, Partial { labelCol?: number | { span: number; offset?: number } wrapperCol?: number | { span: number; offset?: number } diff --git a/packages/next/package.json b/packages/next/package.json index 12d583ea15f..343589c89c5 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -20,7 +20,7 @@ "build": "ts-node --project ../../tsconfig.build.json build.ts" }, "peerDependencies": { - "@alifd/next": "^1.13.1", + "@alifd/next": "^1.19.1", "@babel/runtime": "^7.4.4", "@types/classnames": "^2.2.9", "@types/styled-components": "^4.1.19", @@ -38,7 +38,7 @@ "styled-components": "^4.1.1" }, "devDependencies": { - "@alifd/next": "^1.13.1", + "@alifd/next": "^1.19.1", "@types/classnames": "^2.2.9", "@types/styled-components": "^4.1.19" }, diff --git a/packages/next/src/compat/FormItem.tsx b/packages/next/src/compat/FormItem.tsx index 1c05d4a6be7..4faac053352 100644 --- a/packages/next/src/compat/FormItem.tsx +++ b/packages/next/src/compat/FormItem.tsx @@ -12,10 +12,9 @@ const computeStatus = (props: ICompatItemProps) => { if (props.invalid) { return 'error' } - //todo:暂时不支持 - // if (props.warnings.length) { - // return 'warning' - // } + if (props.warnings.length) { + return 'warning' + } } const computeHelp = (props: ICompatItemProps) => { diff --git a/packages/next/src/shared.ts b/packages/next/src/shared.ts index ad04287e8e8..fee454143d7 100644 --- a/packages/next/src/shared.ts +++ b/packages/next/src/shared.ts @@ -49,11 +49,13 @@ export const mapStyledProps = ( props: IConnectProps, fieldProps: MergedFieldComponentProps ) => { - const { loading, errors } = fieldProps + const { loading, errors, warnings } = fieldProps if (loading) { props.state = props.state || 'loading' } else if (errors && errors.length) { props.state = 'error' + } else if (warnings && warnings.length) { + props.state = 'warning' } } diff --git a/packages/next/src/types.ts b/packages/next/src/types.ts index be1312f0060..0a0866a88c2 100644 --- a/packages/next/src/types.ts +++ b/packages/next/src/types.ts @@ -50,7 +50,7 @@ export type IFormItemTopProps = React.PropsWithChildren< > export interface ICompatItemProps - extends Exclude, + extends Omit, Partial { labelCol?: number | { span: number; offset?: number } wrapperCol?: number | { span: number; offset?: number } diff --git a/scripts/doc-renderer.js b/scripts/doc-renderer.js index a0dd1a255da..4e462bf2e84 100644 --- a/scripts/doc-renderer.js +++ b/scripts/doc-renderer.js @@ -1,16 +1,6 @@ import React from 'react' import SiteRenderer from 'react-site-renderer' -;((window.gitter = {}).chat = {}).options = { - room: 'alibaba-uform/community' -} - -setTimeout(() => { - const script = document.createElement('script') - script.src = '//sidecar.gitter.im/dist/sidecar.v1.js' - document.body.appendChild(script) -}, 1000) - export default ({ docs }) => { return ( - + ` const createDocs = async () => { From 3bfe515ba8f403907efa09ecc3e4c45cde4c115d Mon Sep 17 00:00:00 2001 From: quirkyvar Date: Wed, 27 Nov 2019 15:20:55 +0800 Subject: [PATCH 81/99] fix(@uform/next): formitem compatibility (#440) --- packages/next/src/compat/FormItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/src/compat/FormItem.tsx b/packages/next/src/compat/FormItem.tsx index 4faac053352..52fe1051335 100644 --- a/packages/next/src/compat/FormItem.tsx +++ b/packages/next/src/compat/FormItem.tsx @@ -12,7 +12,7 @@ const computeStatus = (props: ICompatItemProps) => { if (props.invalid) { return 'error' } - if (props.warnings.length) { + if (props.warnings && props.warnings.length) { return 'warning' } } From a6ce4359763134f22d6a1408b3e0f40f4856e36b Mon Sep 17 00:00:00 2001 From: Janry Date: Wed, 27 Nov 2019 15:54:44 +0800 Subject: [PATCH 82/99] update readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index da43d55b5ee..c3fd3b47695 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,10 @@ Use uform core package: npm install --save @uform/core ``` +## Architecture + +![](https://img.alicdn.com/tfs/TB1i9nmolv0gK0jSZKbXXbK2FXa-1882-1144.png) + ## WebSite From c0701f56cfb78abaafd8e97578708e290554f000 Mon Sep 17 00:00:00 2001 From: Janry Date: Wed, 27 Nov 2019 15:55:46 +0800 Subject: [PATCH 83/99] update readme --- README.zh-cn.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.zh-cn.md b/README.zh-cn.md index e346234903b..98d26ed5c3d 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -63,6 +63,10 @@ npm install --save @uform/core https://uformjs.org +## 架构 + +![](https://img.alicdn.com/tfs/TB1i9nmolv0gK0jSZKbXXbK2FXa-1882-1144.png) + ## 文档 - [@uform/antd](./packages/antd/README.zh-cn.md) From 802d2f7d9eeb4d38688f009c972775d1a1a00c4a Mon Sep 17 00:00:00 2001 From: janrywang Date: Wed, 27 Nov 2019 16:02:18 +0800 Subject: [PATCH 84/99] chore(publish): v1.0.0-alpha.3 --- packages/antd/package.json | 10 +++++----- packages/core/package.json | 6 +++--- packages/next/package.json | 8 ++++---- packages/printer/package.json | 4 ++-- packages/react-schema-renderer/package.json | 8 ++++---- packages/react-shared-components/package.json | 4 ++-- packages/react/package.json | 6 +++--- packages/shared/package.json | 2 +- packages/validator/package.json | 4 ++-- 9 files changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/antd/package.json b/packages/antd/package.json index 1e15b216d89..72ea00d2611 100644 --- a/packages/antd/package.json +++ b/packages/antd/package.json @@ -1,6 +1,6 @@ { "name": "@uform/antd", - "version": "1.0.0-alpha.2", + "version": "1.0.0-alpha.3", "license": "MIT", "main": "lib", "module": "esm", @@ -30,12 +30,12 @@ "react-dom": ">=16.8.0" }, "dependencies": { - "@uform/react-schema-renderer": "^1.0.0-alpha.2", - "@uform/react-shared-components": "^1.0.0-alpha.2", - "@uform/shared": "^1.0.0-alpha.2", + "@uform/react-schema-renderer": "^1.0.0-alpha.3", + "@uform/react-shared-components": "^1.0.0-alpha.3", + "@uform/shared": "^1.0.0-alpha.3", "classnames": "^2.2.6", - "react-stikky": "^0.1.15", "react-eva": "^1.0.0-alpha.0", + "react-stikky": "^0.1.15", "rxjs": "^6.5.1", "styled-components": "^4.1.1" }, diff --git a/packages/core/package.json b/packages/core/package.json index 09b2e8ffd67..ab81246ea97 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@uform/core", - "version": "1.0.0-alpha.2", + "version": "1.0.0-alpha.3", "license": "MIT", "main": "lib", "repository": { @@ -26,9 +26,9 @@ "scheduler": ">=0.11.2" }, "dependencies": { - "@uform/shared": "^1.0.0-alpha.2", + "@uform/shared": "^1.0.0-alpha.3", "@uform/types": "^0.4.0", - "@uform/validator": "^1.0.0-alpha.2", + "@uform/validator": "^1.0.0-alpha.3", "immer": "^3.2.0" }, "publishConfig": { diff --git a/packages/next/package.json b/packages/next/package.json index 343589c89c5..d35ec1ce615 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@uform/next", - "version": "1.0.0-alpha.2", + "version": "1.0.0-alpha.3", "license": "MIT", "main": "lib", "module": "esm", @@ -28,9 +28,9 @@ "react-dom": ">=16.8.0" }, "dependencies": { - "@uform/react-schema-renderer": "^1.0.0-alpha.2", - "@uform/react-shared-components": "^1.0.0-alpha.2", - "@uform/shared": "^1.0.0-alpha.2", + "@uform/react-schema-renderer": "^1.0.0-alpha.3", + "@uform/react-shared-components": "^1.0.0-alpha.3", + "@uform/shared": "^1.0.0-alpha.3", "classnames": "^2.2.6", "react-eva": "^1.0.0-alpha.0", "react-stikky": "^0.1.15", diff --git a/packages/printer/package.json b/packages/printer/package.json index b449289db8a..b7d3c36428b 100644 --- a/packages/printer/package.json +++ b/packages/printer/package.json @@ -1,6 +1,6 @@ { "name": "@uform/printer", - "version": "1.0.0-alpha.2", + "version": "1.0.0-alpha.3", "license": "MIT", "main": "lib", "repository": { @@ -26,7 +26,7 @@ "typescript": "^3.5.2" }, "dependencies": { - "@uform/react-schema-renderer": "^1.0.0-alpha.2", + "@uform/react-schema-renderer": "^1.0.0-alpha.3", "react-modal": "^3.8.1", "styled-components": "^4.1.1" }, diff --git a/packages/react-schema-renderer/package.json b/packages/react-schema-renderer/package.json index d685e0d3cf8..0e2e4bfab3a 100644 --- a/packages/react-schema-renderer/package.json +++ b/packages/react-schema-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@uform/react-schema-renderer", - "version": "1.0.0-alpha.2", + "version": "1.0.0-alpha.3", "license": "MIT", "main": "lib", "repository": { @@ -30,9 +30,9 @@ "react-eva": "^1.1.7" }, "dependencies": { - "@uform/react": "^1.0.0-alpha.2", - "@uform/shared": "^1.0.0-alpha.2", - "@uform/validator": "^1.0.0-alpha.2", + "@uform/react": "^1.0.0-alpha.3", + "@uform/shared": "^1.0.0-alpha.3", + "@uform/validator": "^1.0.0-alpha.3", "pascal-case": "^2.0.1" }, "publishConfig": { diff --git a/packages/react-shared-components/package.json b/packages/react-shared-components/package.json index b4839807dd3..1c0d45c20c7 100644 --- a/packages/react-shared-components/package.json +++ b/packages/react-shared-components/package.json @@ -1,6 +1,6 @@ { "name": "@uform/react-shared-components", - "version": "1.0.0-alpha.2", + "version": "1.0.0-alpha.3", "license": "MIT", "main": "lib", "types": "lib/index.d.ts", @@ -29,7 +29,7 @@ "@babel/runtime": "^7.4.4" }, "dependencies": { - "@uform/shared": "^1.0.0-alpha.2", + "@uform/shared": "^1.0.0-alpha.3", "@uform/types": "^0.4.0" } } diff --git a/packages/react/package.json b/packages/react/package.json index 6b670869448..05639ded840 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@uform/react", - "version": "1.0.0-alpha.2", + "version": "1.0.0-alpha.3", "license": "MIT", "main": "lib", "repository": { @@ -31,8 +31,8 @@ "scheduler": ">=0.11.2" }, "dependencies": { - "@uform/core": "^1.0.0-alpha.2", - "@uform/shared": "^1.0.0-alpha.2" + "@uform/core": "^1.0.0-alpha.3", + "@uform/shared": "^1.0.0-alpha.3" }, "publishConfig": { "access": "public" diff --git a/packages/shared/package.json b/packages/shared/package.json index d712418a99d..29bdd95cdbe 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@uform/shared", - "version": "1.0.0-alpha.2", + "version": "1.0.0-alpha.3", "license": "MIT", "main": "lib", "types": "lib/index.d.ts", diff --git a/packages/validator/package.json b/packages/validator/package.json index e48bc188168..47f5625a72a 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -1,6 +1,6 @@ { "name": "@uform/validator", - "version": "1.0.0-alpha.2", + "version": "1.0.0-alpha.3", "license": "MIT", "main": "lib", "repository": { @@ -25,7 +25,7 @@ "@babel/runtime": "^7.4.4" }, "dependencies": { - "@uform/shared": "^1.0.0-alpha.2", + "@uform/shared": "^1.0.0-alpha.3", "@uform/types": "^0.4.0" }, "publishConfig": { From bf144780969285a6e6a598c0414d602731e4f589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E5=AD=90=E6=AF=85?= <576625322@qq.com> Date: Thu, 28 Nov 2019 11:57:27 +0800 Subject: [PATCH 85/99] =?UTF-8?q?=E8=A1=A8=E5=8D=95=20editor=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E8=8F=9C=E5=8D=95=E6=A0=91=E5=8A=9F=E8=83=BD=20(#441)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: merge master * feat: editor 新增菜单树 * feat: fix bug --- package.json | 1 + packages/react-schema-editor/README.md | 46 +++++- .../src/components/FieldEditor.tsx | 38 +---- .../src/components/SchemaTree.tsx | 132 ++++++++++++++++-- packages/react-schema-editor/src/index.tsx | 17 ++- .../react-schema-editor/src/utils/types.ts | 4 +- 6 files changed, 182 insertions(+), 56 deletions(-) diff --git a/package.json b/package.json index a0e8747e959..e2fd4aa3c35 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,7 @@ ] }, "dependencies": { + "lodash": "^4.17.15", "opencollective": "^1.0.3", "opencollective-postinstall": "^2.0.2" }, diff --git a/packages/react-schema-editor/README.md b/packages/react-schema-editor/README.md index a2a2ac112fc..ed77b8ffd86 100644 --- a/packages/react-schema-editor/README.md +++ b/packages/react-schema-editor/README.md @@ -6,5 +6,49 @@ import React from 'react' import { SchemaEditor } from './src' -ReactDOM.render(, document.getElementById('root')) +function SchemaEditorDemo() { + const [schema, setSchema] = React.useState({ + type: 'object', + title: '我是表单标题', + description: '我是表单描述', + properties: { + fieldA: { + type: 'object', + title: '我是层级嵌套标题', + description: '我是层级嵌套描述', + properties: { + arrayA: { + type: 'array', + title: '动物', + description: '我是字段描述', + component: 'Select', + items: { + type: 'string', + enum: ['Dog', 'Cat', 'Horse'] + } + }, + numberB: { + type: 'number', + title: '年龄' + }, + objectC: { + type: 'object' + } + } + } + } + }) + + return ( + + ) +} + +ReactDOM.render( + , + document.getElementById('root') +) ``` diff --git a/packages/react-schema-editor/src/components/FieldEditor.tsx b/packages/react-schema-editor/src/components/FieldEditor.tsx index 775e3f58ebe..d5fd4ccff12 100644 --- a/packages/react-schema-editor/src/components/FieldEditor.tsx +++ b/packages/react-schema-editor/src/components/FieldEditor.tsx @@ -1,4 +1,4 @@ -import { Input, Select, Checkbox, Button, Icon } from '@alifd/next' +import { Input, Select, Checkbox, Button, Icon } from 'antd' import React from 'react' const typeOptions = [ @@ -24,42 +24,16 @@ export const FieldEditor: React.FC = ({ children }) => {
    - - + + - - - + + +
    {children}
    diff --git a/packages/react-schema-editor/src/components/SchemaTree.tsx b/packages/react-schema-editor/src/components/SchemaTree.tsx index 62c98bcf7ff..31f3250e645 100644 --- a/packages/react-schema-editor/src/components/SchemaTree.tsx +++ b/packages/react-schema-editor/src/components/SchemaTree.tsx @@ -1,21 +1,129 @@ -import React from 'react' +import React, { useCallback } from 'react' +import { Tree, Row, Col } from 'antd' import { ISchemaTreeProps } from '../utils/types' -import { FieldEditor } from './FieldEditor' +import * as fp from 'lodash/fp' + +const TreeNode = Tree.TreeNode export const SchemaTree: React.FC = ({ schema, onChange }) => { + const [selectedPath, setSelectedPath] = React.useState(null) + + const handleSelect = React.useCallback((path: string[]) => { + setSelectedPath(path[0]) + }, []) + + const handleDrop = React.useCallback( + (info: any) => { + const sourcePath = info.dragNode.props.eventKey + const sourceKeys = sourcePath.split('.') + const sourceKey = sourceKeys[sourceKeys.length - 1] + const targetPath = info.node.props.eventKey + const sourceValue = fp.get(sourcePath, schema) + const targetValue = + targetPath === 'root' ? schema : fp.get(targetPath, schema) + + if (!targetValue) { + return + } + + if (info.dropToGap) { + // 拖拽到这个元素的同级 + // info.dropPosition -1 表示上方同级,1 表示下方同级 + } else { + // 拖拽到这个元素内部 + if (targetValue.type !== 'object') { + // 只有 object 才能被拖入 + return + } + + if ( + (targetPath === 'root' && sourcePath.split('.').length === 2) || + (targetPath !== 'root' && + fp.dropRight(2, sourcePath.split('.')).join('.') === targetPath) + ) { + // 拖拽到直接父节点,等于不起作用 + return + } + + let newSchema = schema + + // 增加新的 key + const newTargetValue = fp.set( + ['properties', sourceKey], + sourceValue, + targetValue + ) + + newSchema = + targetPath === 'root' + ? newTargetValue + : fp.set(targetPath, newTargetValue, newSchema) + + // 删除旧的 key + newSchema = fp.unset(sourcePath, newSchema) + + onChange(newSchema) + } + }, + [schema, onChange] + ) + + const selectedSchema = + selectedPath && + (selectedPath === 'root' ? schema : fp.get(selectedPath, schema)) + return ( -
    - - - - - - - - -
    + + + + {TreeNodeBySchema({ schema, path: [] })} + + + +
    {selectedSchema && JSON.stringify(selectedSchema, null, 2)}
    + +
    ) } + +const TreeNodeBySchema: React.FC<{ + schema: any + path: string[] +}> = ({ schema, path }) => { + if (!schema) { + return null + } + + const currentTreeLevelProps = { + title: path.length === 0 ? 'root' : path[path.length - 1], + key: path.length === 0 ? 'root' : path.join('.') + } + + switch (schema.type) { + case 'object': + return ( + + {schema.properties && + Object.keys(schema.properties).map(key => + TreeNodeBySchema({ + schema: schema.properties[key], + path: path.concat('properties', key) + }) + )} + + ) + case 'array': + default: + } + + return +} diff --git a/packages/react-schema-editor/src/index.tsx b/packages/react-schema-editor/src/index.tsx index 9002ec9445d..07376adbfab 100644 --- a/packages/react-schema-editor/src/index.tsx +++ b/packages/react-schema-editor/src/index.tsx @@ -1,15 +1,14 @@ import React from 'react' -import { Button, Icon, Grid } from '@alifd/next' -import { jsonToSchema } from './utils' +import { Button, Icon, Row, Col } from 'antd' import { SchemaTree } from './components/SchemaTree' import { SchemaCode } from './components/SchemaCode' -import json from './utils/schema' -const { Row, Col } = Grid -export const SchemaEditor: React.FC = () => { - const initialSchema = jsonToSchema(json) - const [schema, setSchema] = React.useState(initialSchema) +import 'antd/dist/antd.css' +export const SchemaEditor: React.FC<{ + schema: any + onChange: (schema: any) => void +}> = ({ schema, onChange }) => { return (
    @@ -20,10 +19,10 @@ export const SchemaEditor: React.FC = () => {
    - + - +
    diff --git a/packages/react-schema-editor/src/utils/types.ts b/packages/react-schema-editor/src/utils/types.ts index 640083c8b35..94ae41f86c4 100644 --- a/packages/react-schema-editor/src/utils/types.ts +++ b/packages/react-schema-editor/src/utils/types.ts @@ -4,8 +4,8 @@ export interface ISchemaCodeProps { } export interface ISchemaTreeProps { - schema: object - onChange?: Function + schema: any + onChange?: (newSchema: any) => void } export interface ISchemaPreviewProps { From 0872f40e80b583c037f61b39d52f7c4a07d1f989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=BB=84=E9=BB=84?= Date: Thu, 28 Nov 2019 11:57:44 +0800 Subject: [PATCH 86/99] fix(@uform/react/antd/next): Fix field state errors (#442) * test(@uform/react): improve field and virtualField test cases * fix(@uform/react/antd/next): Fix field state errors * fix(@uform/react): remove unused variable --- packages/antd/src/compat/FormItem.tsx | 13 +++++++++++-- packages/next/src/compat/FormItem.tsx | 13 +++++++++++-- packages/react/src/hooks/useField.ts | 14 ++------------ 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/antd/src/compat/FormItem.tsx b/packages/antd/src/compat/FormItem.tsx index 67d38bcf8e0..fc2c658ce24 100644 --- a/packages/antd/src/compat/FormItem.tsx +++ b/packages/antd/src/compat/FormItem.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext } from 'react' +import React, { createContext, useContext, createElement } from 'react' import { Form } from 'antd' import { useFormItem } from './context' import { IFormItemTopProps, ICompatItemProps } from '../types' @@ -20,7 +20,16 @@ const computeStatus = (props: ICompatItemProps) => { const computeHelp = (props: ICompatItemProps) => { if (props.help) return props.help const messages = [].concat(props.errors || [], props.warnings || []) - return messages.length ? messages : props.schema && props.schema.description + return messages.length + ? messages.map((message, index) => + createElement( + 'span', + { key: index }, + message, + messages.length - 1 > index ? ' ,' : '' + ) + ) + : props.schema && props.schema.description } const computeLabel = (props: ICompatItemProps) => { diff --git a/packages/next/src/compat/FormItem.tsx b/packages/next/src/compat/FormItem.tsx index 52fe1051335..186916ecd27 100644 --- a/packages/next/src/compat/FormItem.tsx +++ b/packages/next/src/compat/FormItem.tsx @@ -1,4 +1,4 @@ -import React, { createContext } from 'react' +import React, { createContext, createElement } from 'react' import { Form } from '@alifd/next' import { useFormItem } from './context' import { IFormItemTopProps, ICompatItemProps } from '../types' @@ -20,7 +20,16 @@ const computeStatus = (props: ICompatItemProps) => { const computeHelp = (props: ICompatItemProps) => { if (props.help) return props.help const messages = [].concat(props.errors || [], props.warnings || []) - return messages.length ? messages : props.schema && props.schema.description + return messages.length + ? messages.map((message, index) => + createElement( + 'span', + { key: index }, + message, + messages.length - 1 > index ? ' ,' : '' + ) + ) + : props.schema && props.schema.description } const computeLabel = (props: ICompatItemProps) => { diff --git a/packages/react/src/hooks/useField.ts b/packages/react/src/hooks/useField.ts index 1afb1c94dd2..d3ff91182e4 100644 --- a/packages/react/src/hooks/useField.ts +++ b/packages/react/src/hooks/useField.ts @@ -1,4 +1,4 @@ -import { createElement, useMemo, useEffect, useRef, useContext } from 'react' +import { useMemo, useEffect, useRef, useContext } from 'react' import { each, isFn } from '@uform/shared' import { IFieldState, IForm, IField, IMutators } from '@uform/core' import { getValueFromEvent } from '../shared' @@ -102,17 +102,7 @@ export const useField = (options: IFieldStateUIProps): IFieldHook => { return { form, field: ref.current.field, - state: { - ...state, - errors: state.errors.map((message, index) => { - return createElement( - 'span', - { key: index }, - message, - state.errors.length - 1 > index ? ' ,' : '' - ) - }) - }, + state, mutators, props: state.props } From 013d42704a7bfba60061d8a6607fabad4c3f64aa Mon Sep 17 00:00:00 2001 From: Janry Date: Thu, 28 Nov 2019 13:00:56 +0800 Subject: [PATCH 87/99] Fix xeffect (#443) * fix(@uform/react-schema-renderer): fix x-effect api spec * fix(@uform/react-schema-renderer): fix test --- .../src/__tests__/x-effect.spec.tsx | 60 +++++++++++++++++++ .../src/shared/connect.ts | 14 ++++- 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 packages/react-schema-renderer/src/__tests__/x-effect.spec.tsx diff --git a/packages/react-schema-renderer/src/__tests__/x-effect.spec.tsx b/packages/react-schema-renderer/src/__tests__/x-effect.spec.tsx new file mode 100644 index 00000000000..1c95ff44c1d --- /dev/null +++ b/packages/react-schema-renderer/src/__tests__/x-effect.spec.tsx @@ -0,0 +1,60 @@ +import React, { Fragment } from 'react' +import { + registerFormField, + connect, + SchemaMarkupForm as SchemaForm, + SchemaMarkupField as Field, + createFormActions +} from '../index' +import { render, wait, fireEvent } from '@testing-library/react' + +registerFormField( + 'string', + connect()(props => ) +) + +test('x-effect', async () => { + const actions = createFormActions() + const TestComponent = () => ( + { + $('onChange', 'aaa').subscribe(({ payload }) => { + setFieldState('bbb', state => { + state.value = payload + }) + }) + }} + actions={actions} + > + + { + return { + onChange() { + dispatch('onChange', '123') + } + } + }} + x-props={{ + 'data-testid': 'aaa' + }} + /> + + + + ) + + const { queryByTestId } = render() + await wait() + fireEvent.change(queryByTestId('aaa'), { + target: { + value: 'hello world' + } + }) + await wait() + expect((queryByTestId('bbb') as any).value).toEqual('123') +}) diff --git a/packages/react-schema-renderer/src/shared/connect.ts b/packages/react-schema-renderer/src/shared/connect.ts index d2958e0c67a..18a89f5222b 100644 --- a/packages/react-schema-renderer/src/shared/connect.ts +++ b/packages/react-schema-renderer/src/shared/connect.ts @@ -30,11 +30,16 @@ const createEnum = (enums: any) => { const bindEffects = ( props: {}, + fieldProps: ISchemaFieldComponentProps, effect: ISchema['x-effect'], notify: (type: string, payload?: any) => void ): any => { each( - effect((type, payload) => notify(type, { payload }), { ...props }), + effect( + (type, payload) => + notify(type, { payload, name: fieldProps.name, path: fieldProps.path }), + { ...props } + ), (event, key) => { const prevEvent = key === 'onChange' ? props[key] : undefined props[key] = (...args: any[]) => { @@ -89,7 +94,12 @@ export const connect = (options?: IConnectOptions) => { const extendsEffect = schema.getExtendsEffect() if (isFn(extendsEffect)) { - componentProps = bindEffects(componentProps, extendsEffect, form.notify) + componentProps = bindEffects( + componentProps, + fieldProps, + extendsEffect, + form.notify + ) } if (isFn(options.getProps)) { From 8b3d633d9a150701df21c8af46411cfb2efac551 Mon Sep 17 00:00:00 2001 From: cds803 Date: Thu, 28 Nov 2019 17:14:14 +0800 Subject: [PATCH 88/99] fix(@uform/antd): fix FormItem supports className (#444) * fix(@uform/antd): fix FormItem supports className * fix(@uform/next): fix FormItem supports className --- packages/antd/src/compat/FormItem.tsx | 1 + packages/next/src/compat/FormItem.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/antd/src/compat/FormItem.tsx b/packages/antd/src/compat/FormItem.tsx index fc2c658ce24..dec52e1c9c0 100644 --- a/packages/antd/src/compat/FormItem.tsx +++ b/packages/antd/src/compat/FormItem.tsx @@ -62,6 +62,7 @@ const computeSchemaExtendProps = ( ...props.schema.getExtendsItemProps(), ...props.schema.getExtendsProps() }, + 'className', 'prefix', 'labelAlign', 'labelTextAlign', diff --git a/packages/next/src/compat/FormItem.tsx b/packages/next/src/compat/FormItem.tsx index 186916ecd27..56b9932ac34 100644 --- a/packages/next/src/compat/FormItem.tsx +++ b/packages/next/src/compat/FormItem.tsx @@ -62,6 +62,7 @@ const computeSchemaExtendProps = ( ...props.schema.getExtendsItemProps(), ...props.schema.getExtendsProps() }, + 'className', 'prefix', 'labelAlign', 'labelTextAlign', From 15a414751ccbf3f7505efab98a40f6b0a8c4840d Mon Sep 17 00:00:00 2001 From: henryybai <1261338990@qq.com> Date: Fri, 29 Nov 2019 13:43:38 +0800 Subject: [PATCH 89/99] =?UTF-8?q?SchemaEditor=20MVP=E7=89=88=20(#446)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/react-schema-editor/README.md | 32 +- .../src/components/FieldEditor.css | 13 + .../src/components/FieldEditor.tsx | 511 +- .../src/components/SchemaTree.tsx | 40 +- packages/react-schema-editor/src/main.scss | 1 + .../src/utils/components.ts | 22779 +++++++++++++++- .../src/utils/fieldEditorHelpers.ts | 150 + .../react-schema-editor/src/utils/schema.ts | 14 +- .../react-schema-editor/src/utils/types.ts | 23 + 9 files changed, 23451 insertions(+), 112 deletions(-) create mode 100644 packages/react-schema-editor/src/components/FieldEditor.css create mode 100644 packages/react-schema-editor/src/utils/fieldEditorHelpers.ts diff --git a/packages/react-schema-editor/README.md b/packages/react-schema-editor/README.md index ed77b8ffd86..219cee06929 100644 --- a/packages/react-schema-editor/README.md +++ b/packages/react-schema-editor/README.md @@ -17,22 +17,22 @@ function SchemaEditorDemo() { title: '我是层级嵌套标题', description: '我是层级嵌套描述', properties: { - arrayA: { - type: 'array', - title: '动物', - description: '我是字段描述', - component: 'Select', - items: { - type: 'string', - enum: ['Dog', 'Cat', 'Horse'] - } - }, - numberB: { - type: 'number', - title: '年龄' - }, - objectC: { - type: 'object' + input: { + type: 'string', + 'x-component': 'Input', + 'x-component-props': { + value: 'abc', + onChange: '{{function(){console.log("abcd");}}}' + }, + 'x-props': { + help: 'test' + }, + 'x-rules': [ + { + required: true, + message: '此项必填' + } + ] } } } diff --git a/packages/react-schema-editor/src/components/FieldEditor.css b/packages/react-schema-editor/src/components/FieldEditor.css new file mode 100644 index 00000000000..b14cf695fde --- /dev/null +++ b/packages/react-schema-editor/src/components/FieldEditor.css @@ -0,0 +1,13 @@ +.field-group{ + +} +.field-group-title{ + margin-top: 15px; + font-size: 16px; +} +.field-group-content{ + display: flex; +} +.field-group-form-item{ + flex: 1; +} diff --git a/packages/react-schema-editor/src/components/FieldEditor.tsx b/packages/react-schema-editor/src/components/FieldEditor.tsx index d5fd4ccff12..4eb881658ad 100644 --- a/packages/react-schema-editor/src/components/FieldEditor.tsx +++ b/packages/react-schema-editor/src/components/FieldEditor.tsx @@ -1,42 +1,485 @@ -import { Input, Select, Checkbox, Button, Icon } from 'antd' import React from 'react' +import _ from 'lodash' +import { Form, Button, Checkbox, Input, InputNumber, Select } from 'antd' +import { + getFieldTypeData, + getInputTypeData, + getXComponentData, + getComponentPropsData, + getComponentPropsValue, + getInputType, + getPropertyValue, + getExpressionValue, + getRuleMessage +} from '../utils/fieldEditorHelpers' +import { InputTypes, ComponentPropsTypes } from '../utils/types' +import './FieldEditor.css' -const typeOptions = [ - { label: '字符串', value: 'string' }, - { label: '布尔值', value: 'boolean' }, - { label: '数字', value: 'number' }, - { label: '数组', value: 'array' }, - { label: '对象', value: 'object' } -] +const FormItem = Form.Item +const SelectOption = Select.Option + +const formItemLayout = { + labelCol: { span: 24 }, + wrapperCol: { span: 24 } +} + +const BLANK_PROPERTY_VALUE = '' + +interface IFieldEditorProps { + schema: any + components: any + xProps: any + xRules: any + onChange: (schema: any) => void +} + +interface IFormItemGroupProps extends Partial { + title: string + propsKey: string +} + +const FormItemGroup: React.FC = ({ + title, + schema, + xProps, + xRules, + components, + propsKey, + onChange +}) => { + const componentName = schema[ComponentPropsTypes.X_COMPONENT] + const inputTypeData = getInputTypeData() + const componentPropsData = getComponentPropsData({ + schema, + xProps, + xRules, + components, + componentName, + propsKey + }) + + const componentPropsValue = getComponentPropsValue({ schema, propsKey }) + + const handleXComponentPropsValueChange = (value, property) => { + let newSchema + if (propsKey === ComponentPropsTypes.X_RULES) { + const newRules = _.map(schema[propsKey], rule => { + if (_.has(rule, property)) { + return { + ...rule, + [property]: value + } + } + return rule + }) + newSchema = { + ...schema, + [propsKey]: newRules + } + } else { + newSchema = { + ...schema, + [propsKey]: { + ...schema[propsKey], + [property]: value + } + } + } + onChange(newSchema) + } + + const handleInputTypeChange = (value, property) => { + let newSchema + let defaultValue + switch (value) { + case InputTypes.INPUT: { + defaultValue = '' + break + } + case InputTypes.NUMBER_PICKER: { + defaultValue = 0 + break + } + case InputTypes.CHECKBOX: { + defaultValue = false + break + } + case InputTypes.TEXT_AREA: { + defaultValue = null + break + } + } + if (propsKey === ComponentPropsTypes.X_RULES) { + const newRules = _.map(schema[propsKey], rule => { + if (_.has(rule, property)) { + return { + ...rule, + [property]: defaultValue + } + } + return rule + }) + newSchema = { + ...schema, + [propsKey]: newRules + } + } else { + newSchema = { + ...schema, + [propsKey]: { + ...schema[propsKey], + [property]: defaultValue + } + } + } + onChange(newSchema) + } + + const handlePropertyChange = (value, property) => { + let newComponentProps + if (propsKey === ComponentPropsTypes.X_RULES) { + newComponentProps = _.map(schema[propsKey], rule => { + if (_.has(rule, property)) { + return { + ..._.omit(rule, property), + [value]: rule[property] + } + } + return rule + }) + } else { + newComponentProps = {} + _.map(schema[propsKey], (v, k) => { + if (k === property) { + newComponentProps[value] = v + } else if (k !== value) { + newComponentProps[k] = v + } + }) + } + + onChange({ + ...schema, + [propsKey]: newComponentProps + }) + } + + const handleRuleMessageChange = (value, property) => { + const newRules = _.map(schema[propsKey], rule => { + if (_.has(rule, property)) { + return { + ...rule, + message: value + } + } + return rule + }) + + onChange({ + ...schema, + [ComponentPropsTypes.X_RULES]: newRules + }) + } + + const handleMinusClick = property => { + if (propsKey === ComponentPropsTypes.X_RULES) { + const newRules = _.reduce( + schema[propsKey], + (result, rule) => { + if (_.has(rule, property)) { + return result + } + return _.concat(result, rule) + }, + [] + ) + onChange({ + ...schema, + [ComponentPropsTypes.X_RULES]: newRules + }) + } else { + onChange({ + ..._.omit(schema, `${propsKey}.${property}`) + }) + } + } + + const handlePlusClick = () => { + if (propsKey === ComponentPropsTypes.X_RULES) { + onChange({ + ...schema, + [propsKey]: _.concat(schema[propsKey] || [], { + [componentPropsData.defaultValue]: BLANK_PROPERTY_VALUE + }) + }) + } else { + onChange({ + ...schema, + [propsKey]: { + ...schema[propsKey], + [componentPropsData.defaultValue]: BLANK_PROPERTY_VALUE + } + }) + } + } -export const FieldEditor: React.FC = ({ children }) => { return ( -
    -
    - - - -
    +
    +
    {title}
    + {_.map(componentPropsValue, (property, index) => { + const value = getPropertyValue({ schema, propsKey, property }) + const inputType = getInputType(value) + return ( +
    + + + + + + + {inputType === InputTypes.INPUT && ( + + { + handleXComponentPropsValueChange( + event.target.value, + property + ) + }} + /> + + )} + {inputType === InputTypes.NUMBER_PICKER && ( + + { + handleXComponentPropsValueChange(value, property) + }} + /> + + )} + {inputType === InputTypes.CHECKBOX && ( + + { + handleXComponentPropsValueChange( + event.target.checked, + property + ) + }} + /> + + )} + {inputType === InputTypes.TEXT_AREA && ( + + { + let value = event.target.value + try { + value = JSON.parse(value) + } catch (error) {} + handleXComponentPropsValueChange(value, property) + }} + /> + + )} + {propsKey === ComponentPropsTypes.X_RULES && ( + + { + handleRuleMessageChange(event.target.value, property) + }} + /> + + )} + +
    + ) + })} +
    + ) +} + +const FieldEditor: React.FC = ({ + schema, + components, + xProps, + xRules, + onChange +}) => { + const fieldTypeData = getFieldTypeData() + + const xComponentData = getXComponentData(components) + + return ( +
    - - - - - - +
    字段
    +
    + + + + + + + + { + onChange({ + ...schema, + description: event.target.value + }) + }} + /> + +
    -
    -
    {children}
    -
    + + + + ) } + +export default Form.create()(FieldEditor) diff --git a/packages/react-schema-editor/src/components/SchemaTree.tsx b/packages/react-schema-editor/src/components/SchemaTree.tsx index 31f3250e645..743821de549 100644 --- a/packages/react-schema-editor/src/components/SchemaTree.tsx +++ b/packages/react-schema-editor/src/components/SchemaTree.tsx @@ -2,6 +2,8 @@ import React, { useCallback } from 'react' import { Tree, Row, Col } from 'antd' import { ISchemaTreeProps } from '../utils/types' import * as fp from 'lodash/fp' +import _ from 'lodash' +import FieldEditor from './FieldEditor' const TreeNode = Tree.TreeNode @@ -74,7 +76,8 @@ export const SchemaTree: React.FC = ({ const selectedSchema = selectedPath && (selectedPath === 'root' ? schema : fp.get(selectedPath, schema)) - + console.log('selectedPath====', selectedPath) + console.log('selectedSchema====', selectedSchema) return ( @@ -89,7 +92,40 @@ export const SchemaTree: React.FC = ({ -
    {selectedSchema && JSON.stringify(selectedSchema, null, 2)}
    + {selectedSchema && ( + { + const newSchema = _.clone(schema) + _.set(newSchema, selectedPath, value) + onChange(newSchema) + }} + /> + )}
    ) diff --git a/packages/react-schema-editor/src/main.scss b/packages/react-schema-editor/src/main.scss index b4abed7ba9c..d5ed7753ca1 100644 --- a/packages/react-schema-editor/src/main.scss +++ b/packages/react-schema-editor/src/main.scss @@ -50,6 +50,7 @@ } .field-group { + display: flex; background-color: #F2F3F7; padding: 5px; diff --git a/packages/react-schema-editor/src/utils/components.ts b/packages/react-schema-editor/src/utils/components.ts index 1440de4af17..be1f66e005b 100644 --- a/packages/react-schema-editor/src/utils/components.ts +++ b/packages/react-schema-editor/src/utils/components.ts @@ -1,81 +1,22748 @@ export default { next: [ { - name: 'Affix', - title: '固钉', - typeId: 6, - props: { - container: { - type: { - name: 'func' + "name": "Affix", + "title": "固钉", + "typeId": 6, + "props": { + "container": { + "type": { + "name": "func" }, - required: false, - description: '设置 Affix 需要监听滚动事件的容器元素', - defaultValue: { - value: '() => window', - computed: false + "required": false, + "description": "设置 Affix 需要监听滚动事件的容器元素", + "defaultValue": { + "value": "() => window", + "computed": false }, - docblock: - '设置 Affix 需要监听滚动事件的容器元素\n@return {ReactElement} 目标容器元素的实例', - params: [], - returns: { - description: '目标容器元素的实例', - type: { - name: 'ReactElement' + "docblock": "设置 Affix 需要监听滚动事件的容器元素\n@return {ReactElement} 目标容器元素的实例", + "params": [], + "returns": { + "description": "目标容器元素的实例", + "type": { + "name": "ReactElement" } } }, - offsetTop: { - type: { - name: 'number' + "offsetTop": { + "type": { + "name": "number" }, - required: false, - description: '距离窗口顶部达到指定偏移量后触发', - docblock: '距离窗口顶部达到指定偏移量后触发' + "required": false, + "description": "距离窗口顶部达到指定偏移量后触发", + "docblock": "距离窗口顶部达到指定偏移量后触发" }, - offsetBottom: { - type: { - name: 'number' + "offsetBottom": { + "type": { + "name": "number" }, - required: false, - description: '距离窗口底部达到制定偏移量后触发', - docblock: '距离窗口底部达到制定偏移量后触发' + "required": false, + "description": "距离窗口底部达到制定偏移量后触发", + "docblock": "距离窗口底部达到制定偏移量后触发" }, - onAffix: { - type: { - name: 'func' + "onAffix": { + "type": { + "name": "func" }, - required: false, - description: '当元素的样式发生固钉样式变化时触发的回调函数', - defaultValue: { - value: 'func.noop', - computed: true + "required": false, + "description": "当元素的样式发生固钉样式变化时触发的回调函数", + "defaultValue": { + "value": "func.noop", + "computed": true }, - docblock: - '当元素的样式发生固钉样式变化时触发的回调函数\n@param {Boolean} 元素是否被固钉', - params: [ + "docblock": "当元素的样式发生固钉样式变化时触发的回调函数\n@param {Boolean} 元素是否被固钉", + "params": [{ + "name": "元素是否被固钉", + "description": null, + "type": { + "name": "Boolean" + } + }], + "returns": null + }, + "useAbsolute": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否启用绝对布局实现 affix", + "docblock": "是否启用绝对布局实现 affix\n@param {Boolean} 是否启用绝对布局" + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "Animate", + "title": "动画", + "typeId": 6, + "props": { + "animation": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": "动画 className", + "docblock": "动画 className" + }, + "animationAppear": { + "type": { + "name": "bool" + }, + "required": false, + "description": "子元素第一次挂载时是否执行动画", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "子元素第一次挂载时是否执行动画" + }, + "component": { + "type": { + "name": "any" + }, + "required": false, + "description": "包裹子元素的标签", + "defaultValue": { + "value": "'div'", + "computed": false + }, + "docblock": "包裹子元素的标签" + }, + "singleMode": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否只有单个子元素,如果有多个子元素,请设置为 false", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否只有单个子元素,如果有多个子元素,请设置为 false" + }, + "children": { + "type": { + "name": "union", + "value": [{ + "name": "element" + }, + { + "name": "arrayOf", + "value": { + "name": "element" + } + } + ] + }, + "required": false, + "description": "子元素", + "docblock": "子元素" + }, + "beforeAppear": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行第一次挂载动画前触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行第一次挂载动画前触发的回调函数", + "params": [], + "returns": null + }, + "onAppear": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行第一次挂载动画,添加 xxx-appear-active 类名后触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行第一次挂载动画,添加 xxx-appear-active 类名后触发的回调函数\n @param {HTMLElement} node \b执行动画的 dom 元素", + "params": [{ + "name": "node", + "description": "\b执行动画的 dom 元素", + "type": { + "name": "HTMLElement" + } + }], + "returns": null + }, + "afterAppear": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行完第一次挂载动画后触发的函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行完第一次挂载动画后触发的函数\n@param {HTMLElement} node \b执行动画的 dom 元素", + "params": [{ + "name": "node", + "description": "\b执行动画的 dom 元素", + "type": { + "name": "HTMLElement" + } + }], + "returns": null + }, + "beforeEnter": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行进场动画前触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行进场动画前触发的回调函数\n@param {HTMLElement} node \b执行动画的 dom 元素", + "params": [{ + "name": "node", + "description": "\b执行动画的 dom 元素", + "type": { + "name": "HTMLElement" + } + }], + "returns": null + }, + "onEnter": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行进场动画,添加 xxx-enter-active 类名后触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行进场动画,添加 xxx-enter-active 类名后触发的回调函数\n@param {HTMLElement} node \b执行动画的 dom 元素", + "params": [{ + "name": "node", + "description": "\b执行动画的 dom 元素", + "type": { + "name": "HTMLElement" + } + }], + "returns": null + }, + "afterEnter": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行完进场动画后触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行完进场动画后触发的回调函数\n@param {HTMLElement} node \b执行动画的 dom 元素", + "params": [{ + "name": "node", + "description": "\b执行动画的 dom 元素", + "type": { + "name": "HTMLElement" + } + }], + "returns": null + }, + "beforeLeave": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行离场动画前触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行离场动画前触发的回调函数\n@param {HTMLElement} node \b执行动画的 dom 元素", + "params": [{ + "name": "node", + "description": "\b执行动画的 dom 元素", + "type": { + "name": "HTMLElement" + } + }], + "returns": null + }, + "onLeave": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行离场动画,添加 xxx-leave-active 类名后触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行离场动画,添加 xxx-leave-active 类名后触发的回调函数\n@param {HTMLElement} node \b执行动画的 dom 元素", + "params": [{ + "name": "node", + "description": "\b执行动画的 dom 元素", + "type": { + "name": "HTMLElement" + } + }], + "returns": null + }, + "afterLeave": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行完离场动画后触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行完离场动画后触发的回调函数\n@param {HTMLElement} node \b执行动画的 dom 元素", + "params": [{ + "name": "node", + "description": "\b执行动画的 dom 元素", + "type": { + "name": "HTMLElement" + } + }], + "returns": null + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "Badge", + "title": "徽标", + "typeId": 4, + "props": { + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "徽章依托的内容", + "docblock": "徽章依托的内容" + }, + "count": { + "type": { + "name": "union", + "value": [{ + "name": "number" + }, + { + "name": "string" + } + ] + }, + "required": false, + "description": "展示的数字,大于 overflowCount 时显示为 ${overflowCount}+,为 0 时默认隐藏", + "defaultValue": { + "value": "0", + "computed": false + }, + "docblock": "展示的数字,大于 overflowCount 时显示为 ${overflowCount}+,为 0 时默认隐藏" + }, + "showZero": { + "type": { + "name": "bool" + }, + "required": false, + "description": "当count为0时,是否显示count", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "当count为0时,是否显示count" + }, + "content": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义节点内容", + "docblock": "自定义节点内容" + }, + "overflowCount": { + "type": { + "name": "union", + "value": [{ + "name": "number" + }, + { + "name": "string" + } + ] + }, + "required": false, + "description": "展示的封顶的数字", + "defaultValue": { + "value": "99", + "computed": false + }, + "docblock": "展示的封顶的数字" + }, + "dot": { + "type": { + "name": "bool" + }, + "required": false, + "description": "不展示数字,只展示一个小红点", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "不展示数字,只展示一个小红点" + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "Balloon", + "title": "气泡", + "typeId": 5, + "props": { + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "自定义类名", + "docblock": "自定义类名" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内敛样式", + "docblock": "自定义内敛样式", + "properties": [] + }, + "children": { + "type": { + "name": "any" + }, + "required": false, + "description": "浮层的内容", + "docblock": "浮层的内容" + }, + "type": { + "type": { + "name": "enum", + "value": [{ + "value": "'normal'", + "computed": false + }, + { + "value": "'primary'", + "computed": false + } + ] + }, + "required": false, + "description": "样式类型", + "defaultValue": { + "value": "'normal'", + "computed": false + }, + "docblock": "样式类型" + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层当前显示的状态", + "docblock": "弹层当前显示的状态" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层默认显示的状态", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "弹层默认显示的状态" + }, + "onVisibleChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层在显示和隐藏触发的事件", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层在显示和隐藏触发的事件\n@param {Boolean} visible 弹层是否隐藏和显示\n@param {String} type 触发弹层显示或隐藏的来源, closeClick 表示由自带的关闭按钮触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "params": [{ + "name": "visible", + "description": "弹层是否隐藏和显示", + "type": { + "name": "Boolean" + } + }, + { + "name": "type", + "description": "触发弹层显示或隐藏的来源, closeClick 表示由自带的关闭按钮触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "type": { + "name": "String" + } + } + ], + "returns": null + }, + "alignEdge": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹出层对齐方式", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "弹出层对齐方式" + }, + "closable": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示关闭按钮", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示关闭按钮" + }, + "align": { + "type": { + "name": "enum", + "value": [{ + "value": "'t'", + "computed": false, + "description": "上" + }, + { + "value": "'r'", + "computed": false, + "description": "右" + }, + { + "value": "'b'", + "computed": false, + "description": "下" + }, + { + "value": "'l'", + "computed": false, + "description": "左" + }, + { + "value": "'tl'", + "computed": false, + "description": "上左" + }, + { + "value": "'tr'", + "computed": false, + "description": "上右" + }, + { + "value": "'bl'", + "computed": false, + "description": "下左" + }, + { + "value": "'br'", + "computed": false, + "description": "下右" + }, + { + "value": "'lt'", + "computed": false, + "description": "左上" + }, + { + "value": "'lb'", + "computed": false, + "description": "左下" + }, + { + "value": "'rt'", + "computed": false, + "description": "右上" + }, + { + "value": "'rb'", + "computed": false, + "description": "右下 及其 两两组合" + } + ] + }, + "required": false, + "description": "弹出层位置", + "defaultValue": { + "value": "'b'", + "computed": false + }, + "docblock": "弹出层位置\n@enumdesc 上, 右, 下, 左, 上左, 上右, 下左, 下右, 左上, 左下, 右上, 右下 及其 两两组合", + "value": [{ + "value": "'t'", + "computed": false, + "description": "上" + }, + { + "value": "'r'", + "computed": false, + "description": "右" + }, + { + "value": "'b'", + "computed": false, + "description": "下" + }, + { + "value": "'l'", + "computed": false, + "description": "左" + }, + { + "value": "'tl'", + "computed": false, + "description": "上左" + }, + { + "value": "'tr'", + "computed": false, + "description": "上右" + }, + { + "value": "'bl'", + "computed": false, + "description": "下左" + }, + { + "value": "'br'", + "computed": false, + "description": "下右" + }, + { + "value": "'lt'", + "computed": false, + "description": "左上" + }, + { + "value": "'lb'", + "computed": false, + "description": "左下" + }, + { + "value": "'rt'", + "computed": false, + "description": "右上" + }, + { + "value": "'rb'", + "computed": false, + "description": "右下 及其 两两组合" + } + ] + }, + "offset": { + "type": { + "name": "array" + }, + "required": false, + "description": "弹层相对于trigger的定位的微调, 接收数组[hoz, ver], 表示弹层在 left / top 上的增量\ne.g. [100, 100] 表示往右(RTL 模式下是往左) 、下分布偏移100px", + "defaultValue": { + "value": "[0, 0]", + "computed": false + }, + "docblock": "弹层相对于trigger的定位的微调, 接收数组[hoz, ver], 表示弹层在 left / top 上的增量\ne.g. [100, 100] 表示往右(RTL 模式下是往左) 、下分布偏移100px" + }, + "trigger": { + "type": { + "name": "any" + }, + "required": false, + "description": "触发元素", + "defaultValue": { + "value": "", + "computed": false + }, + "docblock": "触发元素" + }, + "triggerType": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "触发行为\n鼠标悬浮, 鼠标点击('hover','click')或者它们组成的数组,如 ['hover', 'click'], 强烈不建议使用'focus',若弹窗内容有复杂交互请使用click", + "defaultValue": { + "value": "'hover'", + "computed": false + }, + "docblock": "触发行为\n鼠标悬浮, 鼠标点击('hover','click')或者它们组成的数组,如 ['hover', 'click'], 强烈不建议使用'focus',若弹窗内容有复杂交互请使用click" + }, + "onClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "任何visible为false时会触发的事件", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "任何visible为false时会触发的事件", + "params": [], + "returns": null + }, + "needAdjust": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否进行自动位置调整", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否进行自动位置调整" + }, + "delay": { + "type": { + "name": "number" + }, + "required": false, + "description": "弹层在触发以后的延时显示, 单位毫秒 ms", + "docblock": "弹层在触发以后的延时显示, 单位毫秒 ms" + }, + "afterClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "浮层关闭后触发的事件, 如果有动画,则在动画结束后触发", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "浮层关闭后触发的事件, 如果有动画,则在动画结束后触发", + "params": [], + "returns": null + }, + "shouldUpdatePosition": { + "type": { + "name": "bool" + }, + "required": false, + "description": "强制更新定位信息", + "docblock": "强制更新定位信息" + }, + "autoFocus": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层出现后是否自动focus到内部第一个元素", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "弹层出现后是否自动focus到内部第一个元素" + }, + "safeNode": { + "type": { + "name": "string" + }, + "required": false, + "description": "安全节点:对于triggetType为click的浮层,会在点击除了浮层外的其它区域时关闭浮层.safeNode用于添加不触发关闭的节点, 值可以是dom节点的id或者是节点的dom对象", + "defaultValue": { + "value": "undefined", + "computed": true + }, + "docblock": "安全节点:对于triggetType为click的浮层,会在点击除了浮层外的其它区域时关闭浮层.safeNode用于添加不触发关闭的节点, 值可以是dom节点的id或者是节点的dom对象" + }, + "safeId": { + "type": { + "name": "string" + }, + "required": false, + "description": "用来指定safeNode节点的id,和safeNode配合使用", + "defaultValue": { + "value": "null", + "computed": false + }, + "docblock": "用来指定safeNode节点的id,和safeNode配合使用" + }, + "animation": { + "type": { + "name": "union", + "value": [{ + "name": "object" + }, + { + "name": "bool" + } + ] + }, + "required": false, + "description": "配置动画的播放方式", + "defaultValue": { + "value": "{\n in: 'zoomIn',\n out: 'zoomOut',\n}", + "computed": false + }, + "docblock": "配置动画的播放方式\n@param {String} in 进场动画\n@param {String} out 出场动画" + }, + "cache": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层的dom节点关闭时是否删除", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "弹层的dom节点关闭时是否删除" + }, + "popupContainer": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "指定浮层渲染的父节点, 可以为节点id的字符串,也可以返回节点的函数。", + "docblock": "指定浮层渲染的父节点, 可以为节点id的字符串,也可以返回节点的函数。" + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层组件style,透传给Popup", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "弹层组件style,透传给Popup", + "properties": [] + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层组件className,透传给Popup", + "defaultValue": { + "value": "''", + "computed": false + }, + "docblock": "弹层组件className,透传给Popup" + }, + "popupProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层组件属性,透传给Popup", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "弹层组件属性,透传给Popup", + "properties": [] + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随滚动", + "docblock": "是否跟随滚动" + }, + "id": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层id, 传入值才会支持无障碍", + "docblock": "弹层id, 传入值才会支持无障碍" + } + }, + "methods": [], + "subComponents": [{ + "name": "Tooltip", + "title": "文字提示", + "typeId": 5, + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式类名的品牌前缀", + "defaultValue": { + "value": "'next-'", + "computed": false + }, + "docblock": "样式类名的品牌前缀" + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "自定义类名", + "docblock": "自定义类名" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内联样式", + "docblock": "自定义内联样式", + "properties": [] + }, + "children": { + "type": { + "name": "any" + }, + "required": false, + "description": "tooltip的内容", + "docblock": "tooltip的内容" + }, + "align": { + "type": { + "name": "enum", + "value": [{ + "value": "'t'", + "computed": false, + "description": "上" + }, + { + "value": "'r'", + "computed": false, + "description": "右" + }, + { + "value": "'b'", + "computed": false, + "description": "下" + }, + { + "value": "'l'", + "computed": false, + "description": "左" + }, + { + "value": "'tl'", + "computed": false, + "description": "上左" + }, + { + "value": "'tr'", + "computed": false, + "description": "上右" + }, + { + "value": "'bl'", + "computed": false, + "description": "下左" + }, + { + "value": "'br'", + "computed": false, + "description": "下右" + }, + { + "value": "'lt'", + "computed": false, + "description": "左上" + }, + { + "value": "'lb'", + "computed": false, + "description": "左下" + }, + { + "value": "'rt'", + "computed": false, + "description": "右上" + }, + { + "value": "'rb'", + "computed": false, + "description": "右下 及其 两两组合" + } + ] + }, + "required": false, + "description": "弹出层位置", + "defaultValue": { + "value": "'b'", + "computed": false + }, + "docblock": "弹出层位置\n@enumdesc 上, 右, 下, 左, 上左, 上右, 下左, 下右, 左上, 左下, 右上, 右下 及其 两两组合", + "value": [{ + "value": "'t'", + "computed": false, + "description": "上" + }, + { + "value": "'r'", + "computed": false, + "description": "右" + }, + { + "value": "'b'", + "computed": false, + "description": "下" + }, + { + "value": "'l'", + "computed": false, + "description": "左" + }, + { + "value": "'tl'", + "computed": false, + "description": "上左" + }, + { + "value": "'tr'", + "computed": false, + "description": "上右" + }, + { + "value": "'bl'", + "computed": false, + "description": "下左" + }, + { + "value": "'br'", + "computed": false, + "description": "下右" + }, + { + "value": "'lt'", + "computed": false, + "description": "左上" + }, + { + "value": "'lb'", + "computed": false, + "description": "左下" + }, + { + "value": "'rt'", + "computed": false, + "description": "右上" + }, + { + "value": "'rb'", + "computed": false, + "description": "右下 及其 两两组合" + } + ] + }, + "trigger": { + "type": { + "name": "any" + }, + "required": false, + "description": "触发元素", + "defaultValue": { + "value": "", + "computed": false + }, + "docblock": "触发元素" + }, + "triggerType": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "触发行为\n鼠标悬浮, 鼠标点击('hover', 'click')或者它们组成的数组,如 ['hover', 'click'], 强烈不建议使用'focus',若有复杂交互,推荐使用triggerType为click的Balloon组件", + "defaultValue": { + "value": "'hover'", + "computed": false + }, + "docblock": "触发行为\n鼠标悬浮, 鼠标点击('hover', 'click')或者它们组成的数组,如 ['hover', 'click'], 强烈不建议使用'focus',若有复杂交互,推荐使用triggerType为click的Balloon组件" + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层组件style,透传给Popup", + "docblock": "弹层组件style,透传给Popup", + "properties": [] + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层组件className,透传给Popup", + "docblock": "弹层组件className,透传给Popup" + }, + "popupProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层组件属性,透传给Popup", + "docblock": "弹层组件属性,透传给Popup", + "properties": [] + }, + "pure": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否pure render", + "docblock": "是否pure render" + }, + "popupContainer": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "指定浮层渲染的父节点, 可以为节点id的字符串,也可以返回节点的函数。", + "docblock": "指定浮层渲染的父节点, 可以为节点id的字符串,也可以返回节点的函数。" + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随滚动", + "docblock": "是否跟随滚动" + }, + "id": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层id, 传入值才会支持无障碍", + "docblock": "弹层id, 传入值才会支持无障碍" + } + }, + "methods": [] + }] + }, + { + "name": "Breadcrumb", + "title": "面包屑", + "typeId": 2, + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式类名的品牌前缀", + "defaultValue": { + "value": "'next-'", + "computed": false + }, + "docblock": "样式类名的品牌前缀" + }, + "children": { + "type": { + "name": "custom", + "raw": "(props, propName) => {\n Children.forEach(props[propName], child => {\n if (\n !(\n child &&\n typeof child.type === 'function' &&\n child.type._typeMark === 'breadcrumb_item'\n )\n ) {\n throw new Error(\n \"Breadcrumb's children must be Breadcrumb.Item!\"\n );\n }\n });\n}" + }, + "required": false, + "description": "面包屑子节点,需传入 Breadcrumb.Item", + "docblock": "面包屑子节点,需传入 Breadcrumb.Item" + }, + "maxNode": { + "type": { + "name": "number" + }, + "required": false, + "description": "面包屑最多显示个数,超出部分会被隐藏", + "defaultValue": { + "value": "100", + "computed": false + }, + "docblock": "面包屑最多显示个数,超出部分会被隐藏" + }, + "separator": { + "type": { + "name": "node" + }, + "required": false, + "description": "分隔符,可以是文本或 Icon", + "defaultValue": { + "value": "", + "computed": false + }, + "docblock": "分隔符,可以是文本或 Icon" + }, + "component": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "设置标签类型", + "defaultValue": { + "value": "'nav'", + "computed": false + }, + "docblock": "设置标签类型" + } + }, + "methods": [], + "subComponents": [{ + "name": "Item", + "title": "面包屑项", + "typeId": 2, + "props": { + "link": { + "type": { + "name": "string" + }, + "required": false, + "description": "面包屑节点链接,如果设置这个属性,则该节点为`` ,否则是``", + "docblock": "面包屑节点链接,如果设置这个属性,则该节点为`` ,否则是``" + } + }, + "methods": [] + }] + }, + { + "name": "Button", + "title": "按钮", + "typeId": 1, + "props": { + "type": { + "type": { + "name": "enum", + "value": [{ + "value": "'primary'", + "computed": false + }, + { + "value": "'secondary'", + "computed": false + }, + { + "value": "'normal'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮的类型", + "defaultValue": { + "value": "'normal'", + "computed": false + }, + "docblock": "按钮的类型" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮的尺寸", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "按钮的尺寸" + }, + "iconSize": { + "type": { + "name": "enum", + "value": [{ + "value": "'xxs'", + "computed": false + }, + { + "value": "'xs'", + "computed": false + }, + { + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + }, + { + "value": "'xl'", + "computed": false + }, + { + "value": "'xxl'", + "computed": false + }, + { + "value": "'xxxl'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮中 Icon 的尺寸,用于替代 Icon 的默认大小", + "docblock": "按钮中 Icon 的尺寸,用于替代 Icon 的默认大小" + }, + "htmlType": { + "type": { + "name": "enum", + "value": [{ + "value": "'submit'", + "computed": false + }, + { + "value": "'reset'", + "computed": false + }, + { + "value": "'button'", + "computed": false + } + ] + }, + "required": false, + "description": "当 component = 'button' 时,设置 button 标签的 type 值", + "defaultValue": { + "value": "'button'", + "computed": false + }, + "docblock": "当 component = 'button' 时,设置 button 标签的 type 值" + }, + "component": { + "type": { + "name": "enum", + "value": [{ + "value": "'button'", + "computed": false + }, + { + "value": "'a'", + "computed": false + }, + { + "value": "'div'", + "computed": false + }, + { + "value": "'span'", + "computed": false + } + ] + }, + "required": false, + "description": "设置标签类型", + "defaultValue": { + "value": "'button'", + "computed": false + }, + "docblock": "设置标签类型" + }, + "loading": { + "type": { + "name": "bool" + }, + "required": false, + "description": "设置按钮的载入状态", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "设置按钮的载入状态" + }, + "ghost": { + "type": { + "name": "enum", + "value": [{ + "value": "true", + "computed": false + }, + { + "value": "false", + "computed": false + }, + { + "value": "'light'", + "computed": false + }, + { + "value": "'dark'", + "computed": false + } + ] + }, + "required": false, + "description": "是否为幽灵按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为幽灵按钮" + }, + "text": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否为文本按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为文本按钮" + }, + "warning": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否为警告按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为警告按钮" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否禁用" + }, + "onClick": { + "type": { + "name": "func" + }, + "required": false, + "description": "点击按钮的回调", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "点击按钮的回调\n@param {Object} e Event Object", + "params": [{ + "name": "e", + "description": "Event Object", + "type": { + "name": "Object" + } + }], + "returns": null + } + }, + "methods": [], + "subComponents": [{ + "name": "Group", + "title": "按钮组", + "typeId": 1, + "props": { + "size": { + "type": { + "name": "string" + }, + "required": false, + "description": "统一设置 Button 组件的按钮大小", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "统一设置 Button 组件的按钮大小" + } + }, + "methods": [] + }] + }, + { + "name": "Calendar", + "title": "日历", + "typeId": 4, + "props": { + "defaultValue": { + "type": { + "name": "custom", + "raw": "checkMomentObj" + }, + "required": false, + "description": "默认选中的日期(moment 对象)", + "docblock": "默认选中的日期(moment 对象)" + }, + "value": { + "type": { + "name": "custom", + "raw": "checkMomentObj" + }, + "required": false, + "description": "选中的日期值 (moment 对象)", + "docblock": "选中的日期值 (moment 对象)" + }, + "mode": { + "type": { + "name": "enum", + "computed": true, + "value": "CALENDAR_MODES" + }, + "required": false, + "description": "面板模式", + "docblock": "面板模式" + }, + "showOtherMonth": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否展示非本月的日期", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否展示非本月的日期" + }, + "defaultVisibleMonth": { + "type": { + "name": "func" + }, + "required": false, + "description": "默认展示的月份", + "docblock": "默认展示的月份", + "params": [], + "returns": null + }, + "shape": { + "type": { + "name": "enum", + "value": [{ + "value": "'card'", + "computed": false + }, + { + "value": "'fullscreen'", + "computed": false + }, + { + "value": "'panel'", + "computed": false + } + ] + }, + "required": false, + "description": "展现形态", + "defaultValue": { + "value": "'fullscreen'", + "computed": false + }, + "docblock": "展现形态" + }, + "onSelect": { + "type": { + "name": "func" + }, + "required": false, + "description": "选择日期单元格时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "选择日期单元格时的回调\n@param {Object} value 对应的日期值 (moment 对象)", + "params": [{ + "name": "value", + "description": "对应的日期值 (moment 对象)", + "type": { + "name": "Object" + } + }], + "returns": null + }, + "onModeChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "面板模式变化时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "面板模式变化时的回调\n@param {String} mode 对应面板模式 date month year", + "params": [{ + "name": "mode", + "description": "对应面板模式 date month year", + "type": { + "name": "String" + } + }], + "returns": null + }, + "onVisibleMonthChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "展现的月份变化时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "展现的月份变化时的回调\n@param {Object} value 显示的月份 (moment 对象)\n@param {String} reason 触发月份改变原因", + "params": [{ + "name": "value", + "description": "显示的月份 (moment 对象)", + "type": { + "name": "Object" + } + }, + { + "name": "reason", + "description": "触发月份改变原因", + "type": { + "name": "String" + } + } + ], + "returns": null + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "自定义样式类", + "docblock": "自定义样式类" + }, + "dateCellRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义日期渲染函数", + "defaultValue": { + "value": "value => value.date()", + "computed": false + }, + "docblock": "自定义日期渲染函数\n@param {Object} value 日期值(moment对象)\n@returns {ReactNode}", + "params": [{ + "name": "value", + "description": "日期值(moment对象)", + "type": { + "name": "Object" + } + }], + "returns": { + "description": null, + "type": { + "name": "ReactNode" + } + } + }, + "monthCellRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义月份渲染函数", + "docblock": "自定义月份渲染函数\n@param {Object} calendarDate 对应 Calendar 返回的自定义日期对象\n@returns {ReactNode}", + "params": [{ + "name": "calendarDate", + "description": "对应 Calendar 返回的自定义日期对象", + "type": { + "name": "Object" + } + }], + "returns": { + "description": null, + "type": { + "name": "ReactNode" + } + } + }, + "yearRange": { + "type": { + "name": "arrayOf", + "value": { + "name": "number" + } + }, + "required": false, + "description": "年份范围,[START_YEAR, END_YEAR] (只在shape 为 ‘card’, 'fullscreen' 下生效)", + "docblock": "年份范围,[START_YEAR, END_YEAR] (只在shape 为 ‘card’, 'fullscreen' 下生效)" + }, + "disabledDate": { + "type": { + "name": "func" + }, + "required": false, + "description": "不可选择的日期", + "docblock": "不可选择的日期\n@param {Object} calendarDate 对应 Calendar 返回的自定义日期对象\n@param {String} view 当前视图类型,year: 年, month: 月, date: 日\n@returns {Boolean}", + "params": [{ + "name": "calendarDate", + "description": "对应 Calendar 返回的自定义日期对象", + "type": { + "name": "Object" + } + }, + { + "name": "view", + "description": "当前视图类型,year: 年, month: 月, date: 日", + "type": { + "name": "String" + } + } + ], + "returns": { + "description": null, + "type": { + "name": "Boolean" + } + } + }, + "locale": { + "type": { + "name": "object" + }, + "required": false, + "description": "国际化配置", + "defaultValue": { + "value": "nextLocale.Calendar", + "computed": true + }, + "docblock": "国际化配置", + "properties": [] + } + }, + "methods": [{ + "name": "changeVisibleMonthByOffset", + "docblock": "根据日期偏移量设置当前展示的月份\n@param {Number} offset 日期偏移的数量\n@param {String} type 日期偏移的类型 days, months, years", + "modifiers": [], + "params": [{ + "name": "offset", + "description": "日期偏移的数量", + "type": { + "name": "Number" + } + }, + { + "name": "type", + "description": "日期偏移的类型 days, months, years", + "type": { + "name": "String" + } + } + ], + "returns": null, + "description": "根据日期偏移量设置当前展示的月份" + }], + "subComponents": [] + }, + { + "name": "Card", + "title": "卡片", + "typeId": 4, + "props": { + "title": { + "type": { + "name": "node" + }, + "required": false, + "description": "卡片的标题", + "docblock": "卡片的标题" + }, + "subTitle": { + "type": { + "name": "node" + }, + "required": false, + "description": "卡片的副标题", + "docblock": "卡片的副标题" + }, + "showTitleBullet": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示标题的项目符号", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示标题的项目符号" + }, + "showHeadDivider": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否展示头部的分隔线", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否展示头部的分隔线" + }, + "contentHeight": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "内容区域的固定高度", + "defaultValue": { + "value": "120", + "computed": false + }, + "docblock": "内容区域的固定高度" + }, + "extra": { + "type": { + "name": "node" + }, + "required": false, + "description": "标题区域的用户自定义内容", + "docblock": "标题区域的用户自定义内容" + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "Cascader", + "title": "级联", + "typeId": 4, + "props": { + "dataSource": { + "type": { + "name": "arrayOf", + "value": { + "name": "object" + } + }, + "required": false, + "description": "数据源,结构可参考下方说明", + "defaultValue": { + "value": "[]", + "computed": false + }, + "docblock": "数据源,结构可参考下方说明" + }, + "defaultValue": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "arrayOf", + "value": { + "name": "string" + } + } + ] + }, + "required": false, + "description": "(非受控)默认值", + "defaultValue": { + "value": "null", + "computed": false + }, + "docblock": "(非受控)默认值" + }, + "value": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "arrayOf", + "value": { + "name": "string" + } + } + ] + }, + "required": false, + "description": "(受控)当前值", + "docblock": "(受控)当前值" + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "选中值改变时触发的回调函数", + "docblock": "选中值改变时触发的回调函数\n@param {String|Array} value 选中的值,单选时返回单个值,多选时返回数组\n@param {Object|Array} data 选中的数据,包括 value 和 label,单选时返回单个值,多选时返回数组,父子节点选中关联时,同时选中,只返回父节点\n@param {Object} extra 额外参数\n@param {Array} extra.selectedPath 单选时选中的数据的路径\n@param {Boolean} extra.checked 多选时当前的操作是选中还是取消选中\n@param {Object} extra.currentData 多选时当前操作的数据\n@param {Array} extra.checkedData 多选时所有被选中的数据\n@param {Array} extra.indeterminateData 多选时半选的数据", + "params": [{ + "name": "value", + "description": "选中的值,单选时返回单个值,多选时返回数组", + "type": { + "name": "union", + "value": [ + "String", + "Array" + ] + } + }, + { + "name": "data", + "description": "选中的数据,包括 value 和 label,单选时返回单个值,多选时返回数组,父子节点选中关联时,同时选中,只返回父节点", + "type": { + "name": "union", + "value": [ + "Object", + "Array" + ] + } + }, + { + "name": "extra", + "description": "额外参数", + "type": { + "name": "Object" + } + }, + { + "name": "extra.selectedPath", + "description": "单选时选中的数据的路径", + "type": { + "name": "Array" + } + }, + { + "name": "extra.checked", + "description": "多选时当前的操作是选中还是取消选中", + "type": { + "name": "Boolean" + } + }, + { + "name": "extra.currentData", + "description": "多选时当前操作的数据", + "type": { + "name": "Object" + } + }, + { + "name": "extra.checkedData", + "description": "多选时所有被选中的数据", + "type": { + "name": "Array" + } + }, + { + "name": "extra.indeterminateData", + "description": "多选时半选的数据", + "type": { + "name": "Array" + } + } + ], + "returns": null + }, + "defaultExpandedValue": { + "type": { + "name": "arrayOf", + "value": { + "name": "string" + } + }, + "required": false, + "description": "(非受控)默认展开值,如果不设置,组件内部会根据 defaultValue/value 进行自动设置", + "docblock": "(非受控)默认展开值,如果不设置,组件内部会根据 defaultValue/value 进行自动设置" + }, + "expandedValue": { + "type": { + "name": "arrayOf", + "value": { + "name": "string" + } + }, + "required": false, + "description": "(受控)当前展开值", + "docblock": "(受控)当前展开值" + }, + "expandTriggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "展开触发的方式", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "展开触发的方式" + }, + "onExpand": { + "type": { + "name": "func" + }, + "required": false, + "description": "展开时触发的回调函数", + "docblock": "展开时触发的回调函数\n@param {Array} expandedValue 各列展开值的数组", + "params": [{ + "name": "expandedValue", + "description": "各列展开值的数组", + "type": { + "name": "Array" + } + }], + "returns": null + }, + "useVirtual": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否开启虚拟滚动", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否开启虚拟滚动" + }, + "multiple": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否多选", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否多选" + }, + "canOnlySelectLeaf": { + "type": { + "name": "bool" + }, + "required": false, + "description": "单选时是否只能选中叶子节点", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "单选时是否只能选中叶子节点" + }, + "canOnlyCheckLeaf": { + "type": { + "name": "bool" + }, + "required": false, + "description": "多选时是否只能选中叶子节点", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "多选时是否只能选中叶子节点" + }, + "checkStrictly": { + "type": { + "name": "bool" + }, + "required": false, + "description": "父子节点是否选中不关联", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "父子节点是否选中不关联" + }, + "listStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "每列列表样式对象", + "docblock": "每列列表样式对象", + "properties": [] + }, + "listClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "每列列表类名", + "docblock": "每列列表类名" + }, + "itemRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "每列列表项渲染函数", + "defaultValue": { + "value": "item => item.label", + "computed": false + }, + "docblock": "每列列表项渲染函数\n@param {Object} data 数据\n@return {ReactNode} 列表项内容", + "params": [{ + "name": "data", + "description": "数据", + "type": { + "name": "Object" + } + }], + "returns": { + "description": "列表项内容", + "type": { + "name": "ReactNode" + } + } + }, + "loadData": { + "type": { + "name": "func" + }, + "required": false, + "description": "异步加载数据函数", + "docblock": "异步加载数据函数\n@param {Object} data 当前点击异步加载的数据\n@param {Object} source 当前点击数据", + "params": [{ + "name": "data", + "description": "当前点击异步加载的数据", + "type": { + "name": "Object" + } + }, + { + "name": "source", + "description": "当前点击数据", + "type": { + "name": "Object" + } + } + ], + "returns": null + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "CascaderSelect", + "title": "级联选择", + "typeId": 3, + "props": { + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "选择框大小", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "选择框大小" + }, + "placeholder": { + "type": { + "name": "string" + }, + "required": false, + "description": "选择框占位符", + "docblock": "选择框占位符" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否禁用" + }, + "hasArrow": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否有下拉箭头", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否有下拉箭头" + }, + "hasBorder": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否有边框", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否有边框" + }, + "hasClear": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否有清除按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否有清除按钮" + }, + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义内联 label", + "docblock": "自定义内联 label" + }, + "readOnly": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否只读,只读模式下可以展开弹层但不能选", + "docblock": "是否只读,只读模式下可以展开弹层但不能选" + }, + "dataSource": { + "type": { + "name": "arrayOf", + "value": { + "name": "object" + } + }, + "required": false, + "description": "数据源,结构可参考下方说明", + "defaultValue": { + "value": "[]", + "computed": false + }, + "docblock": "数据源,结构可参考下方说明" + }, + "defaultValue": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "arrayOf", + "value": { + "name": "string" + } + } + ] + }, + "required": false, + "description": "(非受控)默认值", + "defaultValue": { + "value": "null", + "computed": false + }, + "docblock": "(非受控)默认值" + }, + "value": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "arrayOf", + "value": { + "name": "string" + } + } + ] + }, + "required": false, + "description": "(受控)当前值", + "docblock": "(受控)当前值" + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "选中值改变时触发的回调函数", + "docblock": "选中值改变时触发的回调函数\n@param {String|Array} value 选中的值,单选时返回单个值,多选时返回数组\n@param {Object|Array} data 选中的数据,包括 value 和 label,单选时返回单个值,多选时返回数组,父子节点选中关联时,同时选中,只返回父节点\n@param {Object} extra 额外参数\n@param {Array} extra.selectedPath 单选时选中的数据的路径\n@param {Boolean} extra.checked 多选时当前的操作是选中还是取消选中\n@param {Object} extra.currentData 多选时当前操作的数据\n@param {Array} extra.checkedData 多选时所有被选中的数据\n@param {Array} extra.indeterminateData 多选时半选的数据", + "params": [{ + "name": "value", + "description": "选中的值,单选时返回单个值,多选时返回数组", + "type": { + "name": "union", + "value": [ + "String", + "Array" + ] + } + }, + { + "name": "data", + "description": "选中的数据,包括 value 和 label,单选时返回单个值,多选时返回数组,父子节点选中关联时,同时选中,只返回父节点", + "type": { + "name": "union", + "value": [ + "Object", + "Array" + ] + } + }, + { + "name": "extra", + "description": "额外参数", + "type": { + "name": "Object" + } + }, + { + "name": "extra.selectedPath", + "description": "单选时选中的数据的路径", + "type": { + "name": "Array" + } + }, + { + "name": "extra.checked", + "description": "多选时当前的操作是选中还是取消选中", + "type": { + "name": "Boolean" + } + }, + { + "name": "extra.currentData", + "description": "多选时当前操作的数据", + "type": { + "name": "Object" + } + }, + { + "name": "extra.checkedData", + "description": "多选时所有被选中的数据", + "type": { + "name": "Array" + } + }, + { + "name": "extra.indeterminateData", + "description": "多选时半选的数据", + "type": { + "name": "Array" + } + } + ], + "returns": null + }, + "defaultExpandedValue": { + "type": { + "name": "arrayOf", + "value": { + "name": "string" + } + }, + "required": false, + "description": "默认展开值,如果不设置,组件内部会根据 defaultValue/value 进行自动设置", + "docblock": "默认展开值,如果不设置,组件内部会根据 defaultValue/value 进行自动设置" + }, + "expandTriggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "展开触发的方式", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "展开触发的方式" + }, + "useVirtual": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否开启虚拟滚动", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否开启虚拟滚动" + }, + "multiple": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否多选", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否多选" + }, + "changeOnSelect": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否选中即发生改变, 该属性仅在单选模式下有效", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否选中即发生改变, 该属性仅在单选模式下有效" + }, + "canOnlyCheckLeaf": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否只能勾选叶子项的checkbox,该属性仅在多选模式下有效", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否只能勾选叶子项的checkbox,该属性仅在多选模式下有效" + }, + "checkStrictly": { + "type": { + "name": "bool" + }, + "required": false, + "description": "父子节点是否选中不关联", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "父子节点是否选中不关联" + }, + "listStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "每列列表样式对象", + "docblock": "每列列表样式对象", + "properties": [] + }, + "listClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "每列列表类名", + "docblock": "每列列表类名" + }, + "displayRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "选择框单选时展示结果的自定义渲染函数", + "docblock": "选择框单选时展示结果的自定义渲染函数\n@param {Array} label 选中路径的文本数组\n@return {ReactNode} 渲染在选择框中的内容\n@default 单选时:labelPath => labelPath.join(' / ');多选时:labelPath => labelPath[labelPath.length - 1]", + "params": [{ + "name": "label", + "description": "选中路径的文本数组", + "type": { + "name": "Array" + } + }], + "returns": { + "description": "渲染在选择框中的内容", + "type": { + "name": "ReactNode" + } + }, + "defaultValue": { + "value": "单选时:labelPath => labelPath.join(' / ');多选时:labelPath => labelPath[labelPath.length - 1]", + "computed": false + } + }, + "itemRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "渲染 item 内容的方法", + "docblock": "渲染 item 内容的方法\n@param {Object} item 渲染节点的item\n@return {ReactNode} item node", + "params": [{ + "name": "item", + "description": "渲染节点的item", + "type": { + "name": "Object" + } + }], + "returns": { + "description": "item node", + "type": { + "name": "ReactNode" + } + } + }, + "showSearch": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示搜索框", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否显示搜索框" + }, + "filter": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义搜索函数", + "defaultValue": { + "value": "根据路径所有节点的文本值模糊匹配", + "computed": false + }, + "docblock": "自定义搜索函数\n@param {String} searchValue 搜索的关键字\n@param {Array} path 节点路径\n@return {Boolean} 是否匹配\n@default 根据路径所有节点的文本值模糊匹配", + "params": [{ + "name": "searchValue", + "description": "搜索的关键字", + "type": { + "name": "String" + } + }, + { + "name": "path", + "description": "节点路径", + "type": { + "name": "Array" + } + } + ], + "returns": { + "description": "是否匹配", + "type": { + "name": "Boolean" + } + } + }, + "resultRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "搜索结果自定义渲染函数", + "defaultValue": { + "value": "按照节点文本 a / b / c 的模式渲染", + "computed": false + }, + "docblock": "搜索结果自定义渲染函数\n@param {String} searchValue 搜索的关键字\n@param {Array} path 匹配到的节点路径\n@return {ReactNode} 渲染的内容\n@default 按照节点文本 a / b / c 的模式渲染", + "params": [{ + "name": "searchValue", + "description": "搜索的关键字", + "type": { + "name": "String" + } + }, + { + "name": "path", + "description": "匹配到的节点路径", + "type": { + "name": "Array" + } + } + ], + "returns": { + "description": "渲染的内容", + "type": { + "name": "ReactNode" + } + } + }, + "resultAutoWidth": { + "type": { + "name": "bool" + }, + "required": false, + "description": "搜索结果列表是否和选择框等宽", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "搜索结果列表是否和选择框等宽" + }, + "notFoundContent": { + "type": { + "name": "node" + }, + "required": false, + "description": "无数据时显示内容", + "defaultValue": { + "value": "'Not Found'", + "computed": false + }, + "docblock": "无数据时显示内容" + }, + "loadData": { + "type": { + "name": "func" + }, + "required": false, + "description": "异步加载数据函数", + "docblock": "异步加载数据函数\n@param {Object} data 当前点击异步加载的数据", + "params": [{ + "name": "data", + "description": "当前点击异步加载的数据", + "type": { + "name": "Object" + } + }], + "returns": null + }, + "header": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义下拉框头部", + "docblock": "自定义下拉框头部" + }, + "footer": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义下拉框底部", + "docblock": "自定义下拉框底部" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "初始下拉框是否显示", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "初始下拉框是否显示" + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "当前下拉框是否显示", + "docblock": "当前下拉框是否显示" + }, + "onVisibleChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "下拉框显示或关闭时触发事件的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "下拉框显示或关闭时触发事件的回调函数\n@param {Boolean} visible 是否显示\n@param {String} type 触发显示关闭的操作类型, fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "params": [{ + "name": "visible", + "description": "是否显示", + "type": { + "name": "Boolean" + } + }, + { + "name": "type", + "description": "触发显示关闭的操作类型, fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "type": { + "name": "String" + } + } + ], + "returns": null + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "下拉框自定义样式对象", + "docblock": "下拉框自定义样式对象", + "properties": [] + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "下拉框样式自定义类名", + "docblock": "下拉框样式自定义类名" + }, + "popupContainer": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "下拉框挂载的容器节点", + "docblock": "下拉框挂载的容器节点" + }, + "popupProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "透传到 Popup 的属性对象", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "透传到 Popup 的属性对象", + "properties": [] + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随滚动", + "docblock": "是否跟随滚动" + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "Checkbox", + "title": "复选按钮", + "typeId": 3, + "props": { + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "自定义类名", + "docblock": "自定义类名" + }, + "id": { + "type": { + "name": "string" + }, + "required": false, + "description": "checkbox id, 挂载在input上", + "docblock": "checkbox id, 挂载在input上" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内敛样式", + "docblock": "自定义内敛样式", + "properties": [] + }, + "checked": { + "type": { + "name": "bool" + }, + "required": false, + "description": "选中状态", + "docblock": "选中状态" + }, + "defaultChecked": { + "type": { + "name": "bool" + }, + "required": false, + "description": "默认选中状态", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "默认选中状态" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "禁用", + "docblock": "禁用" + }, + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "通过属性配置label,", + "docblock": "通过属性配置label," + }, + "indeterminate": { + "type": { + "name": "bool" + }, + "required": false, + "description": "Checkbox 的中间状态,只会影响到 Checkbox 的样式,并不影响其 checked 属性", + "docblock": "Checkbox 的中间状态,只会影响到 Checkbox 的样式,并不影响其 checked 属性" + }, + "defaultIndeterminate": { + "type": { + "name": "bool" + }, + "required": false, + "description": "Checkbox 的默认中间态,只会影响到 Checkbox 的样式,并不影响其 checked 属性", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "Checkbox 的默认中间态,只会影响到 Checkbox 的样式,并不影响其 checked 属性" + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "状态变化时触发的事件", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "状态变化时触发的事件\n@param {Boolean} checked 是否选中\n@param {Event} e Dom 事件对象", + "params": [{ + "name": "checked", + "description": "是否选中", + "type": { + "name": "Boolean" + } + }, + { + "name": "e", + "description": "Dom 事件对象", + "type": { + "name": "Event" + } + } + ], + "returns": null + }, + "onMouseEnter": { + "type": { + "name": "func" + }, + "required": false, + "description": "鼠标进入enter事件", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "鼠标进入enter事件\n@param {Event} e Dom 事件对象", + "params": [{ + "name": "e", + "description": "Dom 事件对象", + "type": { + "name": "Event" + } + }], + "returns": null + }, + "onMouseLeave": { + "type": { + "name": "func" + }, + "required": false, + "description": "鼠标离开Leave事件", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "鼠标离开Leave事件\n@param {Event} e Dom 事件对象", + "params": [{ + "name": "e", + "description": "Dom 事件对象", + "type": { + "name": "Event" + } + }], + "returns": null + } + }, + "methods": [], + "order": 1, + "subComponents": [{ + "name": "Group", + "title": "复选按钮组", + "typeId": 3, + "props": { + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "自定义类名", + "docblock": "自定义类名" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内敛样式", + "docblock": "自定义内敛样式", + "properties": [] + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "整体禁用", + "docblock": "整体禁用" + }, + "dataSource": { + "type": { + "name": "arrayOf", + "value": { + "name": "any" + } + }, + "required": false, + "description": "可选项列表, 数据项可为 String 或者 Object, 如 `['apple', 'pear', 'orange']` 或者 `[{value: 'apple', label: '苹果',}, {value: 'pear', label: '梨'}, {value: 'orange', label: '橙子'}]`", + "defaultValue": { + "value": "[]", + "computed": false + }, + "docblock": "可选项列表, 数据项可为 String 或者 Object, 如 `['apple', 'pear', 'orange']` 或者 `[{value: 'apple', label: '苹果',}, {value: 'pear', label: '梨'}, {value: 'orange', label: '橙子'}]`" + }, + "value": { + "type": { + "name": "union", + "value": [{ + "name": "array" + }, + { + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "被选中的值列表", + "docblock": "被选中的值列表" + }, + "defaultValue": { + "type": { + "name": "union", + "value": [{ + "name": "array" + }, + { + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "默认被选中的值列表", + "docblock": "默认被选中的值列表" + }, + "children": { + "type": { + "name": "arrayOf", + "value": { + "name": "element" + } + }, + "required": false, + "description": "通过子元素方式设置内部 checkbox", + "docblock": "通过子元素方式设置内部 checkbox" + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "选中值改变时的事件", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "选中值改变时的事件\n@param {Array} value 选中项列表\n@param {Event} e Dom 事件对象", + "params": [{ + "name": "value", + "description": "选中项列表", + "type": { + "name": "Array" + } + }, + { + "name": "e", + "description": "Dom 事件对象", + "type": { + "name": "Event" + } + } + ], + "returns": null + }, + "itemDirection": { + "type": { + "name": "enum", + "value": [{ + "value": "'hoz'", + "computed": false + }, + { + "value": "'ver'", + "computed": false + } + ] + }, + "required": false, + "description": "子项目的排列方式\n- hoz: 水平排列 (default)\n- ver: 垂直排列", + "defaultValue": { + "value": "'hoz'", + "computed": false + }, + "docblock": "子项目的排列方式\n- hoz: 水平排列 (default)\n- ver: 垂直排列" + } + }, + "methods": [] + }] + }, + { + "name": "Collapse", + "title": "折叠面板", + "typeId": 4, + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式前缀", + "defaultValue": { + "value": "'next-'", + "computed": false + }, + "docblock": "样式前缀" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "组件接受行内样式", + "docblock": "组件接受行内样式", + "properties": [] + }, + "dataSource": { + "type": { + "name": "array" + }, + "required": false, + "description": "使用数据模型构建", + "docblock": "使用数据模型构建" + }, + "defaultExpandedKeys": { + "type": { + "name": "array" + }, + "required": false, + "description": "默认展开keys", + "docblock": "默认展开keys" + }, + "expandedKeys": { + "type": { + "name": "array" + }, + "required": false, + "description": "受控展开keys", + "docblock": "受控展开keys" + }, + "onExpand": { + "type": { + "name": "func" + }, + "required": false, + "description": "展开状态发升变化时候的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "展开状态发升变化时候的回调", + "params": [], + "returns": null + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "所有禁用", + "docblock": "所有禁用" + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "扩展class", + "docblock": "扩展class" + }, + "accordion": { + "type": { + "name": "bool" + }, + "required": false, + "description": "手风琴模式,一次只能打开一个", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "手风琴模式,一次只能打开一个" + } + }, + "methods": [], + "subComponents": [{ + "name": "Panel", + "title": "单个折叠面板", + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式类名的品牌前缀", + "defaultValue": { + "value": "'next-'", + "computed": false + }, + "docblock": "样式类名的品牌前缀" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "子组件接受行内样式", + "docblock": "子组件接受行内样式", + "properties": [] + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁止用户操作", + "docblock": "是否禁止用户操作" + }, + "title": { + "type": { + "name": "node" + }, + "required": false, + "description": "标题", + "docblock": "标题" + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "扩展class", + "docblock": "扩展class" + } + }, + "methods": [] + }] + }, + { + "name": "ConfigProvider", + "title": "全局配置", + "typeId": 6, + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式类名的品牌前缀", + "docblock": "样式类名的品牌前缀" + }, + "locale": { + "type": { + "name": "object" + }, + "required": false, + "description": "国际化文案对象,属性为组件的 displayName", + "docblock": "国际化文案对象,属性为组件的 displayName", + "properties": [] + }, + "errorBoundary": { + "type": { + "name": "union", + "value": [{ + "name": "bool" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": "是否开启错误捕捉 errorBoundary\n如需自定义参数,请传入对象 对象接受参数列表如下:\n\nfallbackUI `Function(error?: {}, errorInfo?: {}) => Element` 捕获错误后的展示\nafterCatch `Function(error?: {}, errorInfo?: {})` 捕获错误后的行为, 比如埋点上传", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否开启错误捕捉 errorBoundary\n如需自定义参数,请传入对象 对象接受参数列表如下:\n\nfallbackUI `Function(error?: {}, errorInfo?: {}) => Element` 捕获错误后的展示\nafterCatch `Function(error?: {}, errorInfo?: {})` 捕获错误后的行为, 比如埋点上传" + }, + "pure": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否开启 Pure Render 模式,会提高性能,但是也会带来副作用", + "docblock": "是否开启 Pure Render 模式,会提高性能,但是也会带来副作用" + }, + "warning": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否在开发模式下显示组件属性被废弃的 warning 提示", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否在开发模式下显示组件属性被废弃的 warning 提示" + }, + "rtl": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否开启 rtl 模式", + "docblock": "是否开启 rtl 模式" + }, + "children": { + "type": { + "name": "element" + }, + "required": false, + "description": "组件树", + "docblock": "组件树" + } + }, + "methods": [{ + "name": "config", + "docblock": "传入组件,生成受 ConfigProvider 控制的 HOC 组件\n@param {Component} Component 组件类\n@param {Object} options 可选项\n@returns {Component} HOC", + "modifiers": [ + "static" + ], + "params": [{ + "name": "Component", + "description": "组件类", + "type": { + "name": "Component" + } + }, + { + "name": "options", + "description": "可选项", + "type": { + "name": "Object" + } + } + ], + "returns": { + "description": "HOC", + "type": { + "name": "Component" + } + }, + "description": "传入组件,生成受 ConfigProvider 控制的 HOC 组件" + }, + { + "name": "getContextProps", + "docblock": "传入组件的 props 和 displayName,得到和 childContext 计算过的包含有 preifx/locale/pure 的对象,一般用于通过静态方法生成脱离组件树的组件\n@param {Object} props 组件的 props\n@param {String} displayName 组件的 displayName\n@returns {Object} 新的 context props", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "组件的 props", + "type": { + "name": "Object" + } + }, + { + "name": "displayName", + "description": "组件的 displayName", + "type": { + "name": "String" + } + } + ], + "returns": { + "description": "新的 context props", + "type": { + "name": "Object" + } + }, + "description": "传入组件的 props 和 displayName,得到和 childContext 计算过的包含有 preifx/locale/pure 的对象,一般用于通过静态方法生成脱离组件树的组件" + } + ], + "propsExtends": false, + "subComponents": [] + }, + { + "name": "DatePicker", + "title": "日期选择框", + "typeId": 3, + "props": { + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框内置标签", + "docblock": "输入框内置标签" + }, + "state": { + "type": { + "name": "enum", + "value": [{ + "value": "'success'", + "computed": false + }, + { + "value": "'loading'", + "computed": false + }, + { + "value": "'error'", + "computed": false + } + ] + }, + "required": false, + "description": "输入框状态", + "docblock": "输入框状态" + }, + "placeholder": { + "type": { + "name": "string" + }, + "required": false, + "description": "输入提示", + "docblock": "输入提示" + }, + "defaultVisibleMonth": { + "type": { + "name": "func" + }, + "required": false, + "description": "默认展现的月", + "docblock": "默认展现的月\n@return {MomentObject} 返回包含指定月份的 moment 对象实例", + "params": [], + "returns": { + "description": "返回包含指定月份的 moment 对象实例", + "type": { + "name": "MomentObject" + } + } + }, + "value": { + "type": { + "name": "custom", + "raw": "checkDateValue" + }, + "required": false, + "description": "日期值(受控)moment 对象", + "docblock": "日期值(受控)moment 对象" + }, + "defaultValue": { + "type": { + "name": "custom", + "raw": "checkDateValue" + }, + "required": false, + "description": "初始日期值,moment 对象", + "docblock": "初始日期值,moment 对象" + }, + "format": { + "type": { + "name": "string" + }, + "required": false, + "description": "日期值的格式(用于限定用户输入和展示)", + "defaultValue": { + "value": "'YYYY-MM-DD'", + "computed": false + }, + "docblock": "日期值的格式(用于限定用户输入和展示)" + }, + "showTime": { + "type": { + "name": "union", + "value": [{ + "name": "object" + }, + { + "name": "bool" + } + ] + }, + "required": false, + "description": "是否使用时间控件,传入 TimePicker 的属性 { defaultValue, format, ... }", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否使用时间控件,传入 TimePicker 的属性 { defaultValue, format, ... }" + }, + "resetTime": { + "type": { + "name": "bool" + }, + "required": false, + "description": "每次选择日期时是否重置时间(仅在 showTime 开启时有效)", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "每次选择日期时是否重置时间(仅在 showTime 开启时有效)" + }, + "disabledDate": { + "type": { + "name": "func" + }, + "required": false, + "description": "禁用日期函数", + "defaultValue": { + "value": "() => false", + "computed": false + }, + "docblock": "禁用日期函数\n@param {MomentObject} 日期值\n@param {String} view 当前视图类型,year: 年, month: 月, date: 日\n@return {Boolean} 是否禁用", + "params": [{ + "name": "日期值", + "description": null, + "type": { + "name": "MomentObject" + } + }, + { + "name": "view", + "description": "当前视图类型,year: 年, month: 月, date: 日", + "type": { + "name": "String" + } + } + ], + "returns": { + "description": "是否禁用", + "type": { + "name": "Boolean" + } + } + }, + "footerRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义面板页脚", + "defaultValue": { + "value": "() => null", + "computed": false + }, + "docblock": "自定义面板页脚\n@return {Node} 自定义的面板页脚组件", + "params": [], + "returns": { + "description": "自定义的面板页脚组件", + "type": { + "name": "Node" + } + } + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "日期值改变时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "日期值改变时的回调\n@param {MomentObject|String} value 日期值", + "params": [{ + "name": "value", + "description": "日期值", + "type": { + "name": "union", + "value": [ + "MomentObject", + "String" + ] + } + }], + "returns": null + }, + "onOk": { + "type": { + "name": "func" + }, + "required": false, + "description": "点击确认按钮时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "点击确认按钮时的回调\n@return {MomentObject|String} 日期值", + "params": [], + "returns": { + "description": "日期值", + "type": { + "name": "union", + "value": [ + "MomentObject", + "String" + ] + } + } + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "输入框尺寸", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "输入框尺寸" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "docblock": "是否禁用" + }, + "hasClear": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示清空按钮", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示清空按钮" + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层显示状态", + "docblock": "弹层显示状态" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层默认是否显示", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "弹层默认是否显示" + }, + "onVisibleChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层展示状态变化时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层展示状态变化时的回调\n@param {Boolean} visible 弹层是否显示\n@param {String} type 触发弹层显示和隐藏的来源 calendarSelect 表示由日期表盘的选择触发; okBtnClick 表示由确认按钮触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "params": [{ + "name": "visible", + "description": "弹层是否显示", + "type": { + "name": "Boolean" + } + }, + { + "name": "type", + "description": "触发弹层显示和隐藏的来源 calendarSelect 表示由日期表盘的选择触发; okBtnClick 表示由确认按钮触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "type": { + "name": "String" + } + } + ], + "returns": null + }, + "popupTriggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "弹层触发方式", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "弹层触发方式" + }, + "popupAlign": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层对齐方式,具体含义见 OverLay文档", + "defaultValue": { + "value": "'tl tl'", + "computed": false + }, + "docblock": "弹层对齐方式,具体含义见 OverLay文档" + }, + "popupContainer": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层容器", + "docblock": "弹层容器\n@param {Element} target 目标元素\n@return {Element} 弹层的容器元素", + "params": [{ + "name": "target", + "description": "目标元素", + "type": { + "name": "Element" + } + }], + "returns": { + "description": "弹层的容器元素", + "type": { + "name": "Element" + } + } + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层自定义样式", + "docblock": "弹层自定义样式", + "properties": [] + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层自定义样式类", + "docblock": "弹层自定义样式类" + }, + "popupProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层其他属性", + "docblock": "弹层其他属性", + "properties": [] + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随滚动", + "docblock": "是否跟随滚动" + }, + "inputProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "输入框其他属性", + "docblock": "输入框其他属性", + "properties": [] + }, + "dateCellRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义日期渲染函数", + "docblock": "自定义日期渲染函数\n@param {Object} value 日期值(moment对象)\n@returns {ReactNode}", + "params": [{ + "name": "value", + "description": "日期值(moment对象)", + "type": { + "name": "Object" + } + }], + "returns": { + "description": null, + "type": { + "name": "ReactNode" + } + } + }, + "monthCellRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义月份渲染函数", + "docblock": "自定义月份渲染函数\n@param {Object} calendarDate 对应 Calendar 返回的自定义日期对象\n@returns {ReactNode}", + "params": [{ + "name": "calendarDate", + "description": "对应 Calendar 返回的自定义日期对象", + "type": { + "name": "Object" + } + }], + "returns": { + "description": null, + "type": { + "name": "ReactNode" + } + } + }, + "dateInputAriaLabel": { + "type": { + "name": "string" + }, + "required": false, + "description": "日期输入框的 aria-label 属性", + "docblock": "日期输入框的 aria-label 属性" + }, + "timeInputAriaLabel": { + "type": { + "name": "string" + }, + "required": false, + "description": "时间输入框的 aria-label 属性", + "docblock": "时间输入框的 aria-label 属性" + } + }, + "methods": [], + "subComponents": [{ + "name": "MonthPicker", + "title": "月份选择框", + "props": { + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框内置标签", + "docblock": "输入框内置标签" + }, + "state": { + "type": { + "name": "enum", + "value": [{ + "value": "'success'", + "computed": false + }, + { + "value": "'loading'", + "computed": false + }, + { + "value": "'error'", + "computed": false + } + ] + }, + "required": false, + "description": "输入框状态", + "docblock": "输入框状态" + }, + "placeholder": { + "type": { + "name": "string" + }, + "required": false, + "description": "输入提示", + "docblock": "输入提示" + }, + "defaultVisibleYear": { + "type": { + "name": "func" + }, + "required": false, + "description": "默认展现的年", + "docblock": "默认展现的年\n@return {MomentObject} 返回包含指定年份的 moment 对象实例", + "params": [], + "returns": { + "description": "返回包含指定年份的 moment 对象实例", + "type": { + "name": "MomentObject" + } + } + }, + "value": { + "type": { + "name": "custom", + "raw": "checkDateValue" + }, + "required": false, + "description": "日期值(受控)moment 对象", + "docblock": "日期值(受控)moment 对象" + }, + "defaultValue": { + "type": { + "name": "custom", + "raw": "checkDateValue" + }, + "required": false, + "description": "初始日期值,moment 对象", + "docblock": "初始日期值,moment 对象" + }, + "format": { + "type": { + "name": "string" + }, + "required": false, + "description": "日期值的格式(用于限定用户输入和展示)", + "defaultValue": { + "value": "'YYYY-MM'", + "computed": false + }, + "docblock": "日期值的格式(用于限定用户输入和展示)" + }, + "disabledDate": { + "type": { + "name": "func" + }, + "required": false, + "description": "禁用日期函数", + "defaultValue": { + "value": "() => false", + "computed": false + }, + "docblock": "禁用日期函数\n@param {MomentObject} 日期值\n@param {String} view 当前视图类型,year: 年, month: 月, date: 日\n@return {Boolean} 是否禁用", + "params": [{ + "name": "日期值", + "description": null, + "type": { + "name": "MomentObject" + } + }, + { + "name": "view", + "description": "当前视图类型,year: 年, month: 月, date: 日", + "type": { + "name": "String" + } + } + ], + "returns": { + "description": "是否禁用", + "type": { + "name": "Boolean" + } + } + }, + "footerRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义面板页脚", + "defaultValue": { + "value": "() => null", + "computed": false + }, + "docblock": "自定义面板页脚\n@return {Node} 自定义的面板页脚组件", + "params": [], + "returns": { + "description": "自定义的面板页脚组件", + "type": { + "name": "Node" + } + } + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "日期值改变时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "日期值改变时的回调\n@param {MomentObject|String} value 日期值", + "params": [{ + "name": "value", + "description": "日期值", + "type": { + "name": "union", + "value": [ + "MomentObject", + "String" + ] + } + }], + "returns": null + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "输入框尺寸", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "输入框尺寸" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "docblock": "是否禁用" + }, + "hasClear": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示清空按钮", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示清空按钮" + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层显示状态", + "docblock": "弹层显示状态" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层默认是否显示", + "docblock": "弹层默认是否显示" + }, + "onVisibleChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层展示状态变化时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层展示状态变化时的回调\n@param {Boolean} visible 弹层是否显示\n@param {String} type 触发弹层显示和隐藏的来源 calendarSelect 表示由日期表盘的选择触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "params": [{ + "name": "visible", + "description": "弹层是否显示", + "type": { + "name": "Boolean" + } + }, + { + "name": "type", + "description": "触发弹层显示和隐藏的来源 calendarSelect 表示由日期表盘的选择触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "type": { + "name": "String" + } + } + ], + "returns": null + }, + "popupTriggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "弹层触发方式", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "弹层触发方式" + }, + "popupAlign": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层对齐方式, 具体含义见 OverLay文档", + "defaultValue": { + "value": "'tl tl'", + "computed": false + }, + "docblock": "弹层对齐方式, 具体含义见 OverLay文档" + }, + "popupContainer": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层容器", + "docblock": "弹层容器\n@param {Element} target 目标元素\n@return {Element} 弹层的容器元素", + "params": [{ + "name": "target", + "description": "目标元素", + "type": { + "name": "Element" + } + }], + "returns": { + "description": "弹层的容器元素", + "type": { + "name": "Element" + } + } + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层自定义样式", + "docblock": "弹层自定义样式", + "properties": [] + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层自定义样式类", + "docblock": "弹层自定义样式类" + }, + "popupProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层其他属性", + "docblock": "弹层其他属性", + "properties": [] + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随滚动", + "docblock": "是否跟随滚动" + }, + "inputProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "输入框其他属性", + "docblock": "输入框其他属性", + "properties": [] + }, + "monthCellRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义月份渲染函数", + "docblock": "自定义月份渲染函数\n@param {Object} calendarDate 对应 Calendar 返回的自定义日期对象\n@returns {ReactNode}", + "params": [{ + "name": "calendarDate", + "description": "对应 Calendar 返回的自定义日期对象", + "type": { + "name": "Object" + } + }], + "returns": { + "description": null, + "type": { + "name": "ReactNode" + } + } + }, + "dateInputAriaLabel": { + "type": { + "name": "string" + }, + "required": false, + "description": "日期输入框的 aria-label 属性", + "docblock": "日期输入框的 aria-label 属性" + } + }, + "methods": [] + }, + { + "name": "RangePicker", + "title": "区间选择框", + "props": { + "defaultVisibleMonth": { + "type": { + "name": "func" + }, + "required": false, + "description": "默认展示的起始月份", + "docblock": "默认展示的起始月份\n@return {MomentObject} 返回包含指定月份的 moment 对象实例", + "params": [], + "returns": { + "description": "返回包含指定月份的 moment 对象实例", + "type": { + "name": "MomentObject" + } + } + }, + "value": { + "type": { + "name": "array" + }, + "required": false, + "description": "日期范围值数组 [moment, moment]", + "docblock": "日期范围值数组 [moment, moment]" + }, + "defaultValue": { + "type": { + "name": "array" + }, + "required": false, + "description": "初始的日期范围值数组 [moment, moment]", + "docblock": "初始的日期范围值数组 [moment, moment]" + }, + "format": { + "type": { + "name": "string" + }, + "required": false, + "description": "日期格式", + "defaultValue": { + "value": "'YYYY-MM-DD'", + "computed": false + }, + "docblock": "日期格式" + }, + "showTime": { + "type": { + "name": "union", + "value": [{ + "name": "object" + }, + { + "name": "bool" + } + ] + }, + "required": false, + "description": "是否使用时间控件,支持传入 TimePicker 的属性", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否使用时间控件,支持传入 TimePicker 的属性" + }, + "resetTime": { + "type": { + "name": "bool" + }, + "required": false, + "description": "每次选择是否重置时间(仅在 showTime 开启时有效)", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "每次选择是否重置时间(仅在 showTime 开启时有效)" + }, + "disabledDate": { + "type": { + "name": "func" + }, + "required": false, + "description": "禁用日期函数", + "defaultValue": { + "value": "() => false", + "computed": false + }, + "docblock": "禁用日期函数\n@param {MomentObject} 日期值\n@param {String} view 当前视图类型,year: 年, month: 月, date: 日\n@return {Boolean} 是否禁用", + "params": [{ + "name": "日期值", + "description": null, + "type": { + "name": "MomentObject" + } + }, + { + "name": "view", + "description": "当前视图类型,year: 年, month: 月, date: 日", + "type": { + "name": "String" + } + } + ], + "returns": { + "description": "是否禁用", + "type": { + "name": "Boolean" + } + } + }, + "footerRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义面板页脚", + "defaultValue": { + "value": "() => null", + "computed": false + }, + "docblock": "自定义面板页脚\n@return {Node} 自定义的面板页脚组件", + "params": [], + "returns": { + "description": "自定义的面板页脚组件", + "type": { + "name": "Node" + } + } + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "日期范围值改变时的回调 [ MomentObject|String, MomentObject|String ]", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "日期范围值改变时的回调 [ MomentObject|String, MomentObject|String ]\n@param {Array} value 日期值", + "params": [{ + "name": "value", + "description": "日期值", + "type": { + "name": "Array" + } + }], + "returns": null + }, + "onOk": { + "type": { + "name": "func" + }, + "required": false, + "description": "点击确认按钮时的回调 返回开始时间和结束时间`[ MomentObject|String, MomentObject|String ]`", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "点击确认按钮时的回调 返回开始时间和结束时间`[ MomentObject|String, MomentObject|String ]`\n@return {Array} 日期范围", + "params": [], + "returns": { + "description": "日期范围", + "type": { + "name": "Array" + } + } + }, + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框内置标签", + "docblock": "输入框内置标签" + }, + "state": { + "type": { + "name": "enum", + "value": [{ + "value": "'error'", + "computed": false + }, + { + "value": "'loading'", + "computed": false + }, + { + "value": "'success'", + "computed": false + } + ] + }, + "required": false, + "description": "输入框状态", + "docblock": "输入框状态" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "输入框尺寸", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "输入框尺寸" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "docblock": "是否禁用" + }, + "hasClear": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示清空按钮", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示清空按钮" + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层显示状态", + "docblock": "弹层显示状态" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层默认是否显示", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "弹层默认是否显示" + }, + "onVisibleChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层展示状态变化时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层展示状态变化时的回调\n@param {Boolean} visible 弹层是否显示\n@param {String} type 触发弹层显示和隐藏的来源 okBtnClick 表示由确认按钮触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "params": [{ + "name": "visible", + "description": "弹层是否显示", + "type": { + "name": "Boolean" + } + }, + { + "name": "type", + "description": "触发弹层显示和隐藏的来源 okBtnClick 表示由确认按钮触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "type": { + "name": "String" + } + } + ], + "returns": null + }, + "popupTriggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "弹层触发方式", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "弹层触发方式" + }, + "popupAlign": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层对齐方式, 具体含义见 OverLay文档", + "defaultValue": { + "value": "'tl tl'", + "computed": false + }, + "docblock": "弹层对齐方式, 具体含义见 OverLay文档" + }, + "popupContainer": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层容器", + "docblock": "弹层容器\n@param {Element} target 目标元素\n@return {Element} 弹层的容器元素", + "params": [{ + "name": "target", + "description": "目标元素", + "type": { + "name": "Element" + } + }], + "returns": { + "description": "弹层的容器元素", + "type": { + "name": "Element" + } + } + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层自定义样式", + "docblock": "弹层自定义样式", + "properties": [] + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层自定义样式类", + "docblock": "弹层自定义样式类" + }, + "popupProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层其他属性", + "docblock": "弹层其他属性", + "properties": [] + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随滚动", + "docblock": "是否跟随滚动" + }, + "inputProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "输入框其他属性", + "docblock": "输入框其他属性", + "properties": [] + }, + "dateCellRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义日期单元格渲染", + "docblock": "自定义日期单元格渲染", + "params": [], + "returns": null + }, + "monthCellRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义月份渲染函数", + "docblock": "自定义月份渲染函数\n@param {Object} calendarDate 对应 Calendar 返回的自定义日期对象\n@returns {ReactNode}", + "params": [{ + "name": "calendarDate", + "description": "对应 Calendar 返回的自定义日期对象", + "type": { + "name": "Object" + } + }], + "returns": { + "description": null, + "type": { + "name": "ReactNode" + } + } + }, + "startDateInputAriaLabel": { + "type": { + "name": "string" + }, + "required": false, + "description": "开始日期输入框的 aria-label 属性", + "docblock": "开始日期输入框的 aria-label 属性" + }, + "startTimeInputAriaLabel": { + "type": { + "name": "string" + }, + "required": false, + "description": "开始时间输入框的 aria-label 属性", + "docblock": "开始时间输入框的 aria-label 属性" + }, + "endDateInputAriaLabel": { + "type": { + "name": "string" + }, + "required": false, + "description": "结束日期输入框的 aria-label 属性", + "docblock": "结束日期输入框的 aria-label 属性" + }, + "endTimeInputAriaLabel": { + "type": { + "name": "string" + }, + "required": false, + "description": "结束时间输入框的 aria-label 属性", + "docblock": "结束时间输入框的 aria-label 属性" + } + }, + "methods": [] + }, + { + "name": "YearPicker", + "title": "年份选择框", + "props": { + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框内置标签", + "docblock": "输入框内置标签" + }, + "state": { + "type": { + "name": "enum", + "value": [{ + "value": "'success'", + "computed": false + }, + { + "value": "'loading'", + "computed": false + }, + { + "value": "'error'", + "computed": false + } + ] + }, + "required": false, + "description": "输入框状态", + "docblock": "输入框状态" + }, + "placeholder": { + "type": { + "name": "string" + }, + "required": false, + "description": "输入提示", + "docblock": "输入提示" + }, + "value": { + "type": { + "name": "custom", + "raw": "checkDateValue" + }, + "required": false, + "description": "日期值(受控)moment 对象", + "docblock": "日期值(受控)moment 对象" + }, + "defaultValue": { + "type": { + "name": "custom", + "raw": "checkDateValue" + }, + "required": false, + "description": "初始日期值,moment 对象", + "docblock": "初始日期值,moment 对象" + }, + "format": { + "type": { + "name": "string" + }, + "required": false, + "description": "日期值的格式(用于限定用户输入和展示)", + "defaultValue": { + "value": "'YYYY'", + "computed": false + }, + "docblock": "日期值的格式(用于限定用户输入和展示)" + }, + "disabledDate": { + "type": { + "name": "func" + }, + "required": false, + "description": "禁用日期函数", + "defaultValue": { + "value": "() => false", + "computed": false + }, + "docblock": "禁用日期函数\n@param {MomentObject} 日期值\n@param {String} view 当前视图类型,year: 年, month: 月, date: 日\n@return {Boolean} 是否禁用", + "params": [{ + "name": "日期值", + "description": null, + "type": { + "name": "MomentObject" + } + }, + { + "name": "view", + "description": "当前视图类型,year: 年, month: 月, date: 日", + "type": { + "name": "String" + } + } + ], + "returns": { + "description": "是否禁用", + "type": { + "name": "Boolean" + } + } + }, + "footerRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义面板页脚", + "defaultValue": { + "value": "() => null", + "computed": false + }, + "docblock": "自定义面板页脚\n@return {Node} 自定义的面板页脚组件", + "params": [], + "returns": { + "description": "自定义的面板页脚组件", + "type": { + "name": "Node" + } + } + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "日期值改变时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "日期值改变时的回调\n@param {MomentObject|String} value 日期值", + "params": [{ + "name": "value", + "description": "日期值", + "type": { + "name": "union", + "value": [ + "MomentObject", + "String" + ] + } + }], + "returns": null + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "输入框尺寸", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "输入框尺寸" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "docblock": "是否禁用" + }, + "hasClear": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示清空按钮", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示清空按钮" + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层显示状态", + "docblock": "弹层显示状态" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层默认是否显示", + "docblock": "弹层默认是否显示" + }, + "onVisibleChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层展示状态变化时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层展示状态变化时的回调\n@param {Boolean} visible 弹层是否显示\n@param {String} reason 触发弹层显示和隐藏的来源 calendarSelect 表示由日期表盘的选择触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "params": [{ + "name": "visible", + "description": "弹层是否显示", + "type": { + "name": "Boolean" + } + }, + { + "name": "reason", + "description": "触发弹层显示和隐藏的来源 calendarSelect 表示由日期表盘的选择触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "type": { + "name": "String" + } + } + ], + "returns": null + }, + "popupTriggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "弹层触发方式", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "弹层触发方式" + }, + "popupAlign": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层对齐方式, 具体含义见 OverLay文档", + "defaultValue": { + "value": "'tl tl'", + "computed": false + }, + "docblock": "弹层对齐方式, 具体含义见 OverLay文档" + }, + "popupContainer": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层容器", + "docblock": "弹层容器\n@param {Element} target 目标元素\n@return {Element} 弹层的容器元素", + "params": [{ + "name": "target", + "description": "目标元素", + "type": { + "name": "Element" + } + }], + "returns": { + "description": "弹层的容器元素", + "type": { + "name": "Element" + } + } + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层自定义样式", + "docblock": "弹层自定义样式", + "properties": [] + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层自定义样式类", + "docblock": "弹层自定义样式类" + }, + "popupProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层其他属性", + "docblock": "弹层其他属性", + "properties": [] + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随滚动", + "docblock": "是否跟随滚动" + }, + "inputProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "输入框其他属性", + "docblock": "输入框其他属性", + "properties": [] + }, + "dateInputAriaLabel": { + "type": { + "name": "string" + }, + "required": false, + "description": "日期输入框的 aria-label 属性", + "docblock": "日期输入框的 aria-label 属性" + } + }, + "methods": [] + } + ] + }, + { + "name": "Dialog", + "title": "弹窗", + "typeId": 5, + "props": { + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否显示" + }, + "title": { + "type": { + "name": "node" + }, + "required": false, + "description": "标题", + "docblock": "标题" + }, + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "内容", + "docblock": "内容" + }, + "footer": { + "type": { + "name": "union", + "value": [{ + "name": "bool" + }, + { + "name": "node" + } + ] + }, + "required": false, + "description": "底部内容,设置为 false,则不进行显示", + "docblock": "底部内容,设置为 false,则不进行显示\n@default [, ]", + "defaultValue": { + "value": "[, ]", + "computed": false + } + }, + "footerAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'left'", + "computed": false + }, + { + "value": "'center'", + "computed": false + }, + { + "value": "'right'", + "computed": false + } + ] + }, + "required": false, + "description": "底部按钮的对齐方式", + "defaultValue": { + "value": "'right'", + "computed": false + }, + "docblock": "底部按钮的对齐方式" + }, + "footerActions": { + "type": { + "name": "array" + }, + "required": false, + "description": "指定确定按钮和取消按钮是否存在以及如何排列,

    **可选值**:\n['ok', 'cancel'](确认取消按钮同时存在,确认按钮在左)\n['cancel', 'ok'](确认取消按钮同时存在,确认按钮在右)\n['ok'](只存在确认按钮)\n['cancel'](只存在取消按钮)", + "defaultValue": { + "value": "['ok', 'cancel']", + "computed": false + }, + "docblock": "指定确定按钮和取消按钮是否存在以及如何排列,

    **可选值**:\n['ok', 'cancel'](确认取消按钮同时存在,确认按钮在左)\n['cancel', 'ok'](确认取消按钮同时存在,确认按钮在右)\n['ok'](只存在确认按钮)\n['cancel'](只存在取消按钮)" + }, + "onOk": { + "type": { + "name": "func" + }, + "required": false, + "description": "在点击确定按钮时触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "在点击确定按钮时触发的回调函数\n@param {Object} event 点击事件对象", + "params": [{ + "name": "event", + "description": "点击事件对象", + "type": { + "name": "Object" + } + }], + "returns": null + }, + "onCancel": { + "type": { + "name": "func" + }, + "required": false, + "description": "在点击取消按钮时触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "在点击取消按钮时触发的回调函数\n@param {Object} event 点击事件对象", + "params": [{ + "name": "event", + "description": "点击事件对象", + "type": { + "name": "Object" + } + }], + "returns": null + }, + "okProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "应用于确定按钮的属性对象", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "应用于确定按钮的属性对象", + "properties": [] + }, + "cancelProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "应用于取消按钮的属性对象", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "应用于取消按钮的属性对象", + "properties": [] + }, + "closeable": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "bool" + } + ] + }, + "required": false, + "description": "控制对话框关闭的方式,值可以为字符串或者布尔值,其中字符串是由以下值组成:\n**close** 表示点击关闭按钮可以关闭对话框\n**mask** 表示点击遮罩区域可以关闭对话框\n**esc** 表示按下 esc 键可以关闭对话框\n如 'close' 或 'close,esc,mask'\n如果设置为 true,则以上关闭方式全部生效\n如果设置为 false,则以上关闭方式全部失效", + "defaultValue": { + "value": "'esc,close'", + "computed": false + }, + "docblock": "控制对话框关闭的方式,值可以为字符串或者布尔值,其中字符串是由以下值组成:\n**close** 表示点击关闭按钮可以关闭对话框\n**mask** 表示点击遮罩区域可以关闭对话框\n**esc** 表示按下 esc 键可以关闭对话框\n如 'close' 或 'close,esc,mask'\n如果设置为 true,则以上关闭方式全部生效\n如果设置为 false,则以上关闭方式全部失效" + }, + "onClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "对话框关闭时触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "对话框关闭时触发的回调函数\n@param {String} trigger 关闭触发行为的描述字符串\n@param {Object} event 关闭时事件对象", + "params": [{ + "name": "trigger", + "description": "关闭触发行为的描述字符串", + "type": { + "name": "String" + } + }, + { + "name": "event", + "description": "关闭时事件对象", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "afterClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "对话框关闭后触发的回调函数, 如果有动画,则在动画结束后触发", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "对话框关闭后触发的回调函数, 如果有动画,则在动画结束后触发", + "params": [], + "returns": null + }, + "hasMask": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示遮罩", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示遮罩" + }, + "animation": { + "type": { + "name": "union", + "value": [{ + "name": "object" + }, + { + "name": "bool" + } + ] + }, + "required": false, + "description": "显示隐藏时动画的播放方式", + "defaultValue": { + "value": "{\n in: 'fadeInDown',\n out: 'fadeOutUp',\n}", + "computed": false + }, + "docblock": "显示隐藏时动画的播放方式\n@property {String} in 进场动画\n@property {String} out 出场动画" + }, + "autoFocus": { + "type": { + "name": "bool" + }, + "required": false, + "description": "对话框弹出时是否自动获得焦点", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "对话框弹出时是否自动获得焦点" + }, + "align": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "bool" + } + ] + }, + "required": false, + "description": "对话框对齐方式, 具体见Overlay文档", + "defaultValue": { + "value": "'cc cc'", + "computed": false + }, + "docblock": "对话框对齐方式, 具体见Overlay文档" + }, + "isFullScreen": { + "type": { + "name": "bool" + }, + "required": false, + "description": "当对话框高度超过浏览器视口高度时,是否显示所有内容而不是出现滚动条以保证对话框完整显示在浏览器视口内,该属性仅在对话框垂直水平居中时生效,即 align 被设置为 'cc cc' 时", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "当对话框高度超过浏览器视口高度时,是否显示所有内容而不是出现滚动条以保证对话框完整显示在浏览器视口内,该属性仅在对话框垂直水平居中时生效,即 align 被设置为 'cc cc' 时" + }, + "shouldUpdatePosition": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否在对话框重新渲染时及时更新对话框位置,一般用于对话框高度变化后依然能保证原来的对齐方式", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否在对话框重新渲染时及时更新对话框位置,一般用于对话框高度变化后依然能保证原来的对齐方式" + }, + "minMargin": { + "type": { + "name": "number" + }, + "required": false, + "description": "对话框距离浏览器顶部和底部的最小间距,align 被设置为 'cc cc' 并且 isFullScreen 被设置为 true 时不生效", + "defaultValue": { + "value": "40", + "computed": false + }, + "docblock": "对话框距离浏览器顶部和底部的最小间距,align 被设置为 'cc cc' 并且 isFullScreen 被设置为 true 时不生效" + }, + "overlayProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "透传到弹层组件的属性对象", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "透传到弹层组件的属性对象", + "properties": [] + }, + "locale": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义国际化文案对象", + "defaultValue": { + "value": "zhCN.Dialog", + "computed": true + }, + "docblock": "自定义国际化文案对象\n@property {String} ok 确认按钮文案\n@property {String} cancel 取消按钮文案", + "properties": [{ + "name": "ok", + "description": "确认按钮文案", + "type": { + "name": "String" + } + }, + { + "name": "cancel", + "description": "取消按钮文案", + "type": { + "name": "String" + } + } + ] + }, + "height": { + "type": { + "name": "string" + }, + "required": false, + "description": "对话框的高度样式属性", + "docblock": "对话框的高度样式属性" + } + }, + "methods": [{ + "name": "show", + "docblock": "\n 创建对话框\n @exportName show\n @param {Object} config 配置项\n @returns {Object} \b包含有 hide 方法,可用来关闭对话框\n ", + "description": "创建对话框", + "modifiers": [ + "static" + ], + "params": [{ + "name": "config", + "description": "配置项", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": { + "description": "\b包含有 hide 方法,可用来关闭对话框", + "type": { + "type": "NameExpression", + "name": "Object" + } + } + }, + { + "name": "alert", + "docblock": "\n 创建警示对话框\n @exportName alert\n @param {Object} config 配置项\n @returns {Object} \b包含有 hide 方法,可用来关闭对话框\n ", + "description": "创建警示对话框", + "modifiers": [ + "static" + ], + "params": [{ + "name": "config", + "description": "配置项", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": { + "description": "\b包含有 hide 方法,可用来关闭对话框", + "type": { + "type": "NameExpression", + "name": "Object" + } + } + }, + { + "name": "confirm", + "docblock": "\n 创建确认对话框\n @exportName confirm\n @param {Object} config 配置项\n @returns {Object} \b包含有 hide 方法,可用来关闭对话框\n ", + "description": "创建确认对话框", + "modifiers": [ + "static" + ], + "params": [{ + "name": "config", + "description": "配置项", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": { + "description": "\b包含有 hide 方法,可用来关闭对话框", + "type": { + "type": "NameExpression", + "name": "Object" + } + } + } + ], + "subComponents": [] + }, + { + "name": "Dropdown", + "title": "下拉菜单", + "typeId": 6, + "props": { + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "弹层内容", + "docblock": "弹层内容" + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层当前是否显示", + "docblock": "弹层当前是否显示" + }, + "onRequestClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层请求关闭时触发事件的回调函数", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层请求关闭时触发事件的回调函数\n@param {String} type 弹层关闭的来源\n@param {Object} e DOM 事件", + "params": [{ + "name": "type", + "description": "弹层关闭的来源", + "type": { + "name": "String" + } + }, + { + "name": "e", + "description": "DOM 事件", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "target": { + "type": { + "name": "any" + }, + "required": false, + "description": "弹层定位的参照元素", + "docblock": "弹层定位的参照元素\n@default target 属性,即触发元素", + "defaultValue": { + "value": "target 属性,即触发元素", + "computed": false + } + }, + "align": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层相对于触发元素的定位, 详见 Overlay 的定位部分", + "defaultValue": { + "value": "'tl bl'", + "computed": false + }, + "docblock": "弹层相对于触发元素的定位, 详见 Overlay 的定位部分" + }, + "offset": { + "type": { + "name": "array" + }, + "required": false, + "description": "弹层相对于trigger的定位的微调, 接收数组[hoz, ver], 表示弹层在 left / top 上的增量\ne.g. [100, 100] 表示往右(RTL 模式下是往左) 、下分布偏移100px", + "defaultValue": { + "value": "[0, 0]", + "computed": false + }, + "docblock": "弹层相对于trigger的定位的微调, 接收数组[hoz, ver], 表示弹层在 left / top 上的增量\ne.g. [100, 100] 表示往右(RTL 模式下是往左) 、下分布偏移100px" + }, + "container": { + "type": { + "name": "any" + }, + "required": false, + "description": "渲染组件的容器,如果是函数需要返回 ref,如果是字符串则是该 DOM 的 id,也可以直接传入 DOM 节点", + "docblock": "渲染组件的容器,如果是函数需要返回 ref,如果是字符串则是该 DOM 的 id,也可以直接传入 DOM 节点" + }, + "hasMask": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示遮罩", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否显示遮罩" + }, + "canCloseByEsc": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否支持 esc 按键关闭弹层", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否支持 esc 按键关闭弹层" + }, + "canCloseByOutSideClick": { + "type": { + "name": "bool" + }, + "required": false, + "description": "点击弹层外的区域是否关闭弹层,不显示遮罩时生效", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "点击弹层外的区域是否关闭弹层,不显示遮罩时生效" + }, + "canCloseByMask": { + "type": { + "name": "bool" + }, + "required": false, + "description": "点击遮罩区域是否关闭弹层,显示遮罩时生效", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "点击遮罩区域是否关闭弹层,显示遮罩时生效" + }, + "beforeOpen": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层打开前触发事件的回调函数", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层打开前触发事件的回调函数", + "params": [], + "returns": null + }, + "onOpen": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层打开时触发事件的回调函数", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层打开时触发事件的回调函数", + "params": [], + "returns": null + }, + "afterOpen": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层打开后触发事件的回调函数, 如果有动画,则在动画结束后触发", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层打开后触发事件的回调函数, 如果有动画,则在动画结束后触发", + "params": [], + "returns": null + }, + "beforeClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层关闭前触发事件的回调函数", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层关闭前触发事件的回调函数", + "params": [], + "returns": null + }, + "onClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层关闭时触发事件的回调函数", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层关闭时触发事件的回调函数", + "params": [], + "returns": null + }, + "afterClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层关闭后触发事件的回调函数, 如果有动画,则在动画结束后触发", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层关闭后触发事件的回调函数, 如果有动画,则在动画结束后触发", + "params": [], + "returns": null + }, + "beforePosition": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层定位完成前触发的事件", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层定位完成前触发的事件", + "params": [], + "returns": null + }, + "onPosition": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层定位完成时触发的事件", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层定位完成时触发的事件\n@param {Object} config 定位的参数\n@param {Array} config.align 对齐方式,如 ['cc', 'cc'](如果开启 needAdjust,可能和预先设置的 align 不同)\n@param {Number} config.top 距离视口顶部距离\n@param {Number} config.left 距离视口左侧距离\n@param {Object} node 定位参照的容器节点", + "params": [{ + "name": "config", + "description": "定位的参数", + "type": { + "name": "Object" + } + }, + { + "name": "config.align", + "description": "对齐方式,如 ['cc', 'cc'](如果开启 needAdjust,可能和预先设置的 align 不同)", + "type": { + "name": "Array" + } + }, + { + "name": "config.top", + "description": "距离视口顶部距离", + "type": { + "name": "Number" + } + }, + { + "name": "config.left", + "description": "距离视口左侧距离", + "type": { + "name": "Number" + } + }, + { + "name": "node", + "description": "定位参照的容器节点", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "shouldUpdatePosition": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否在每次弹层重新渲染后强制更新定位信息,一般用于弹层内容区域大小发生变化时,仍需保持原来的定位方式", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否在每次弹层重新渲染后强制更新定位信息,一般用于弹层内容区域大小发生变化时,仍需保持原来的定位方式" + }, + "autoFocus": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层打开时是否让其中的元素自动获取焦点", + "docblock": "弹层打开时是否让其中的元素自动获取焦点" + }, + "needAdjust": { + "type": { + "name": "bool" + }, + "required": false, + "description": "当弹层由于页面滚动等情况不在可视区域时,是否自动调整定位以出现在可视区域", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "当弹层由于页面滚动等情况不在可视区域时,是否自动调整定位以出现在可视区域" + }, + "disableScroll": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用页面滚动", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否禁用页面滚动" + }, + "cache": { + "type": { + "name": "bool" + }, + "required": false, + "description": "隐藏时是否保留子节点", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "隐藏时是否保留子节点" + }, + "safeNode": { + "type": { + "name": "any" + }, + "required": false, + "description": "安全节点,当点击 document 的时候,如果包含该节点则不会关闭弹层,如果是函数需要返回 ref,如果是字符串则是该 DOM 的 id,也可以直接传入 DOM 节点,或者以上值组成的数组", + "docblock": "安全节点,当点击 document 的时候,如果包含该节点则不会关闭弹层,如果是函数需要返回 ref,如果是字符串则是该 DOM 的 id,也可以直接传入 DOM 节点,或者以上值组成的数组" + }, + "wrapperClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层的根节点的样式类", + "docblock": "弹层的根节点的样式类" + }, + "wrapperStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层的根节点的内联样式", + "docblock": "弹层的根节点的内联样式", + "properties": [] + }, + "animation": { + "type": { + "name": "union", + "value": [{ + "name": "object" + }, + { + "name": "bool" + } + ] + }, + "required": false, + "description": "配置动画的播放方式,支持 { in: 'enter-class', out: 'leave-class' } 的对象参数,如果设置为 false,则不播放动画", + "docblock": "配置动画的播放方式,支持 { in: 'enter-class', out: 'leave-class' } 的对象参数,如果设置为 false,则不播放动画\n@default { in: 'expandInDown', out: 'expandOutUp' }", + "defaultValue": { + "value": "{ in: 'expandInDown', out: 'expandOutUp' }", + "computed": false + } + }, + "trigger": { + "type": { + "name": "node" + }, + "required": false, + "description": "触发弹层显示或者隐藏的元素", + "docblock": "触发弹层显示或者隐藏的元素" + }, + "triggerType": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "触发弹层显示或隐藏的操作类型,可以是 'click','hover',或者它们组成的数组,如 ['hover', 'click']", + "defaultValue": { + "value": "'hover'", + "computed": false + }, + "docblock": "触发弹层显示或隐藏的操作类型,可以是 'click','hover',或者它们组成的数组,如 ['hover', 'click']" + }, + "triggerClickKeycode": { + "type": { + "name": "union", + "value": [{ + "name": "number" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "当 triggerType 为 click 时才生效,可自定义触发弹层显示的键盘码", + "defaultValue": { + "value": "[KEYCODE.SPACE, KEYCODE.ENTER]", + "computed": false + }, + "docblock": "当 triggerType 为 click 时才生效,可自定义触发弹层显示的键盘码" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层默认是否显示", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "弹层默认是否显示" + }, + "onVisibleChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层显示或隐藏时触发的回调函数", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层显示或隐藏时触发的回调函数\n@param {Boolean} visible 弹层是否显示\n@param {String} type 触发弹层显示或隐藏的来源 fromContent 表示由Dropdown内容触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "params": [{ + "name": "visible", + "description": "弹层是否显示", + "type": { + "name": "Boolean" + } + }, + { + "name": "type", + "description": "触发弹层显示或隐藏的来源 fromContent 表示由Dropdown内容触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "type": { + "name": "String" + } + } + ], + "returns": null + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "设置此属性,弹层无法显示或隐藏", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "设置此属性,弹层无法显示或隐藏" + }, + "delay": { + "type": { + "name": "number" + }, + "required": false, + "description": "弹层显示或隐藏的延时时间(以毫秒为单位),在 triggerType 被设置为 hover 时生效", + "defaultValue": { + "value": "200", + "computed": false + }, + "docblock": "弹层显示或隐藏的延时时间(以毫秒为单位),在 triggerType 被设置为 hover 时生效" + }, + "canCloseByTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "trigger 是否可以关闭弹层", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "trigger 是否可以关闭弹层" + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随trigger滚动", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否跟随trigger滚动" + } + }, + "methods": [], + "description": "继承 Popup 的 API,除非特别说明", + "subComponents": [] + }, + { + "name": "Form", + "title": "表单", + "typeId": 3, + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式前缀", + "defaultValue": { + "value": "'next-'", + "computed": false + }, + "docblock": "样式前缀" + }, + "inline": { + "type": { + "name": "bool" + }, + "required": false, + "description": "内联表单", + "docblock": "内联表单" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'large'", + "computed": false, + "description": "大" + }, + { + "value": "'medium'", + "computed": false, + "description": "中" + }, + { + "value": "'small'", + "computed": false, + "description": "小" + } + ] + }, + "required": false, + "description": "单个 Item 的 size 自定义,优先级高于 Form 的 size, 并且当组件与 Item 一起使用时,组件自身设置 size 属性无效。", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "单个 Item 的 size 自定义,优先级高于 Form 的 size, 并且当组件与 Item 一起使用时,组件自身设置 size 属性无效。\n@enumdesc 大, 中, 小", + "value": [{ + "value": "'large'", + "computed": false, + "description": "大" + }, + { + "value": "'medium'", + "computed": false, + "description": "中" + }, + { + "value": "'small'", + "computed": false, + "description": "小" + } + ] + }, + "labelAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'top'", + "computed": false, + "description": "上" + }, + { + "value": "'left'", + "computed": false, + "description": "左" + }, + { + "value": "'inset'", + "computed": false, + "description": "内" + } + ] + }, + "required": false, + "description": "标签的位置", + "defaultValue": { + "value": "'left'", + "computed": false + }, + "docblock": "标签的位置\n@enumdesc 上, 左, 内", + "value": [{ + "value": "'top'", + "computed": false, + "description": "上" + }, + { + "value": "'left'", + "computed": false, + "description": "左" + }, + { + "value": "'inset'", + "computed": false, + "description": "内" + } + ] + }, + "labelTextAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'left'", + "computed": false, + "description": "左" + }, + { + "value": "'right'", + "computed": false, + "description": "右" + } + ] + }, + "required": false, + "description": "标签的左右对齐方式", + "docblock": "标签的左右对齐方式\n@enumdesc 左, 右", + "value": [{ + "value": "'left'", + "computed": false, + "description": "左" + }, + { + "value": "'right'", + "computed": false, + "description": "右" + } + ] + }, + "field": { + "type": { + "name": "any" + }, + "required": false, + "description": "经 `new Field(this)` 初始化后,直接传给 Form 即可 用到表单校验则不可忽略此项", + "docblock": "经 `new Field(this)` 初始化后,直接传给 Form 即可 用到表单校验则不可忽略此项" + }, + "saveField": { + "type": { + "name": "func" + }, + "required": false, + "description": "保存 Form 自动生成的 field 对象", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "保存 Form 自动生成的 field 对象", + "params": [], + "returns": null + }, + "labelCol": { + "type": { + "name": "object" + }, + "required": false, + "description": "控制第一级 Item 的 labelCol", + "docblock": "控制第一级 Item 的 labelCol", + "properties": [] + }, + "wrapperCol": { + "type": { + "name": "object" + }, + "required": false, + "description": "控制第一级 Item 的 wrapperCol", + "docblock": "控制第一级 Item 的 wrapperCol", + "properties": [] + }, + "onSubmit": { + "type": { + "name": "func" + }, + "required": false, + "description": "form内有 `htmlType=\"submit\"` 的元素的时候会触发", + "defaultValue": { + "value": "function preventDefault(e) {\n e.preventDefault();\n}", + "computed": false + }, + "docblock": "form内有 `htmlType=\"submit\"` 的元素的时候会触发", + "params": [], + "returns": null + }, + "children": { + "type": { + "name": "any" + }, + "required": false, + "description": "子元素", + "docblock": "子元素" + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "扩展class", + "docblock": "扩展class" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内联样式", + "docblock": "自定义内联样式", + "properties": [] + }, + "value": { + "type": { + "name": "object" + }, + "required": false, + "description": "表单数值", + "docblock": "表单数值", + "properties": [] + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "表单变化回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "表单变化回调\n@param {Object} values 表单数据\n@param {Object} item 详细\n@param {String} item.name 变化的组件名\n@param {String} item.value 变化的数据\n@param {Object} item.field field 实例", + "params": [{ + "name": "values", + "description": "表单数据", + "type": { + "name": "Object" + } + }, + { + "name": "item", + "description": "详细", + "type": { + "name": "Object" + } + }, + { + "name": "item.name", + "description": "变化的组件名", + "type": { + "name": "String" + } + }, + { + "name": "item.value", + "description": "变化的数据", + "type": { + "name": "String" + } + }, + { + "name": "item.field", + "description": "field 实例", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "component": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "设置标签类型", + "defaultValue": { + "value": "'form'", + "computed": false + }, + "docblock": "设置标签类型" + } + }, + "methods": [], + "subComponents": [{ + "name": "Item", + "title": "表单项", + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式前缀", + "defaultValue": { + "value": "'next-'", + "computed": false + }, + "docblock": "样式前缀" + }, + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "label 标签的文本", + "docblock": "label 标签的文本" + }, + "labelCol": { + "type": { + "name": "object" + }, + "required": false, + "description": "label 标签布局,通 `` 组件,设置 span offset 值,如 {span: 8, offset: 16},该项仅在垂直表单有效", + "docblock": "label 标签布局,通 `` 组件,设置 span offset 值,如 {span: 8, offset: 16},该项仅在垂直表单有效", + "properties": [] + }, + "wrapperCol": { + "type": { + "name": "object" + }, + "required": false, + "description": "需要为输入控件设置布局样式时,使用该属性,用法同 labelCol", + "docblock": "需要为输入控件设置布局样式时,使用该属性,用法同 labelCol", + "properties": [] + }, + "help": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义提示信息,如不设置,则会根据校验规则自动生成.", + "docblock": "自定义提示信息,如不设置,则会根据校验规则自动生成." + }, + "extra": { + "type": { + "name": "node" + }, + "required": false, + "description": "额外的提示信息,和 help 类似,当需要错误信息和提示文案同时出现时,可以使用这个。 位于错误信息后面", + "docblock": "额外的提示信息,和 help 类似,当需要错误信息和提示文案同时出现时,可以使用这个。 位于错误信息后面" + }, + "validateState": { + "type": { + "name": "enum", + "value": [{ + "value": "'error'", + "computed": false, + "description": "失败" + }, + { + "value": "'success'", + "computed": false, + "description": "成功" + }, + { + "value": "'loading'", + "computed": false, + "description": "校验中" + } + ] + }, + "required": false, + "description": "校验状态,如不设置,则会根据校验规则自动生成", + "docblock": "校验状态,如不设置,则会根据校验规则自动生成\n@enumdesc 失败, 成功, 校验中", + "value": [{ + "value": "'error'", + "computed": false, + "description": "失败" + }, + { + "value": "'success'", + "computed": false, + "description": "成功" + }, + { + "value": "'loading'", + "computed": false, + "description": "校验中" + } + ] + }, + "hasFeedback": { + "type": { + "name": "bool" + }, + "required": false, + "description": "配合 validateState 属性使用,是否展示 success/loading 的校验状态图标, 目前只有Input支持", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "配合 validateState 属性使用,是否展示 success/loading 的校验状态图标, 目前只有Input支持" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内联样式", + "docblock": "自定义内联样式", + "properties": [] + }, + "children": { + "type": { + "name": "union", + "value": [{ + "name": "node" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "node 或者 function(values)", + "docblock": "node 或者 function(values)" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'large'", + "computed": false + }, + { + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + } + ] + }, + "required": false, + "description": "单个 Item 的 size 自定义,优先级高于 Form 的 size, 并且当组件与 Item 一起使用时,组件自身设置 size 属性无效。", + "docblock": "单个 Item 的 size 自定义,优先级高于 Form 的 size, 并且当组件与 Item 一起使用时,组件自身设置 size 属性无效。" + }, + "labelAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'top'", + "computed": false, + "description": "上" + }, + { + "value": "'left'", + "computed": false, + "description": "左" + }, + { + "value": "'inset'", + "computed": false, + "description": "内" + } + ] + }, + "required": false, + "description": "标签的位置", + "docblock": "标签的位置\n@enumdesc 上, 左, 内", + "value": [{ + "value": "'top'", + "computed": false, + "description": "上" + }, + { + "value": "'left'", + "computed": false, + "description": "左" + }, + { + "value": "'inset'", + "computed": false, + "description": "内" + } + ] + }, + "labelTextAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'left'", + "computed": false, + "description": "左" + }, + { + "value": "'right'", + "computed": false, + "description": "右" + } + ] + }, + "required": false, + "description": "标签的左右对齐方式", + "docblock": "标签的左右对齐方式\n@enumdesc 左, 右", + "value": [{ + "value": "'left'", + "computed": false, + "description": "左" + }, + { + "value": "'right'", + "computed": false, + "description": "右" + } + ] + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "扩展class", + "docblock": "扩展class" + }, + "required": { + "type": { + "name": "bool" + }, + "required": false, + "description": "[表单校验] 不能为空", + "docblock": "[表单校验] 不能为空" + }, + "asterisk": { + "type": { + "name": "bool" + }, + "required": false, + "description": "required 的星号是否显示", + "docblock": "required 的星号是否显示" + }, + "requiredMessage": { + "type": { + "name": "string" + }, + "required": false, + "description": "required 自定义错误信息", + "docblock": "required 自定义错误信息" + }, + "requiredTrigger": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "required 自定义触发方式", + "docblock": "required 自定义触发方式" + }, + "min": { + "type": { + "name": "number" + }, + "required": false, + "description": "[表单校验] 最小值", + "docblock": "[表单校验] 最小值" + }, + "max": { + "type": { + "name": "number" + }, + "required": false, + "description": "[表单校验] 最大值", + "docblock": "[表单校验] 最大值" + }, + "minmaxMessage": { + "type": { + "name": "string" + }, + "required": false, + "description": "min/max 自定义错误信息", + "docblock": "min/max 自定义错误信息" + }, + "minmaxTrigger": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "min/max 自定义触发方式", + "docblock": "min/max 自定义触发方式" + }, + "minLength": { + "type": { + "name": "number" + }, + "required": false, + "description": "[表单校验] 字符串最小长度 / 数组最小个数", + "docblock": "[表单校验] 字符串最小长度 / 数组最小个数" + }, + "maxLength": { + "type": { + "name": "number" + }, + "required": false, + "description": "[表单校验] 字符串最大长度 / 数组最大个数", + "docblock": "[表单校验] 字符串最大长度 / 数组最大个数" + }, + "minmaxLengthMessage": { + "type": { + "name": "string" + }, + "required": false, + "description": "minLength/maxLength 自定义错误信息", + "docblock": "minLength/maxLength 自定义错误信息" + }, + "minmaxLengthTrigger": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "minLength/maxLength 自定义触发方式", + "docblock": "minLength/maxLength 自定义触发方式" + }, + "length": { + "type": { + "name": "number" + }, + "required": false, + "description": "[表单校验] 字符串精确长度 / 数组精确个数", + "docblock": "[表单校验] 字符串精确长度 / 数组精确个数" + }, + "lengthMessage": { + "type": { + "name": "string" + }, + "required": false, + "description": "length 自定义错误信息", + "docblock": "length 自定义错误信息" + }, + "lengthTrigger": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "length 自定义触发方式", + "docblock": "length 自定义触发方式" + }, + "pattern": { + "type": { + "name": "any" + }, + "required": false, + "description": "正则校验", + "docblock": "正则校验" + }, + "patternMessage": { + "type": { + "name": "string" + }, + "required": false, + "description": "pattern 自定义错误信息", + "docblock": "pattern 自定义错误信息" + }, + "patternTrigger": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "pattern 自定义触发方式", + "docblock": "pattern 自定义触发方式" + }, + "format": { + "type": { + "name": "enum", + "value": [{ + "value": "'number'", + "computed": false + }, + { + "value": "'email'", + "computed": false + }, + { + "value": "'url'", + "computed": false + }, + { + "value": "'tel'", + "computed": false + } + ] + }, + "required": false, + "description": "[表单校验] 四种常用的 pattern", + "docblock": "[表单校验] 四种常用的 pattern" + }, + "formatMessage": { + "type": { + "name": "string" + }, + "required": false, + "description": "format 自定义错误信息", + "docblock": "format 自定义错误信息" + }, + "formatTrigger": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "format 自定义触发方式", + "docblock": "format 自定义触发方式" + }, + "validator": { + "type": { + "name": "func" + }, + "required": false, + "description": "[表单校验] 自定义校验函数", + "docblock": "[表单校验] 自定义校验函数", + "params": [], + "returns": null + }, + "validatorTrigger": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "validator 自定义触发方式", + "docblock": "validator 自定义触发方式" + }, + "autoValidate": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否修改数据时自动触发校验", + "docblock": "是否修改数据时自动触发校验" + } + }, + "methods": [{ + "name": "getNames", + "docblock": "从子元素里面提取表单组件", + "modifiers": [], + "params": [], + "returns": null, + "description": "从子元素里面提取表单组件" + }], + "description": "手动传递了 wrapCol labelCol 会使用 Grid 辅助布局; labelAlign='top' 会强制禁用 Grid", + "order": 1 + }, + { + "name": "Submit", + "title": "表单提交", + "props": { + "type": { + "type": { + "name": "enum", + "value": [{ + "value": "'primary'", + "computed": false + }, + { + "value": "'secondary'", + "computed": false + }, + { + "value": "'normal'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮的类型", + "defaultValue": { + "value": "'normal'", + "computed": false + }, + "docblock": "按钮的类型" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮的尺寸", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "按钮的尺寸" + }, + "iconSize": { + "type": { + "name": "enum", + "value": [{ + "value": "'xxs'", + "computed": false + }, + { + "value": "'xs'", + "computed": false + }, + { + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + }, + { + "value": "'xl'", + "computed": false + }, + { + "value": "'xxl'", + "computed": false + }, + { + "value": "'xxxl'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮中 Icon 的尺寸,用于替代 Icon 的默认大小", + "docblock": "按钮中 Icon 的尺寸,用于替代 Icon 的默认大小" + }, + "htmlType": { + "type": { + "name": "enum", + "value": [{ + "value": "'submit'", + "computed": false + }, + { + "value": "'reset'", + "computed": false + }, + { + "value": "'button'", + "computed": false + } + ] + }, + "required": false, + "description": "当 component = 'button' 时,设置 button 标签的 type 值", + "defaultValue": { + "value": "'button'", + "computed": false + }, + "docblock": "当 component = 'button' 时,设置 button 标签的 type 值" + }, + "component": { + "type": { + "name": "enum", + "value": [{ + "value": "'button'", + "computed": false + }, + { + "value": "'a'", + "computed": false + }, + { + "value": "'div'", + "computed": false + }, + { + "value": "'span'", + "computed": false + } + ] + }, + "required": false, + "description": "设置标签类型", + "defaultValue": { + "value": "'button'", + "computed": false + }, + "docblock": "设置标签类型" + }, + "loading": { + "type": { + "name": "bool" + }, + "required": false, + "description": "设置按钮的载入状态", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "设置按钮的载入状态" + }, + "ghost": { + "type": { + "name": "enum", + "value": [{ + "value": "true", + "computed": false + }, + { + "value": "false", + "computed": false + }, + { + "value": "'light'", + "computed": false + }, + { + "value": "'dark'", + "computed": false + } + ] + }, + "required": false, + "description": "是否为幽灵按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为幽灵按钮" + }, + "text": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否为文本按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为文本按钮" + }, + "warning": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否为警告按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为警告按钮" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否禁用" + }, + "onClick": { + "type": { + "name": "func" + }, + "required": false, + "description": "点击提交后触发", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "点击提交后触发\n@param {Object} value 数据\n@param {Object} errors 错误数据\n@param {class} field 实例", + "params": [{ + "name": "value", + "description": "数据", + "type": { + "name": "Object" + } + }, + { + "name": "errors", + "description": "错误数据", + "type": { + "name": "Object" + } + }, + { + "name": "field", + "description": "实例", + "type": { + "name": "class" + } + } + ], + "returns": null + }, + "validate": { + "type": { + "name": "union", + "value": [{ + "name": "bool" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "是否校验/需要校验的 name 数组", + "docblock": "是否校验/需要校验的 name 数组" + }, + "field": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义 field (在 Form 内不需要设置)", + "docblock": "自定义 field (在 Form 内不需要设置)", + "properties": [] + } + }, + "methods": [], + "description": "继承 Button API", + "order": 2 + }, + { + "name": "Reset", + "title": "表单重置", + "props": { + "type": { + "type": { + "name": "enum", + "value": [{ + "value": "'primary'", + "computed": false + }, + { + "value": "'secondary'", + "computed": false + }, + { + "value": "'normal'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮的类型", + "defaultValue": { + "value": "'normal'", + "computed": false + }, + "docblock": "按钮的类型" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮的尺寸", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "按钮的尺寸" + }, + "iconSize": { + "type": { + "name": "enum", + "value": [{ + "value": "'xxs'", + "computed": false + }, + { + "value": "'xs'", + "computed": false + }, + { + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + }, + { + "value": "'xl'", + "computed": false + }, + { + "value": "'xxl'", + "computed": false + }, + { + "value": "'xxxl'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮中 Icon 的尺寸,用于替代 Icon 的默认大小", + "docblock": "按钮中 Icon 的尺寸,用于替代 Icon 的默认大小" + }, + "htmlType": { + "type": { + "name": "enum", + "value": [{ + "value": "'submit'", + "computed": false + }, + { + "value": "'reset'", + "computed": false + }, + { + "value": "'button'", + "computed": false + } + ] + }, + "required": false, + "description": "当 component = 'button' 时,设置 button 标签的 type 值", + "defaultValue": { + "value": "'button'", + "computed": false + }, + "docblock": "当 component = 'button' 时,设置 button 标签的 type 值" + }, + "component": { + "type": { + "name": "enum", + "value": [{ + "value": "'button'", + "computed": false + }, + { + "value": "'a'", + "computed": false + }, + { + "value": "'div'", + "computed": false + }, + { + "value": "'span'", + "computed": false + } + ] + }, + "required": false, + "description": "设置标签类型", + "defaultValue": { + "value": "'button'", + "computed": false + }, + "docblock": "设置标签类型" + }, + "loading": { + "type": { + "name": "bool" + }, + "required": false, + "description": "设置按钮的载入状态", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "设置按钮的载入状态" + }, + "ghost": { + "type": { + "name": "enum", + "value": [{ + "value": "true", + "computed": false + }, + { + "value": "false", + "computed": false + }, + { + "value": "'light'", + "computed": false + }, + { + "value": "'dark'", + "computed": false + } + ] + }, + "required": false, + "description": "是否为幽灵按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为幽灵按钮" + }, + "text": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否为文本按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为文本按钮" + }, + "warning": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否为警告按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为警告按钮" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否禁用" + }, + "onClick": { + "type": { + "name": "func" + }, + "required": false, + "description": "点击提交后触发", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "点击提交后触发", + "params": [], + "returns": null + }, + "names": { + "type": { + "name": "array" + }, + "required": false, + "description": "自定义重置的字段", + "docblock": "自定义重置的字段" + }, + "toDefault": { + "type": { + "name": "bool" + }, + "required": false, + "description": "返回默认值", + "docblock": "返回默认值" + }, + "field": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义 field (在 Form 内不需要设置)", + "docblock": "自定义 field (在 Form 内不需要设置)", + "properties": [] + } + }, + "methods": [], + "description": "继承 Button API", + "order": 3 + }, + { + "name": "Error", + "title": "表单错误", + "props": { + "name": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "表单名", + "docblock": "表单名" + }, + "field": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义 field (在 Form 内不需要设置)", + "docblock": "自定义 field (在 Form 内不需要设置)", + "properties": [] + }, + "children": { + "type": { + "name": "union", + "value": [{ + "name": "node" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "自定义错误渲染, 可以是 node 或者 function(errors, state)", + "docblock": "自定义错误渲染, 可以是 node 或者 function(errors, state)" + } + }, + "methods": [], + "description": "自定义错误展示", + "order": 4 + } + ] + }, + { + "name": "Grid", + "title": "栅格", + "typeId": 1, + "subComponents": [{ + "name": "Row", + "title": "行", + "props": { + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "行内容", + "docblock": "行内容" + }, + "gutter": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "列间隔", + "defaultValue": { + "value": "0", + "computed": false + }, + "docblock": "列间隔" + }, + "wrap": { + "type": { + "name": "bool" + }, + "required": false, + "description": "列在行中宽度溢出后是否换行", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "列在行中宽度溢出后是否换行" + }, + "fixed": { + "type": { + "name": "bool" + }, + "required": false, + "description": "行在某一断点下宽度是否保持不变(默认行宽度随视口变化而变化)", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "行在某一断点下宽度是否保持不变(默认行宽度随视口变化而变化)" + }, + "fixedWidth": { + "type": { + "name": "enum", + "value": [{ + "value": "'xxs'", + "computed": false, + "description": "320px" + }, + { + "value": "'xs'", + "computed": false, + "description": "480px" + }, + { + "value": "'s'", + "computed": false, + "description": "720px" + }, + { + "value": "'m'", + "computed": false, + "description": "990px" + }, + { + "value": "'l'", + "computed": false, + "description": "1200px" + }, + { + "value": "'xl'", + "computed": false, + "description": "1500px" + } + ] + }, + "required": false, + "description": "固定行的宽度为某一断点的宽度,不受视口影响而变动", + "docblock": "固定行的宽度为某一断点的宽度,不受视口影响而变动\n@enumdesc 320px, 480px, 720px, 990px, 1200px, 1500px", + "value": [{ + "value": "'xxs'", + "computed": false, + "description": "320px" + }, + { + "value": "'xs'", + "computed": false, + "description": "480px" + }, + { + "value": "'s'", + "computed": false, + "description": "720px" + }, + { + "value": "'m'", + "computed": false, + "description": "990px" + }, + { + "value": "'l'", + "computed": false, + "description": "1200px" + }, + { + "value": "'xl'", + "computed": false, + "description": "1500px" + } + ] + }, + "align": { + "type": { + "name": "enum", + "value": [{ + "value": "'top'", + "computed": false, + "description": "顶部对齐" + }, + { + "value": "'center'", + "computed": false, + "description": "居中对齐" + }, + { + "value": "'bottom'", + "computed": false, + "description": "底部对齐" + }, + { + "value": "'baseline'", + "computed": false, + "description": "按第一行文字基线对齐" + }, + { + "value": "'stretch'", + "computed": false, + "description": "未设置高度或设为 auto,将占满整个容器的高度" + } + ] + }, + "required": false, + "description": "(不支持IE9浏览器)多列垂直方向对齐方式", + "docblock": "(不支持IE9浏览器)多列垂直方向对齐方式\n@enumdesc 顶部对齐, 居中对齐, 底部对齐, 按第一行文字基线对齐, 未设置高度或设为 auto,将占满整个容器的高度", + "value": [{ + "value": "'top'", + "computed": false, + "description": "顶部对齐" + }, + { + "value": "'center'", + "computed": false, + "description": "居中对齐" + }, + { + "value": "'bottom'", + "computed": false, + "description": "底部对齐" + }, + { + "value": "'baseline'", + "computed": false, + "description": "按第一行文字基线对齐" + }, + { + "value": "'stretch'", + "computed": false, + "description": "未设置高度或设为 auto,将占满整个容器的高度" + } + ] + }, + "justify": { + "type": { + "name": "enum", + "value": [{ + "value": "'start'", + "computed": false, + "description": "左对齐" + }, + { + "value": "'center'", + "computed": false, + "description": "居中对齐" + }, + { + "value": "'end'", + "computed": false, + "description": "右对齐" + }, + { + "value": "'space-between'", + "computed": false, + "description": "两端对齐,列之间间距相等" + }, + { + "value": "'space-around'", + "computed": false, + "description": "每列具有相同的左右间距,行两端间距是列间距的二分之一" + } + ] + }, + "required": false, + "description": "(不支持IE9浏览器)行内具有多余空间时的布局方式", + "docblock": "(不支持IE9浏览器)行内具有多余空间时的布局方式\n@enumdesc 左对齐, 居中对齐, 右对齐, 两端对齐,列之间间距相等, 每列具有相同的左右间距,行两端间距是列间距的二分之一", + "value": [{ + "value": "'start'", + "computed": false, + "description": "左对齐" + }, + { + "value": "'center'", + "computed": false, + "description": "居中对齐" + }, + { + "value": "'end'", + "computed": false, + "description": "右对齐" + }, + { + "value": "'space-between'", + "computed": false, + "description": "两端对齐,列之间间距相等" + }, + { + "value": "'space-around'", + "computed": false, + "description": "每列具有相同的左右间距,行两端间距是列间距的二分之一" + } + ] + }, + "hidden": { + "type": { + "name": "union", + "value": [{ + "name": "bool" + }, + { + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "行在不同断点下的显示与隐藏

    **可选值**:
    true(在所有断点下隐藏)
    false(在所有断点下显示)
    'xs'(在 xs 断点下隐藏)
    ['xxs', 'xs', 's', 'm', 'l', 'xl'](在 xxs, xs, s, m, l, xl 断点下隐藏)", + "docblock": "行在不同断点下的显示与隐藏

    **可选值**:
    true(在所有断点下隐藏)
    false(在所有断点下显示)
    'xs'(在 xs 断点下隐藏)
    ['xxs', 'xs', 's', 'm', 'l', 'xl'](在 xxs, xs, s, m, l, xl 断点下隐藏)" + }, + "component": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "指定以何种元素渲染该节点\n- 默认为 'div'", + "defaultValue": { + "value": "'div'", + "computed": false + }, + "docblock": "指定以何种元素渲染该节点\n- 默认为 'div'" + } + }, + "methods": [], + "order": 1 + }, + { + "name": "Col", + "title": "列", + "props": { + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "列内容", + "docblock": "列内容" + }, + "span": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "列宽度

    **可选值**:
    1, 2, 3, ..., 22, 23, 24", + "docblock": "列宽度

    **可选值**:
    1, 2, 3, ..., 22, 23, 24" + }, + "fixedSpan": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "固定列宽度,宽度值为20 * 栅格数

    **可选值**:
    1, 2, 3, ..., 28, 29, 30", + "docblock": "固定列宽度,宽度值为20 * 栅格数

    **可选值**:
    1, 2, 3, ..., 28, 29, 30" + }, + "offset": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "(不支持IE9浏览器)列偏移

    **可选值**:
    1, 2, 3, ..., 22, 23, 24", + "docblock": "(不支持IE9浏览器)列偏移

    **可选值**:
    1, 2, 3, ..., 22, 23, 24" + }, + "fixedOffset": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "(不支持IE9浏览器)固定列偏移,宽度值为20 * 栅格数

    **可选值**:
    1, 2, 3, ..., 28, 29, 30", + "docblock": "(不支持IE9浏览器)固定列偏移,宽度值为20 * 栅格数

    **可选值**:
    1, 2, 3, ..., 28, 29, 30" + }, + "align": { + "type": { + "name": "enum", + "value": [{ + "value": "'top'", + "computed": false + }, + { + "value": "'center'", + "computed": false + }, + { + "value": "'bottom'", + "computed": false + }, + { + "value": "'baseline'", + "computed": false + }, + { + "value": "'stretch'", + "computed": false + } + ] + }, + "required": false, + "description": "(不支持IE9浏览器)多列垂直方向对齐方式,可覆盖Row的align属性", + "docblock": "(不支持IE9浏览器)多列垂直方向对齐方式,可覆盖Row的align属性" + }, + "hidden": { + "type": { + "name": "union", + "value": [{ + "name": "bool" + }, + { + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "列在不同断点下的显示与隐藏

    **可选值**:
    true(在所有断点下隐藏)
    false(在所有断点下显示)
    'xs'(在 xs 断点下隐藏)
    ['xxs', 'xs', 's', 'm', 'l', 'xl'](在 xxs, xs, s, m, l, xl 断点下隐藏)", + "docblock": "列在不同断点下的显示与隐藏

    **可选值**:
    true(在所有断点下隐藏)
    false(在所有断点下显示)
    'xs'(在 xs 断点下隐藏)
    ['xxs', 'xs', 's', 'm', 'l', 'xl'](在 xxs, xs, s, m, l, xl 断点下隐藏)" + }, + "xxs": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": ">=320px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象", + "docblock": ">=320px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象" + }, + "xs": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": ">=480px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象", + "docblock": ">=480px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象" + }, + "s": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": ">=720px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象", + "docblock": ">=720px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象" + }, + "m": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": ">=990px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象", + "docblock": ">=990px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象" + }, + "l": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": ">=1200px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象", + "docblock": ">=1200px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象" + }, + "xl": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": ">=1500px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象", + "docblock": ">=1500px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象" + }, + "component": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "指定以何种元素渲染该节点,默认为 'div'", + "defaultValue": { + "value": "'div'", + "computed": false + }, + "docblock": "指定以何种元素渲染该节点,默认为 'div'" + } + }, + "methods": [], + "order": 2 + } + ], + "methods": [] + }, + { + "name": "Icon", + "title": "图标", + "typeId": 1, + "props": { + "type": { + "type": { + "name": "string" + }, + "required": false, + "description": "指定显示哪种图标", + "docblock": "指定显示哪种图标" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'xxs'", + "computed": false + }, + { + "value": "'xs'", + "computed": false + }, + { + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + }, + { + "value": "'xl'", + "computed": false + }, + { + "value": "'xxl'", + "computed": false + }, + { + "value": "'xxxl'", + "computed": false + }, + { + "value": "'inherit'", + "computed": false + } + ] + }, + "required": false, + "description": "指定图标大小", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "指定图标大小" + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "Input", + "title": "输入框", + "typeId": 3, + "props": { + "value": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "当前值", + "docblock": "当前值" + }, + "defaultValue": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "初始化值", + "docblock": "初始化值" + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "发生改变的时候触发的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "发生改变的时候触发的回调\n@param {String} value 数据\n@param {Event} e DOM事件对象", + "params": [{ + "name": "value", + "description": "数据", + "type": { + "name": "String" + } + }, + { + "name": "e", + "description": "DOM事件对象", + "type": { + "name": "Event" + } + } + ], + "returns": null + }, + "onKeyDown": { + "type": { + "name": "func" + }, + "required": false, + "description": "键盘按下的时候触发的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "键盘按下的时候触发的回调\n@param {Event} e DOM事件对象\n@param {Object} opts 可扩展的附加信息:
    - opts.overMaxLength: {Boolean} 已超出最大长度
    - opts.beTrimed: {Boolean} 输入的空格被清理", + "params": [{ + "name": "e", + "description": "DOM事件对象", + "type": { + "name": "Event" + } + }, + { + "name": "opts", + "description": "可扩展的附加信息:
    - opts.overMaxLength: {Boolean} 已超出最大长度
    - opts.beTrimed: {Boolean} 输入的空格被清理", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "禁用状态", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "禁用状态" + }, + "maxLength": { + "type": { + "name": "number" + }, + "required": false, + "description": "最大长度", + "defaultValue": { + "value": "null", + "computed": false + }, + "docblock": "最大长度" + }, + "hasLimitHint": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否展现最大长度样式", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否展现最大长度样式" + }, + "cutString": { + "type": { + "name": "bool" + }, + "required": false, + "description": "当设置了maxLength时,是否截断超出字符串", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "当设置了maxLength时,是否截断超出字符串" + }, + "readOnly": { + "type": { + "name": "bool" + }, + "required": false, + "description": "只读", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "只读" + }, + "trim": { + "type": { + "name": "bool" + }, + "required": false, + "description": "onChange返回会自动去除头尾空字符", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "onChange返回会自动去除头尾空字符" + }, + "placeholder": { + "type": { + "name": "string" + }, + "required": false, + "description": "输入提示", + "docblock": "输入提示" + }, + "onFocus": { + "type": { + "name": "func" + }, + "required": false, + "description": "获取焦点时候触发的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "获取焦点时候触发的回调\n@param {Event} e DOM事件对象", + "params": [{ + "name": "e", + "description": "DOM事件对象", + "type": { + "name": "Event" + } + }], + "returns": null + }, + "onBlur": { + "type": { + "name": "func" + }, + "required": false, + "description": "失去焦点时候触发的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "失去焦点时候触发的回调\n@param {Event} e DOM事件对象", + "params": [{ + "name": "e", + "description": "DOM事件对象", + "type": { + "name": "Event" + } + }], + "returns": null + }, + "getValueLength": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义字符串计算长度方式", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "自定义字符串计算长度方式\n@param {String} value 数据\n@returns {Number} 自定义长度", + "params": [{ + "name": "value", + "description": "数据", + "type": { + "name": "String" + } + }], + "returns": { + "description": "自定义长度", + "type": { + "name": "Number" + } + } + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "自定义class", + "docblock": "自定义class" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内联样式", + "docblock": "自定义内联样式", + "properties": [] + }, + "htmlType": { + "type": { + "name": "string" + }, + "required": false, + "description": "原生type", + "docblock": "原生type" + }, + "name": { + "type": { + "name": "string" + }, + "required": false, + "description": "name", + "docblock": "name" + }, + "state": { + "type": { + "name": "enum", + "value": [{ + "value": "'error'", + "computed": false, + "description": "错误" + }, + { + "value": "'loading'", + "computed": false, + "description": "校验中" + }, + { + "value": "'success'", + "computed": false, + "description": "成功" + } + ] + }, + "required": false, + "description": "状态", + "docblock": "状态\n@enumdesc 错误, 校验中, 成功", + "value": [{ + "value": "'error'", + "computed": false, + "description": "错误" + }, + { + "value": "'loading'", + "computed": false, + "description": "校验中" + }, + { + "value": "'success'", + "computed": false, + "description": "成功" + } + ] + }, + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "label", + "docblock": "label" + }, + "hasClear": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否出现clear按钮", + "docblock": "是否出现clear按钮" + }, + "hasBorder": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否有边框", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否有边框" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false, + "description": "小" + }, + { + "value": "'medium'", + "computed": false, + "description": "中" + }, + { + "value": "'large'", + "computed": false, + "description": "大" + } + ] + }, + "required": false, + "description": "尺寸", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "尺寸\n@enumdesc 小, 中, 大", + "value": [{ + "value": "'small'", + "computed": false, + "description": "小" + }, + { + "value": "'medium'", + "computed": false, + "description": "中" + }, + { + "value": "'large'", + "computed": false, + "description": "大" + } + ] + }, + "onPressEnter": { + "type": { + "name": "func" + }, + "required": false, + "description": "按下回车的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "按下回车的回调", + "params": [], + "returns": null + }, + "hint": { + "type": { + "name": "string" + }, + "required": false, + "description": "水印 (Icon的type类型,和hasClear占用一个地方)", + "docblock": "水印 (Icon的type类型,和hasClear占用一个地方)" + }, + "innerBefore": { + "type": { + "name": "node" + }, + "required": false, + "description": "文字前附加内容", + "docblock": "文字前附加内容" + }, + "innerAfter": { + "type": { + "name": "node" + }, + "required": false, + "description": "文字后附加内容", + "docblock": "文字后附加内容" + }, + "addonBefore": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框前附加内容", + "docblock": "输入框前附加内容" + }, + "addonAfter": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框后附加内容", + "docblock": "输入框后附加内容" + }, + "addonTextBefore": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框前附加文字", + "docblock": "输入框前附加文字" + }, + "addonTextAfter": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框后附加文字", + "docblock": "输入框后附加文字" + }, + "autoComplete": { + "type": { + "name": "string" + }, + "required": false, + "description": "(原生input支持)", + "defaultValue": { + "value": "'off'", + "computed": false + }, + "docblock": "(原生input支持)" + }, + "autoFocus": { + "type": { + "name": "bool" + }, + "required": false, + "description": "自动聚焦(原生input支持)", + "docblock": "自动聚焦(原生input支持)" + } + }, + "methods": [], + "subComponents": [{ + "name": "TextArea", + "title": "文本域", + "props": { + "value": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "当前值", + "docblock": "当前值" + }, + "defaultValue": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "初始化值", + "docblock": "初始化值" + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "发生改变的时候触发的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "发生改变的时候触发的回调\n@param {String} value 数据\n@param {Event} e DOM事件对象", + "params": [{ + "name": "value", + "description": "数据", + "type": { + "name": "String" + } + }, + { + "name": "e", + "description": "DOM事件对象", + "type": { + "name": "Event" + } + } + ], + "returns": null + }, + "onKeyDown": { + "type": { + "name": "func" + }, + "required": false, + "description": "键盘按下的时候触发的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "键盘按下的时候触发的回调\n@param {Event} e DOM事件对象\n@param {Object} opts 可扩展的附加信息:
    - opts.overMaxLength: {Boolean} 已超出最大长度
    - opts.beTrimed: {Boolean} 输入的空格被清理", + "params": [{ + "name": "e", + "description": "DOM事件对象", + "type": { + "name": "Event" + } + }, + { + "name": "opts", + "description": "可扩展的附加信息:
    - opts.overMaxLength: {Boolean} 已超出最大长度
    - opts.beTrimed: {Boolean} 输入的空格被清理", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "禁用状态", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "禁用状态" + }, + "maxLength": { + "type": { + "name": "number" + }, + "required": false, + "description": "最大长度", + "defaultValue": { + "value": "null", + "computed": false + }, + "docblock": "最大长度" + }, + "hasLimitHint": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否展现最大长度样式", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否展现最大长度样式" + }, + "cutString": { + "type": { + "name": "bool" + }, + "required": false, + "description": "当设置了maxLength时,是否截断超出字符串", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "当设置了maxLength时,是否截断超出字符串" + }, + "readOnly": { + "type": { + "name": "bool" + }, + "required": false, + "description": "只读", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "只读" + }, + "trim": { + "type": { + "name": "bool" + }, + "required": false, + "description": "onChange返回会自动去除头尾空字符", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "onChange返回会自动去除头尾空字符" + }, + "placeholder": { + "type": { + "name": "string" + }, + "required": false, + "description": "输入提示", + "docblock": "输入提示" + }, + "onFocus": { + "type": { + "name": "func" + }, + "required": false, + "description": "获取焦点时候触发的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "获取焦点时候触发的回调\n@param {Event} e DOM事件对象", + "params": [{ + "name": "e", + "description": "DOM事件对象", + "type": { + "name": "Event" + } + }], + "returns": null + }, + "onBlur": { + "type": { + "name": "func" + }, + "required": false, + "description": "失去焦点时候触发的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "失去焦点时候触发的回调\n@param {Event} e DOM事件对象", + "params": [{ + "name": "e", + "description": "DOM事件对象", + "type": { + "name": "Event" + } + }], + "returns": null + }, + "getValueLength": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义字符串计算长度方式", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "自定义字符串计算长度方式\n@param {String} value 数据\n@returns {Number} 自定义长度", + "params": [{ + "name": "value", + "description": "数据", + "type": { + "name": "String" + } + }], + "returns": { + "description": "自定义长度", + "type": { + "name": "Number" + } + } + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "自定义class", + "docblock": "自定义class" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内联样式", + "docblock": "自定义内联样式", + "properties": [] + }, + "htmlType": { + "type": { + "name": "string" + }, + "required": false, + "description": "原生type", + "docblock": "原生type" + }, + "name": { + "type": { + "name": "string" + }, + "required": false, + "description": "name", + "docblock": "name" + }, + "state": { + "type": { + "name": "enum", + "value": [{ + "value": "'error'", + "computed": false, + "description": "错误" + }] + }, + "required": false, + "description": "状态", + "docblock": "状态\n@enumdesc 错误", + "value": [{ + "value": "'error'", + "computed": false, + "description": "错误" + }] + }, + "hasBorder": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否有边框", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否有边框" + }, + "autoHeight": { + "type": { + "name": "union", + "value": [{ + "name": "bool" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": "自动高度 true / {minRows: 2, maxRows: 4}", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "自动高度 true / {minRows: 2, maxRows: 4}" + }, + "rows": { + "type": { + "name": "number" + }, + "required": false, + "description": "多行文本框高度
    (不要直接用height设置多行文本框的高度, ie9 10会有兼容性问题)", + "defaultValue": { + "value": "4", + "computed": false + }, + "docblock": "多行文本框高度
    (不要直接用height设置多行文本框的高度, ie9 10会有兼容性问题)" + } + }, + "methods": [{ + "name": "getValueLength", + "docblock": "value.length !== maxLength in ie/safari(mac) while value has `Enter`\nabout maxLength compute: `Enter` was considered to be one char(\\n) in chrome , but two chars(\\r\\n) in ie/safari(mac).\nso while value has `Enter`, we should let display length + 1", + "modifiers": [], + "params": [{ + "name": "value" + }], + "returns": null, + "description": "value.length !== maxLength in ie/safari(mac) while value has `Enter`\nabout maxLength compute: `Enter` was considered to be one char(\\n) in chrome , but two chars(\\r\\n) in ie/safari(mac).\nso while value has `Enter`, we should let display length + 1" + }], + "order": 2 + }, + { + "name": "Group", + "title": "输入框组", + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式前缀", + "defaultValue": { + "value": "'next-'", + "computed": false + }, + "docblock": "样式前缀" + }, + "addonBefore": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框前附加内容", + "docblock": "输入框前附加内容" + }, + "addonBeforeClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "输入框前附加内容css", + "docblock": "输入框前附加内容css" + }, + "addonAfter": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框后附加内容", + "docblock": "输入框后附加内容" + }, + "addonAfterClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "输入框后额外css", + "docblock": "输入框后额外css" + }, + "rtl": { + "type": { + "name": "bool" + }, + "required": false, + "description": "rtl", + "docblock": "rtl" + } + }, + "methods": [] + } + ] + }, + { + "name": "Loading", + "title": "加载", + "typeId": 5, + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式前缀", + "defaultValue": { + "value": "'next-'", + "computed": false + }, + "docblock": "样式前缀" + }, + "tip": { + "type": { + "name": "any" + }, + "required": false, + "description": "自定义内容", + "docblock": "自定义内容" + }, + "tipAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'right'", + "computed": false, + "description": "出现在动画右边" + }, + { + "value": "'bottom'", + "computed": false, + "description": "出现在动画下面" + } + ] + }, + "required": false, + "description": "自定义内容位置", + "defaultValue": { + "value": "'bottom'", + "computed": false + }, + "docblock": "自定义内容位置\n@enumdesc 出现在动画右边, 出现在动画下面", + "value": [{ + "value": "'right'", + "computed": false, + "description": "出现在动画右边" + }, + { + "value": "'bottom'", + "computed": false, + "description": "出现在动画下面" + } + ] + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "loading 状态, 默认 true", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "loading 状态, 默认 true" + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "自定义class", + "docblock": "自定义class" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内联样式", + "docblock": "自定义内联样式", + "properties": [] + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'large'", + "computed": false, + "description": "大号" + }, + { + "value": "'medium'", + "computed": false, + "description": "中号" + } + ] + }, + "required": false, + "description": "设置动画尺寸", + "defaultValue": { + "value": "'large'", + "computed": false + }, + "docblock": "设置动画尺寸\n@description 仅仅对默认动画效果起作用\n@enumdesc 大号, 中号", + "value": [{ + "value": "'large'", + "computed": false, + "description": "大号" + }, + { + "value": "'medium'", + "computed": false, + "description": "中号" + } + ] + }, + "indicator": { + "type": { + "name": "any" + }, + "required": false, + "description": "自定义动画", + "docblock": "自定义动画" + }, + "color": { + "type": { + "name": "string" + }, + "required": false, + "description": "动画颜色", + "docblock": "动画颜色" + }, + "fullScreen": { + "type": { + "name": "bool" + }, + "required": false, + "description": "全屏展示", + "docblock": "全屏展示" + }, + "children": { + "type": { + "name": "any" + }, + "required": false, + "description": "子元素", + "docblock": "子元素" + }, + "inline": { + "type": { + "name": "bool" + }, + "required": false, + "description": "should loader be displayed inline", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "should loader be displayed inline" + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "Menu", + "title": "菜单", + "typeId": 4, + "props": { + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "菜单项和子菜单", + "docblock": "菜单项和子菜单" + }, + "onItemClick": { + "type": { + "name": "func" + }, + "required": false, + "description": "点击菜单项触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "点击菜单项触发的回调函数\n@param {String} key 点击的菜单项的 key 值\n@param {Object} item 点击的菜单项对象\n@param {Object} event 点击的事件对象", + "params": [{ + "name": "key", + "description": "点击的菜单项的 key 值", + "type": { + "name": "String" + } + }, + { + "name": "item", + "description": "点击的菜单项对象", + "type": { + "name": "Object" + } + }, + { + "name": "event", + "description": "点击的事件对象", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "openKeys": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "当前打开的子菜单的 key 值", + "docblock": "当前打开的子菜单的 key 值" + }, + "defaultOpenKeys": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "初始打开的子菜单的 key 值", + "defaultValue": { + "value": "[]", + "computed": false + }, + "docblock": "初始打开的子菜单的 key 值" + }, + "defaultOpenAll": { + "type": { + "name": "bool" + }, + "required": false, + "description": "初始展开所有的子菜单,只在 mode 设置为 'inline' 以及 openMode 设置为 'multiple' 下生效,优先级高于 defaultOpenKeys", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "初始展开所有的子菜单,只在 mode 设置为 'inline' 以及 openMode 设置为 'multiple' 下生效,优先级高于 defaultOpenKeys" + }, + "onOpen": { + "type": { + "name": "func" + }, + "required": false, + "description": "打开或关闭子菜单触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "打开或关闭子菜单触发的回调函数\n@param {String} key 打开的所有子菜单的 key 值\n@param {Object} extra 额外参数\n@param {String} extra.key 当前操作子菜单的 key 值\n@param {Boolean} extra.open 是否是打开", + "params": [{ + "name": "key", + "description": "打开的所有子菜单的 key 值", + "type": { + "name": "String" + } + }, + { + "name": "extra", + "description": "额外参数", + "type": { + "name": "Object" + } + }, + { + "name": "extra.key", + "description": "当前操作子菜单的 key 值", + "type": { + "name": "String" + } + }, + { + "name": "extra.open", + "description": "是否是打开", + "type": { + "name": "Boolean" + } + } + ], + "returns": null + }, + "mode": { + "type": { + "name": "enum", + "value": [{ + "value": "'inline'", + "computed": false + }, + { + "value": "'popup'", + "computed": false + } + ] + }, + "required": false, + "description": "子菜单打开的模式", + "defaultValue": { + "value": "'inline'", + "computed": false + }, + "docblock": "子菜单打开的模式" + }, + "triggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "子菜单打开的触发行为", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "子菜单打开的触发行为" + }, + "openMode": { + "type": { + "name": "enum", + "value": [{ + "value": "'single'", + "computed": false + }, + { + "value": "'multiple'", + "computed": false + } + ] + }, + "required": false, + "description": "展开内连子菜单的模式,同时可以展开一个子菜单还是多个子菜单,该属性仅在 mode 为 inline 时生效", + "defaultValue": { + "value": "'multiple'", + "computed": false + }, + "docblock": "展开内连子菜单的模式,同时可以展开一个子菜单还是多个子菜单,该属性仅在 mode 为 inline 时生效" + }, + "inlineIndent": { + "type": { + "name": "number" + }, + "required": false, + "description": "内连子菜单缩进距离", + "defaultValue": { + "value": "20", + "computed": false + }, + "docblock": "内连子菜单缩进距离" + }, + "popupAutoWidth": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否自动让弹层的宽度和菜单项保持一致,如果弹层的宽度比菜单项小则和菜单项保持一致,如果宽度大于菜单项则不做处理", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否自动让弹层的宽度和菜单项保持一致,如果弹层的宽度比菜单项小则和菜单项保持一致,如果宽度大于菜单项则不做处理" + }, + "popupAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'follow'", + "computed": false + }, + { + "value": "'outside'", + "computed": false + } + ] + }, + "required": false, + "description": "弹层的对齐方式", + "defaultValue": { + "value": "'follow'", + "computed": false + }, + "docblock": "弹层的对齐方式" + }, + "popupProps": { + "type": { + "name": "union", + "value": [{ + "name": "object" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "弹层自定义 props", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "弹层自定义 props" + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹出子菜单自定义 className", + "docblock": "弹出子菜单自定义 className" + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹出子菜单自定义 style", + "docblock": "弹出子菜单自定义 style", + "properties": [] + }, + "selectedKeys": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "当前选中菜单项的 key 值", + "docblock": "当前选中菜单项的 key 值" + }, + "defaultSelectedKeys": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "初始选中菜单项的 key 值", + "defaultValue": { + "value": "[]", + "computed": false + }, + "docblock": "初始选中菜单项的 key 值" + }, + "onSelect": { + "type": { + "name": "func" + }, + "required": false, + "description": "选中或取消选中菜单项触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "选中或取消选中菜单项触发的回调函数\n@param {Array} selectedKeys 选中的所有菜单项的值\n@param {Object} item 选中或取消选中的菜单项\n@param {Object} extra 额外参数\n@param {Boolean} extra.select 是否是选中\n@param {Array} extra.key 菜单项的 key\n@param {Object} extra.label 菜单项的文本\n@param {Array} extra.keyPath 菜单项 key 的路径", + "params": [{ + "name": "selectedKeys", + "description": "选中的所有菜单项的值", + "type": { + "name": "Array" + } + }, + { + "name": "item", + "description": "选中或取消选中的菜单项", + "type": { + "name": "Object" + } + }, + { + "name": "extra", + "description": "额外参数", + "type": { + "name": "Object" + } + }, + { + "name": "extra.select", + "description": "是否是选中", + "type": { + "name": "Boolean" + } + }, + { + "name": "extra.key", + "description": "菜单项的 key", + "type": { + "name": "Array" + } + }, + { + "name": "extra.label", + "description": "菜单项的文本", + "type": { + "name": "Object" + } + }, + { + "name": "extra.keyPath", + "description": "菜单项 key 的路径", + "type": { + "name": "Array" + } + } + ], + "returns": null + }, + "selectMode": { + "type": { + "name": "enum", + "value": [{ + "value": "'single'", + "computed": false + }, + { + "value": "'multiple'", + "computed": false + } + ] + }, + "required": false, + "description": "选中模式,单选还是多选,默认无值,不可选", + "docblock": "选中模式,单选还是多选,默认无值,不可选" + }, + "shallowSelect": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否只能选择第一层菜单项(不能选择子菜单中的菜单项)", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否只能选择第一层菜单项(不能选择子菜单中的菜单项)" + }, + "hasSelectedIcon": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示选中图标,如果设置为 false 需配合配置平台设置选中时的背景色以示区分", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示选中图标,如果设置为 false 需配合配置平台设置选中时的背景色以示区分" + }, + "isSelectIconRight": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否将选中图标居右,仅当 hasSelectedIcon 为true 时生效。\n注意:SubMenu 上的选中图标一直居左,不受此API控制", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否将选中图标居右,仅当 hasSelectedIcon 为true 时生效。\n注意:SubMenu 上的选中图标一直居左,不受此API控制" + }, + "direction": { + "type": { + "name": "enum", + "value": [{ + "value": "'ver'", + "computed": false + }, + { + "value": "'hoz'", + "computed": false + } + ] + }, + "required": false, + "description": "菜单第一层展示方向", + "defaultValue": { + "value": "'ver'", + "computed": false + }, + "docblock": "菜单第一层展示方向" + }, + "hozAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'left'", + "computed": false + }, + { + "value": "'right'", + "computed": false + } + ] + }, + "required": false, + "description": "横向菜单条 item 和 footer 的对齐方向,在 direction 设置为 'hoz' 并且 header 存在时生效", + "defaultValue": { + "value": "'left'", + "computed": false + }, + "docblock": "横向菜单条 item 和 footer 的对齐方向,在 direction 设置为 'hoz' 并且 header 存在时生效" + }, + "hozInLine": { + "type": { + "name": "bool" + }, + "required": false, + "description": "横向菜单模式下,是否维持在一行,即超出一行折叠成 SubMenu 显示, 仅在 direction='hoz' mode='popup' 时生效", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "横向菜单模式下,是否维持在一行,即超出一行折叠成 SubMenu 显示, 仅在 direction='hoz' mode='popup' 时生效" + }, + "header": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义菜单头部", + "docblock": "自定义菜单头部" + }, + "footer": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义菜单尾部", + "docblock": "自定义菜单尾部" + }, + "autoFocus": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否自动获得焦点", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否自动获得焦点" + }, + "focusedKey": { + "type": { + "name": "string" + }, + "required": false, + "description": "当前获得焦点的子菜单或菜单项 key 值", + "docblock": "当前获得焦点的子菜单或菜单项 key 值" + }, + "embeddable": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否开启嵌入式模式,一般用于Layout的布局中,开启后没有默认背景、外层border、box-shadow,可以配合`` 自定义高度", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否开启嵌入式模式,一般用于Layout的布局中,开启后没有默认背景、外层border、box-shadow,可以配合`` 自定义高度" + } + }, + "methods": [{ + "name": "create", + "docblock": "\n 创建上下文菜单\n @exportName create\n @param {Object} props 属性对象\n ", + "description": "创建上下文菜单", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "属性对象", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": null + }], + "subComponents": [{ + "name": "Item", + "title": "菜单项", + "props": { + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否禁用" + }, + "helper": { + "type": { + "name": "node" + }, + "required": false, + "description": "帮助文本", + "docblock": "帮助文本" + }, + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "菜单项标签内容", + "docblock": "菜单项标签内容" + } + }, + "methods": [], + "order": 0 + }, + { + "name": "SubMenu", + "title": "子菜单", + "props": { + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "标签内容", + "docblock": "标签内容" + }, + "selectable": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否可选,该属性仅在设置 Menu 组件 selectMode 属性后生效", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否可选,该属性仅在设置 Menu 组件 selectMode 属性后生效" + }, + "mode": { + "type": { + "name": "enum", + "value": [{ + "value": "'inline'", + "computed": false + }, + { + "value": "'popup'", + "computed": false + } + ] + }, + "required": false, + "description": "子菜单打开方式,如果设置会覆盖 Menu 上的同名属性", + "docblock": "子菜单打开方式,如果设置会覆盖 Menu 上的同名属性\n@default Menu 的 mode 属性值", + "defaultValue": { + "value": "Menu 的 mode 属性值", + "computed": false + } + }, + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "菜单项或下一级子菜单", + "docblock": "菜单项或下一级子菜单" + } + }, + "methods": [], + "order": 1 + }, + { + "name": "PopupItem", + "title": "弹出菜单", + "props": { + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "标签内容", + "docblock": "标签内容" + }, + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义弹层内容", + "docblock": "自定义弹层内容" + } + }, + "methods": [], + "order": 2 + }, + { + "name": "CheckboxItem", + "title": "复选菜单", + "props": { + "checked": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否选中", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否选中" + }, + "indeterminate": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否半选中", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否半选中" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否禁用" + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "选中或取消选中触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "选中或取消选中触发的回调函数\n@param {Boolean} checked 是否选中\n@param {Object} event 选中事件对象", + "params": [{ + "name": "checked", + "description": "是否选中", + "type": { + "name": "Boolean" + } + }, + { + "name": "event", + "description": "选中事件对象", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "helper": { + "type": { + "name": "node" + }, + "required": false, + "description": "帮助文本", + "docblock": "帮助文本" + }, + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "标签内容", + "docblock": "标签内容" + } + }, + "methods": [], + "description": "该子组件选中情况不受 defaultSelectedKeys/selectedKeys 控制,请自行控制选中逻辑", + "order": 3 + }, + { + "name": "RadioItem", + "title": "单选菜单", + "props": { + "checked": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否选中", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否选中" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否禁用" + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "选中或取消选中触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "选中或取消选中触发的回调函数\n@param {Boolean} checked 是否选中\n@param {Object} event 选中事件对象", + "params": [{ + "name": "checked", + "description": "是否选中", + "type": { + "name": "Boolean" + } + }, + { + "name": "event", + "description": "选中事件对象", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "helper": { + "type": { + "name": "node" + }, + "required": false, + "description": "帮助文本", + "docblock": "帮助文本" + }, + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "标签内容", + "docblock": "标签内容" + } + }, + "methods": [], + "description": "该子组件选中情况不受 defaultSelectedKeys/selectedKeys 控制,请自行控制选中逻辑", + "order": 4 + }, + { + "name": "Group", + "title": "菜单组", + "props": { + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "标签内容", + "docblock": "标签内容" + }, + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "菜单项", + "docblock": "菜单项" + } + }, + "methods": [], + "order": 5 + }, + { + "name": "Divider", + "title": "菜单分隔", + "props": {}, + "methods": [], + "order": 6 + } + ] + }, + { + "name": "MenuButton", + "title": "菜单按钮", + "typeId": 1, + "props": { + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "按钮上的文本内容", + "docblock": "按钮上的文本内容" + }, + "autoWidth": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层是否与按钮宽度相同", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "弹层是否与按钮宽度相同" + }, + "popupTriggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "弹层触发方式", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "弹层触发方式" + }, + "popupContainer": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层容器", + "docblock": "弹层容器", + "params": [], + "returns": null + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层展开状态", + "docblock": "弹层展开状态" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层默认是否展开", + "docblock": "弹层默认是否展开" + }, + "onVisibleChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层在显示和隐藏触发的事件", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层在显示和隐藏触发的事件", + "params": [], + "returns": null + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层自定义样式", + "docblock": "弹层自定义样式", + "properties": [] + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层自定义样式类", + "docblock": "弹层自定义样式类" + }, + "popupProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层属性透传", + "docblock": "弹层属性透传", + "properties": [] + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随滚动", + "docblock": "是否跟随滚动" + }, + "defaultSelectedKeys": { + "type": { + "name": "array" + }, + "required": false, + "description": "默认激活的菜单项(用法同 Menu 非受控)", + "defaultValue": { + "value": "[]", + "computed": false + }, + "docblock": "默认激活的菜单项(用法同 Menu 非受控)" + }, + "selectedKeys": { + "type": { + "name": "array" + }, + "required": false, + "description": "激活的菜单项(用法同 Menu 受控)", + "docblock": "激活的菜单项(用法同 Menu 受控)" + }, + "selectMode": { + "type": { + "name": "enum", + "value": [{ + "value": "'single'", + "computed": false + }, + { + "value": "'multiple'", + "computed": false + } + ] + }, + "required": false, + "description": "菜单的选择模式,同 Menu", + "docblock": "菜单的选择模式,同 Menu" + }, + "onItemClick": { + "type": { + "name": "func" + }, + "required": false, + "description": "点击菜单项后的回调,同 Menu", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "点击菜单项后的回调,同 Menu", + "params": [], + "returns": null + }, + "onSelect": { + "type": { + "name": "func" + }, + "required": false, + "description": "选择菜单后的回调,同 Menu", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "选择菜单后的回调,同 Menu", + "params": [], + "returns": null + }, + "menuProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "菜单属性透传", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "菜单属性透传", + "properties": [] + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "Message", + "title": "信息", + "typeId": 5, + "props": { + "type": { + "type": { + "name": "enum", + "value": [{ + "value": "'success'", + "computed": false + }, + { + "value": "'warning'", + "computed": false + }, + { + "value": "'error'", + "computed": false + }, + { + "value": "'notice'", + "computed": false + }, + { + "value": "'help'", + "computed": false + }, + { + "value": "'loading'", + "computed": false + } + ] + }, + "required": false, + "description": "反馈类型", + "defaultValue": { + "value": "'success'", + "computed": false + }, + "docblock": "反馈类型" + }, + "shape": { + "type": { + "name": "enum", + "value": [{ + "value": "'inline'", + "computed": false + }, + { + "value": "'addon'", + "computed": false + }, + { + "value": "'toast'", + "computed": false + } + ] + }, + "required": false, + "description": "反馈外观", + "defaultValue": { + "value": "'inline'", + "computed": false + }, + "docblock": "反馈外观" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "反馈大小", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "反馈大小" + }, + "title": { + "type": { + "name": "node" + }, + "required": false, + "description": "标题", + "docblock": "标题" + }, + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "内容", + "docblock": "内容" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "默认是否显示", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "默认是否显示" + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "当前是否显示", + "docblock": "当前是否显示" + }, + "iconType": { + "type": { + "name": "string" + }, + "required": false, + "description": "显示的图标类型,会覆盖内部设置的IconType", + "docblock": "显示的图标类型,会覆盖内部设置的IconType" + }, + "closeable": { + "type": { + "name": "bool" + }, + "required": false, + "description": "显示关闭按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "显示关闭按钮" + }, + "onClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "关闭按钮的回调", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "关闭按钮的回调", + "params": [], + "returns": null + }, + "afterClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "关闭之后调用的函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "关闭之后调用的函数", + "params": [], + "returns": null + }, + "animation": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否开启展开收起动画", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否开启展开收起动画" + } + }, + "methods": [{ + "name": "show", + "docblock": "\n 创建提示弹层\n @exportName show\n @param {Object} props 属性对象\n ", + "description": "创建提示弹层", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "属性对象", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": null + }, + { + "name": "hide", + "docblock": "\n 关闭提示弹层\n @exportName hide\n ", + "description": "关闭提示弹层", + "modifiers": [ + "static" + ], + "params": [], + "returns": null + }, + { + "name": "success", + "docblock": "\n 创建成功提示弹层\n @exportName success\n @param {Object} props 属性对象\n ", + "description": "创建成功提示弹层", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "属性对象", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": null + }, + { + "name": "warning", + "docblock": "\n 创建警告提示弹层\n @exportName warning\n @param {Object} props 属性对象\n ", + "description": "创建警告提示弹层", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "属性对象", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": null + }, + { + "name": "error", + "docblock": "\n 创建错误提示弹层\n @exportName error\n @param {Object} props 属性对象\n ", + "description": "创建错误提示弹层", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "属性对象", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": null + }, + { + "name": "help", + "docblock": "\n 创建帮助提示弹层\n @exportName help\n @param {Object} props 属性对象\n ", + "description": "创建帮助提示弹层", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "属性对象", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": null + }, + { + "name": "loading", + "docblock": "\n 创建加载中提示弹层\n @exportName loading\n @param {Object} props 属性对象\n ", + "description": "创建加载中提示弹层", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "属性对象", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": null + }, + { + "name": "notice", + "docblock": "\n 创建通知提示弹层\n @exportName notice\n @param {Object} props 属性对象\n ", + "description": "创建通知提示弹层", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "属性对象", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": null + } + ], + "subComponents": [] + }, + { + "name": "Nav", + "title": "导航", + "typeId": 2, + "props": { + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "导航项和子导航", + "docblock": "导航项和子导航" + }, + "onItemClick": { + "type": { + "name": "func" + }, + "required": false, + "description": "点击菜单项触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "点击菜单项触发的回调函数\n@param {String} key 点击的菜单项的 key 值\n@param {Object} item 点击的菜单项对象\n@param {Object} event 点击的事件对象", + "params": [{ + "name": "key", + "description": "点击的菜单项的 key 值", + "type": { + "name": "String" + } + }, + { + "name": "item", + "description": "点击的菜单项对象", + "type": { + "name": "Object" + } + }, + { + "name": "event", + "description": "点击的事件对象", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "openKeys": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "当前打开的子菜单的 key 值", + "docblock": "当前打开的子菜单的 key 值" + }, + "defaultOpenKeys": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "初始打开的子菜单的 key 值", + "defaultValue": { + "value": "[]", + "computed": false + }, + "docblock": "初始打开的子菜单的 key 值" + }, + "defaultOpenAll": { + "type": { + "name": "bool" + }, + "required": false, + "description": "初始展开所有的子导航,只在 mode 设置为 'inline' 以及 openMode 设置为 'multiple' 下生效", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "初始展开所有的子导航,只在 mode 设置为 'inline' 以及 openMode 设置为 'multiple' 下生效" + }, + "onOpen": { + "type": { + "name": "func" + }, + "required": false, + "description": "打开或关闭子菜单触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "打开或关闭子菜单触发的回调函数\n@param {String} key 打开的所有子菜单的 key 值\n@param {Object} extra 额外参数\n@param {String} extra.key 当前操作子菜单的 key 值\n@param {Boolean} extra.open 是否是打开", + "params": [{ + "name": "key", + "description": "打开的所有子菜单的 key 值", + "type": { + "name": "String" + } + }, + { + "name": "extra", + "description": "额外参数", + "type": { + "name": "Object" + } + }, + { + "name": "extra.key", + "description": "当前操作子菜单的 key 值", + "type": { + "name": "String" + } + }, + { + "name": "extra.open", + "description": "是否是打开", + "type": { + "name": "Boolean" + } + } + ], + "returns": null + }, + "mode": { + "type": { + "name": "enum", + "value": [{ + "value": "'inline'", + "computed": false + }, + { + "value": "'popup'", + "computed": false + } + ] + }, + "required": false, + "description": "子导航打开的模式(水平导航只支持弹出)", + "defaultValue": { + "value": "'inline'", + "computed": false + }, + "docblock": "子导航打开的模式(水平导航只支持弹出)\n@eumdesc 行内, 弹出" + }, + "triggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "子导航打开的触发方式", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "子导航打开的触发方式" + }, + "openMode": { + "type": { + "name": "enum", + "value": [{ + "value": "'single'", + "computed": false + }, + { + "value": "'multiple'", + "computed": false + } + ] + }, + "required": false, + "description": "内联子导航的展开模式,同时可以展开一个同级子导航还是多个同级子导航,该属性仅在 mode 为 inline 时生效", + "defaultValue": { + "value": "'multiple'", + "computed": false + }, + "docblock": "内联子导航的展开模式,同时可以展开一个同级子导航还是多个同级子导航,该属性仅在 mode 为 inline 时生效\n@eumdesc 一个, 多个" + }, + "inlineIndent": { + "type": { + "name": "number" + }, + "required": false, + "description": "内联子导航缩进距离", + "defaultValue": { + "value": "20", + "computed": false + }, + "docblock": "内联子导航缩进距离" + }, + "popupAutoWidth": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否自动让弹层的宽度和菜单项保持一致,如果弹层的宽度比菜单项小则和菜单项保持一致,如果宽度大于菜单项则不做处理", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否自动让弹层的宽度和菜单项保持一致,如果弹层的宽度比菜单项小则和菜单项保持一致,如果宽度大于菜单项则不做处理" + }, + "popupAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'follow'", + "computed": false + }, + { + "value": "'outside'", + "computed": false + } + ] + }, + "required": false, + "description": "弹出子导航的对齐方式(水平导航只支持 follow )", + "defaultValue": { + "value": "'follow'", + "computed": false + }, + "docblock": "弹出子导航的对齐方式(水平导航只支持 follow )\n@eumdesc Item 顶端对齐, Nav 顶端对齐" + }, + "popupProps": { + "type": { + "name": "union", + "value": [{ + "name": "object" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "弹层自定义 props", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "弹层自定义 props" + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹出子导航的自定义类名", + "docblock": "弹出子导航的自定义类名" + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹出子菜单自定义 style", + "docblock": "弹出子菜单自定义 style", + "properties": [] + }, + "selectedKeys": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "当前选中导航项的 key 值", + "docblock": "当前选中导航项的 key 值" + }, + "defaultSelectedKeys": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "初始选中导航项的 key 值", + "defaultValue": { + "value": "[]", + "computed": false + }, + "docblock": "初始选中导航项的 key 值" + }, + "onSelect": { + "type": { + "name": "func" + }, + "required": false, + "description": "选中或取消选中导航项触发的回调函数", + "docblock": "选中或取消选中导航项触发的回调函数\n@param {Array} selectedKeys 选中的所有导航项的 key\n@param {Object} item 选中或取消选中的导航项\n@param {Object} extra 额外参数\n@param {Boolean} extra.select 是否是选中\n@param {Array} extra.key 导航项的 key\n@param {Object} extra.label 导航项的文本\n@param {Array} extra.keyPath 导航项 key 的路径", + "params": [{ + "name": "selectedKeys", + "description": "选中的所有导航项的 key", + "type": { + "name": "Array" + } + }, + { + "name": "item", + "description": "选中或取消选中的导航项", + "type": { + "name": "Object" + } + }, + { + "name": "extra", + "description": "额外参数", + "type": { + "name": "Object" + } + }, + { + "name": "extra.select", + "description": "是否是选中", + "type": { + "name": "Boolean" + } + }, + { + "name": "extra.key", + "description": "导航项的 key", + "type": { + "name": "Array" + } + }, + { + "name": "extra.label", + "description": "导航项的文本", + "type": { + "name": "Object" + } + }, + { + "name": "extra.keyPath", + "description": "导航项 key 的路径", + "type": { + "name": "Array" + } + } + ], + "returns": null + }, + "selectMode": { + "type": { + "name": "enum", + "value": [{ + "value": "'single'", + "computed": false + }, + { + "value": "'multiple'", + "computed": false + } + ] + }, + "required": false, + "description": "选中模式,单选还是多选,默认无值,不可选", + "docblock": "选中模式,单选还是多选,默认无值,不可选" + }, + "shallowSelect": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否只能选择第一层菜单项(不能选择子菜单中的菜单项)", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否只能选择第一层菜单项(不能选择子菜单中的菜单项)" + }, + "hasSelectedIcon": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示选中图标,如果设置为 false 需配合配置平台设置选中时的背景色以示区分", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示选中图标,如果设置为 false 需配合配置平台设置选中时的背景色以示区分" + }, + "isSelectIconRight": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否将选中图标居右,仅当 hasSelectedIcon 为true 时生效。\n注意:SubMenu 上的选中图标一直居左,不受此API控制", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否将选中图标居右,仅当 hasSelectedIcon 为true 时生效。\n注意:SubMenu 上的选中图标一直居左,不受此API控制" + }, + "direction": { + "type": { + "name": "enum", + "value": [{ + "value": "'hoz'", + "computed": false, + "description": "水平" + }, + { + "value": "'ver'", + "computed": false, + "description": "垂直" + } + ] + }, + "required": false, + "description": "导航布局", + "defaultValue": { + "value": "'ver'", + "computed": false + }, + "docblock": "导航布局\n@enumdesc 水平, 垂直", + "value": [{ + "value": "'hoz'", + "computed": false, + "description": "水平" + }, + { + "value": "'ver'", + "computed": false, + "description": "垂直" + } + ] + }, + "hozAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'left'", + "computed": false + }, + { + "value": "'right'", + "computed": false + } + ] + }, + "required": false, + "description": "横向导航条 items 和 footer 的对齐方向,在 direction 设置为 'hoz' 并且 header 存在时生效", + "defaultValue": { + "value": "'left'", + "computed": false + }, + "docblock": "横向导航条 items 和 footer 的对齐方向,在 direction 设置为 'hoz' 并且 header 存在时生效" + }, + "hozInLine": { + "type": { + "name": "bool" + }, + "required": false, + "description": "横向菜单模式下,是否维持在一行,即超出一行折叠成 SubMenu 显示, 仅在 direction='hoz' mode='popup' 时生效", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "横向菜单模式下,是否维持在一行,即超出一行折叠成 SubMenu 显示, 仅在 direction='hoz' mode='popup' 时生效" + }, + "header": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义导航头部", + "docblock": "自定义导航头部" + }, + "footer": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义导航尾部", + "docblock": "自定义导航尾部" + }, + "autoFocus": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否自动获得焦点", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否自动获得焦点" + }, + "focusedKey": { + "type": { + "name": "string" + }, + "required": false, + "description": "当前获得焦点的子菜单或菜单项 key 值", + "docblock": "当前获得焦点的子菜单或菜单项 key 值" + }, + "embeddable": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否开启嵌入式模式,一般用于Layout的布局中,开启后没有默认背景、外层border、box-shadow,可以配合`