├── .commitlintrc ├── .dumirc.ts ├── .editorconfig ├── .eslintrc ├── .github └── workflows │ ├── docs.yml │ └── testing.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .prettierrc ├── .stylelintrc ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── examples ├── components │ └── SyntaxHighlighter │ │ ├── index.tsx │ │ └── styles.less ├── core-react │ ├── 001-intro.md │ ├── 002-quick-start.md │ ├── 003-schema.md │ ├── 100-layout.md │ ├── 101-item-layout.md │ ├── 102-prefix-cls.md │ ├── 200-change-event.md │ ├── 210-relation.md │ ├── 220-renderer.md │ ├── 221-renderer-object.md │ ├── 222-renderer-array.md │ ├── 223-validator.md │ ├── 230-global-status.md │ ├── 250-scroll.md │ ├── 280-open-api.md │ ├── 300-default-value.md │ ├── 600-catch-error-tips.md │ ├── 700-locale.md │ ├── 800-api-reference.md │ ├── 890-benchmark.md │ ├── 900-dev-tool.md │ ├── 910-playground.md │ ├── 930-traverse-schema.md │ ├── 931-schema-transform.md │ ├── 950-async.md │ ├── 951-controllable.md │ ├── 952-multi-renderer-onChange.md │ ├── 953-disabled-component.md │ ├── images │ │ ├── item-layout.png │ │ ├── renderer-flow.png │ │ └── schema.png │ ├── item-layout │ │ ├── Horizontal │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ └── Vertical │ │ │ ├── index.tsx │ │ │ └── styles.ts │ ├── renderers │ │ ├── common.tsx │ │ └── object │ │ │ ├── ObjectCollapse.tsx │ │ │ ├── ObjectNormal.tsx │ │ │ └── index.tsx │ └── schemas │ │ ├── object.ts │ │ └── validator-shape.ts ├── dumi.scss ├── form-render-react │ ├── 001-intro.md │ ├── 002-ssr.md │ ├── 100-item-layout.md │ ├── 200-label.md │ ├── 300-actions.md │ ├── 500-builtin-renderers.md │ ├── 700-locale.md │ ├── 800-api-reference.md │ ├── 850-position.md │ ├── 851-steps.md │ ├── 852-audit-renderer.md │ ├── 853-validate-on-blur.md │ ├── 854-scroll-to-first-error.md │ ├── 880-relation-checkbox.md │ ├── 881-relation-select-multiple.md │ ├── 890-benchmark.md │ ├── 900-renderers-warning.md │ ├── 901-label-vertical.md │ ├── renderers │ │ ├── BankSelect.tsx │ │ └── index.tsx │ ├── schemas │ │ └── all.ts │ └── styles │ │ └── index.ts ├── global.scss ├── index.md ├── search-react │ ├── 001-intro.md │ ├── 002-ssr.md │ ├── 300-actions.md │ ├── 700-locale.md │ ├── 800-api-reference.md │ ├── 850-category-title.md │ ├── helpers │ │ ├── columns.ts │ │ └── createDataSource.ts │ └── renderers │ │ ├── BankSelect.tsx │ │ ├── ObjectSectionTitle.tsx │ │ └── SectionTitle.tsx ├── search-table-react │ ├── 001-intro.md │ ├── 002-ssr.md │ ├── 010-table-actions.md │ ├── 020-table-height.md │ ├── 030-table-summay.md │ ├── 040-table-sort.md │ ├── 050-tabs.md │ ├── 060-value-type.md │ ├── 100-columns-setting.md │ ├── 700-locale.md │ ├── 800-api-reference.md │ ├── 801-open-api.md │ ├── 900-table-editable.md │ ├── 901-table-action-link.md │ ├── 902-table-action-permission.md │ ├── 910-no-search.md │ └── helpers │ │ ├── columns-en.ts │ │ ├── columns-tree.ts │ │ ├── columns-value-type.ts │ │ ├── columns.ts │ │ ├── createDataSource-en.ts │ │ ├── createDataSource-vt.ts │ │ ├── createDataSource.ts │ │ ├── schema-en.ts │ │ └── schema.ts └── utils │ ├── cssinjs.ts │ └── index.ts ├── jest.config.ts ├── package.json ├── packages ├── core-react │ ├── .fatherrc.ts │ ├── README.md │ ├── package.json │ └── src │ │ ├── Core.tsx │ │ ├── MemoCore.tsx │ │ ├── RootContext.tsx │ │ ├── components │ │ └── ErrorBoundary.tsx │ │ ├── constants │ │ └── index.ts │ │ ├── hooks │ │ ├── useAsyncEffect.ts │ │ ├── useCoreValue.ts │ │ ├── useDebounceFn.ts │ │ ├── useDevTool.ts │ │ ├── useForceUpdate.ts │ │ ├── useLatest.ts │ │ ├── useLayoutStyle.ts │ │ ├── useMemoizedFn.ts │ │ ├── useMounted.ts │ │ ├── useOpenApi.ts │ │ ├── useRootContext.ts │ │ ├── useUnmount.ts │ │ └── useUnmountedRef.ts │ │ ├── index.tsx │ │ ├── locale │ │ ├── en_US.ts │ │ └── zh_CN.ts │ │ ├── services │ │ ├── RendererDistributor.tsx │ │ ├── RendererExecutor.tsx │ │ └── RendererIterator.tsx │ │ ├── typings │ │ ├── common.d.ts │ │ ├── core.d.ts │ │ ├── rootContext.d.ts │ │ └── schema.d.ts │ │ └── utils │ │ ├── assert.ts │ │ ├── base.ts │ │ ├── checking.ts │ │ ├── classnames.ts │ │ ├── dom.ts │ │ ├── index.export.ts │ │ ├── logger.ts │ │ ├── misc.ts │ │ ├── renderer.ts │ │ ├── schemaRulesValidator.ts │ │ ├── statement.ts │ │ ├── tinyLodash.ts │ │ ├── traverseSchema.ts │ │ └── validator.ts ├── form-render-react │ ├── .fatherrc.ts │ ├── README.md │ ├── package.json │ └── src │ │ ├── FormRender.tsx │ │ ├── FormRenderContext.tsx │ │ ├── components │ │ ├── Actions.tsx │ │ └── Description.tsx │ │ ├── constants │ │ └── index.tsx │ │ ├── cssinjs │ │ └── index.ts │ │ ├── hooks │ │ ├── useActions.tsx │ │ ├── useFormRenderContext.tsx │ │ ├── useItemLayout.tsx │ │ └── useSchema.ts │ │ ├── index.style.ts │ │ ├── index.tsx │ │ ├── itemLayouts │ │ ├── Horizontal │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ └── Vertical │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── locale │ │ ├── en_US.ts │ │ └── zh_CN.ts │ │ ├── renderers │ │ ├── Checkbox.tsx │ │ ├── DatePicker.tsx │ │ ├── DateRangePicker.tsx │ │ ├── Description.tsx │ │ ├── FromRenderActions.tsx │ │ ├── InputNumber.tsx │ │ ├── InputText.tsx │ │ ├── Object.tsx │ │ ├── ObjectNull.tsx │ │ ├── Password.tsx │ │ ├── Radio.tsx │ │ ├── Rate.tsx │ │ ├── Select.tsx │ │ ├── SelectMultiple.tsx │ │ ├── Switch.tsx │ │ ├── SwitchBox.tsx │ │ ├── TextArea.tsx │ │ └── index.tsx │ │ ├── typings │ │ └── index.d.ts │ │ └── utils │ │ └── index.ts ├── search-react │ ├── .fatherrc.ts │ ├── README.md │ ├── package.json │ └── src │ │ ├── Search.tsx │ │ ├── constants │ │ └── index.tsx │ │ ├── hooks │ │ ├── useCollapse.tsx │ │ └── useResize.ts │ │ ├── index.tsx │ │ ├── locale │ │ ├── en_US.ts │ │ └── zh_CN.ts │ │ ├── typings │ │ └── index.d.ts │ │ └── utils │ │ ├── actions.ts │ │ ├── collapsedNumber.ts │ │ ├── createDisplayedSchema.ts │ │ └── tinyLodash.ts └── search-table-react │ ├── .fatherrc.ts │ ├── README.md │ ├── package.json │ └── src │ ├── RootContext.tsx │ ├── SearchTable.tsx │ ├── components │ ├── ButtonLoading │ │ └── index.tsx │ ├── ColumnSettingContent │ │ ├── index.column.tsx │ │ ├── index.style.ts │ │ └── index.tsx │ ├── ColumnSettingModal │ │ └── index.tsx │ ├── ImagesPreview │ │ └── index.tsx │ └── Sortable │ │ └── index.tsx │ ├── constants │ ├── index.tsx │ ├── regexp.tsx │ └── style.tsx │ ├── hooks │ ├── useColumns │ │ ├── helpers │ │ │ ├── actions.tsx │ │ │ ├── setting.tsx │ │ │ ├── sort.tsx │ │ │ └── traverse.tsx │ │ ├── index.tsx │ │ ├── useBaseColumns.tsx │ │ ├── useFinalColumns.tsx │ │ └── useSortColumns.tsx │ ├── useOpenApi.tsx │ ├── useRequest.tsx │ ├── useRootContext.ts │ ├── useScrollY.tsx │ ├── useSearch.tsx │ ├── useSummary │ │ ├── helpers.tsx │ │ └── index.tsx │ └── useTitle.tsx │ ├── index.tsx │ ├── locale │ ├── en_US.ts │ └── zh_CN.ts │ ├── typings │ ├── index.d.ts │ ├── rootContext.d.ts │ └── table.d.ts │ ├── utils │ ├── common.ts │ └── dom.ts │ └── valueTypes │ ├── CommaNumber.tsx │ ├── LongText.tsx │ ├── LongTextModal.tsx │ └── index.tsx ├── public ├── favicon.png └── wechat-praise.jpg ├── scripts ├── build-cli.mjs ├── build.mjs ├── config │ ├── swc.cjs.json │ └── swc.esm.json ├── logger.mjs └── release.mjs ├── test ├── @helpers │ ├── dom.ts │ ├── jest-setup.ts │ ├── schema.ts │ └── sleep.ts ├── @mock │ └── mockWindowAttrs.ts └── core-react │ ├── @helpers │ ├── CompletedCore.tsx │ ├── Vertical.tsx │ └── renderers.tsx │ ├── components │ └── ErrorBoundary.test.tsx │ ├── disabled.test.tsx │ ├── hooks │ ├── useDebounceFn.test.tsx │ ├── useDevTool.test.tsx │ ├── useLatest.test.tsx │ ├── useLayoutStyle.test.tsx │ ├── useMemoizedFn.test.tsx │ ├── useUnmount.test.tsx │ └── useUnmountedRef.test.tsx │ ├── onChange.test.tsx │ ├── onItemChange.test.tsx │ ├── openApi.test.tsx │ ├── readonly.test.tsx │ ├── rootClassNames.test.tsx │ ├── span.test.tsx │ ├── utils │ ├── assert.test.ts │ ├── base.test.ts │ ├── checking.test.ts │ ├── classnames.test.ts │ ├── logger.test.ts │ ├── schemaRulesValidator.test.ts │ ├── tinyLodash.test.ts │ └── validator.test.ts │ └── watch.test.tsx ├── tsconfig.json ├── tsconfig.test.json └── yarn.lock /.commitlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@j-lints/commitlint-config"] 3 | } 4 | -------------------------------------------------------------------------------- /.dumirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi' 2 | import sass from 'sass' 3 | import path from 'path' 4 | 5 | const globalStyles = sass.compile('./examples/global.scss') 6 | 7 | export default defineConfig({ 8 | outputPath: 'docs-dist', 9 | resolve: { 10 | docDirs: ['examples'], 11 | }, 12 | styles: [globalStyles.css], 13 | themeConfig: { 14 | name: 'SchemaRender', 15 | logo: false, 16 | nav: [ 17 | { title: '💎 Core', activePath: '/core-react', link: '/core-react/001-intro' }, 18 | { title: '🚀 FormRender', activePath: '/form-render-react', link: '/form-render-react/001-intro' }, 19 | { title: '⛵️ Search', activePath: '/search-react', link: '/search-react/001-intro' }, 20 | { title: '🛳 SearchTable', activePath: '/search-table-react', link: '/search-table-react/001-intro' }, 21 | ], 22 | socialLinks: { 23 | github: 'https://github.com/Barrior/schema-render', 24 | }, 25 | rtl: true, 26 | footer: false, 27 | }, 28 | favicons: [ 29 | // 指向 public/favicon.png 30 | '/favicon.png', 31 | ], 32 | alias: { 33 | // 根据精确程度按顺序声明 34 | '@schema-render/core-react/dist/esm': path.resolve('packages/core-react/src'), 35 | '@schema-render/form-render-react/dist/esm': path.resolve( 36 | 'packages/form-render-react/src' 37 | ), 38 | '@schema-render/search-react/dist/esm': path.resolve('packages/search-react/src'), 39 | '@schema-render/search-table-react/dist/esm': path.resolve( 40 | 'packages/search-table-react/src' 41 | ), 42 | 43 | '@examples': path.resolve('examples'), 44 | '@schema-render/core-react': path.resolve('packages/core-react/src'), 45 | '@schema-render/form-render-react': path.resolve('packages/form-render-react/src'), 46 | '@schema-render/search-react': path.resolve('packages/search-react/src'), 47 | '@schema-render/search-table-react': path.resolve('packages/search-table-react/src'), 48 | }, 49 | mfsu: false, 50 | analytics: { 51 | // 百度统计 52 | baidu: 'c5249e1a7532f134490322a9c0c82945', 53 | }, 54 | }) 55 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@j-lints/eslint-config-ts-react"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | 9 | # allow actions to edit the project 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | build-and-deploy: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Copy project to machine 18 | uses: actions/checkout@v3 19 | with: 20 | # 确保可以 Dumi 可以拿到正确的最后更新时间,当配置 themeConfig.lastUpdated 为 true 21 | fetch-depth: 0 22 | 23 | - name: Setup Node 24 | uses: actions/setup-node@v3 25 | with: 26 | node: 18 27 | 28 | - name: Restore node_modules 29 | id: restore-node-modules 30 | uses: actions/cache/restore@v3 31 | with: 32 | path: node_modules 33 | key: cache-${{ hashFiles('yarn.lock') }} 34 | 35 | - name: Install dependencies 36 | if: steps.restore-node-modules.outputs.cache-hit != 'true' 37 | run: yarn install 38 | 39 | - name: Cache node_modules 40 | if: steps.restore-node-modules.outputs.cache-hit != 'true' 41 | uses: actions/cache/save@v3 42 | with: 43 | path: node_modules 44 | key: ${{ steps.restore-node-modules.outputs.cache-primary-key }} 45 | 46 | - name: Build docs 47 | run: yarn docs:build 48 | 49 | - name: Deploy 50 | uses: JamesIves/github-pages-deploy-action@v4 51 | with: 52 | folder: docs-dist 53 | branch: gh-pages 54 | clean: true 55 | clean-exclude: CNAME 56 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: push 4 | 5 | jobs: 6 | testing: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Copy project to machine 10 | uses: actions/checkout@v3 11 | 12 | - name: Setup Node 13 | uses: actions/setup-node@v3 14 | with: 15 | node: 18 16 | 17 | - name: Restore node_modules 18 | id: restore-node-modules 19 | uses: actions/cache/restore@v3 20 | with: 21 | path: node_modules 22 | key: cache-${{ hashFiles('yarn.lock') }} 23 | 24 | - name: Install dependencies 25 | if: steps.restore-node-modules.outputs.cache-hit != 'true' 26 | run: yarn install 27 | 28 | - name: Cache node_modules 29 | if: steps.restore-node-modules.outputs.cache-hit != 'true' 30 | uses: actions/cache/save@v3 31 | with: 32 | path: node_modules 33 | key: ${{ steps.restore-node-modules.outputs.cache-primary-key }} 34 | 35 | - name: Run lint 36 | run: yarn lint 37 | 38 | - name: Run testing with coverage 39 | run: yarn cov 40 | 41 | - name: Upload coverage reports to Codecov 42 | uses: codecov/codecov-action@v3 43 | env: 44 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MacOS files 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | *.lcov 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # TypeScript cache 24 | *.tsbuildinfo 25 | 26 | # Optional npm cache directory 27 | .npm 28 | 29 | # Optional eslint cache 30 | .eslintcache 31 | 32 | # Optional stylelint cache 33 | .stylelintcache 34 | 35 | # Output of 'npm pack' 36 | *.tgz 37 | 38 | # yarn v2 39 | .yarn/cache 40 | .yarn/unplugged 41 | .yarn/build-state.yml 42 | .yarn/install-state.gz 43 | .pnp.* 44 | 45 | # node_modules 46 | node_modules/ 47 | 48 | # Dumi 49 | .dumi/ 50 | 51 | # Output Dir 52 | dist/ 53 | /docs-dist/ 54 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | # https://github.com/typicode/husky/issues/912#issuecomment-817522060 5 | # sets the correct PATH before running hook for use nvm 6 | export NVM_DIR="$HOME/.nvm" 7 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm for general case 8 | [ -s "/usr/local/opt/nvm/nvm.sh" ] && . "/usr/local/opt/nvm/nvm.sh" # This loads nvm for brew case 9 | 10 | npx commitlint --edit $1 11 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | # https://typicode.github.io/husky/#/?id=command-not-found 5 | # This loads nvm.sh and sets the correct PATH before running hook 6 | export NVM_DIR="$HOME/.nvm" 7 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm for general case 8 | [ -s "/usr/local/opt/nvm/nvm.sh" ] && . "/usr/local/opt/nvm/nvm.sh" # This loads nvm for brew case 9 | 10 | npx lint-staged 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 90, 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@j-lints/stylelint-config-scss"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "AttachJestTest", 11 | "port": 9229 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "antd", 4 | "Codecov", 5 | "commitlint", 6 | "cssinjs", 7 | "dumi", 8 | "execa", 9 | "Immer", 10 | "Popconfirm", 11 | "shenzhen", 12 | "stylelint", 13 | "testid" 14 | ], 15 | "stylelint.validate": ["css", "less", "postcss", "scss"], 16 | "editor.formatOnSave": true 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-present 花祁 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## SchemaRender 2 | 3 | [![Documentation Build](https://github.com/Barrior/schema-render/actions/workflows/docs.yml/badge.svg)](https://github.com/Barrior/schema-render/actions/workflows/docs.yml) 4 | [![Testing](https://github.com/Barrior/schema-render/actions/workflows/testing.yml/badge.svg)](https://github.com/Barrior/schema-render/actions/workflows/testing.yml) 5 | [![codecov](https://codecov.io/gh/Barrior/schema-render/graph/badge.svg?token=38DB2R33FD)](https://codecov.io/gh/Barrior/schema-render) 6 | [![npm version](https://img.shields.io/npm/v/@schema-render/core-react)](https://www.npmjs.com/package/@schema-render/core-react) 7 | [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Barrior/JParticles/blob/master/LICENSE) 8 | 9 | `SchemaRender` 是一套 `默认简单`,但又追求 `灵活`、`可高定`、`好用的` 表单渲染解决方案,旨在沉淀解决常见表单案例的渲染库,与相关的通用组件,以提升研发效率。 10 | 11 | 内容目录: 12 | 13 | - `Core`: 表单渲染库内核,通过一套简易的 JSON Schema 构建出一套表单,内核主要处理 Schema 协议、渲染器编排、逻辑联动、校验能力等,支持 SSR(Server-Side Rendering)。 14 | - 特点:协议驱动、简洁易用、高可定制、轻量级。 15 | - `FormRender`: 基于 Core + [Antd](https://ant.design) 封装的开箱即用的表单渲染库。 16 | - `Search`:基于 FormRender 封装的开箱即用的条件搜索组件。 17 | - `SearchTable`:基于 Search + Antd Table 封装的条件搜索表格组件。 18 | 19 | 更多详情请移步至官网:[schema-render.js.org](https://schema-render.js.org) 20 | 21 | 22 | 23 | --- 24 | 25 | `SchemaRender` is a set of `simple by default`, but also pursues `flexible`, `highly customizable` and `easy to use` form rendering solutions. It aims to precipitate a rendering library to solve common form cases, and related common components , to improve development efficiency. 26 | 27 | Table of Contents: 28 | 29 | - `Core`: The core of the form rendering library, which builds a set of forms through a simple JSON Schema. The core mainly handles the Schema protocol, renderer arrangement, logical linkage, verification capabilities, etc., and supports SSR (Server-Side Rendering). 30 | - Features: Protocol driven, easy to use, highly customizable, lightweight. 31 | - `FormRender`: An out-of-the-box form rendering library based on Core + [Antd](https://ant.design). 32 | - `Search`: out-of-the-box conditional search component based on FormRender. 33 | - `SearchTable`: A conditional search table component based on Search + Antd Table. 34 | 35 | For more details, please go to the official website: [schema-render.js.org](https://schema-render.js.org) 36 | -------------------------------------------------------------------------------- /examples/components/SyntaxHighlighter/index.tsx: -------------------------------------------------------------------------------- 1 | import './styles.less' 2 | 3 | import type { CSSProperties } from 'react' 4 | import SyntaxHighlighter from 'react-syntax-highlighter' 5 | // @ts-ignore 6 | import { stackoverflowLight } from 'react-syntax-highlighter/dist/esm/styles/hljs' 7 | 8 | interface IProps { 9 | value: object | string 10 | language?: string 11 | style?: CSSProperties 12 | } 13 | 14 | const ESyntaxHighlighter: React.FC = ({ value, language = 'json', style }) => { 15 | return ( 16 |
17 | 22 | {language === 'json' ? JSON.stringify(value, null, 2) : (value as string)} 23 | 24 |
25 | ) 26 | } 27 | 28 | export default ESyntaxHighlighter 29 | -------------------------------------------------------------------------------- /examples/components/SyntaxHighlighter/styles.less: -------------------------------------------------------------------------------- 1 | .example-hl { 2 | padding: 1.2em !important; 3 | margin: 0; 4 | border-radius: 6px; 5 | text-align: left; 6 | 7 | code[class*="language-"], 8 | pre[class*="language-"] { 9 | background: none; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/core-react/001-intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: 介绍 4 | order: 0 5 | toc: false 6 | --- 7 | 8 | # 简介 9 | 10 | ## SchemaRender 11 | 12 | `SchemaRender` 是一套 `默认简单`,但又追求 `灵活`、`可高定`、`好用的` 表单渲染解决方案,旨在沉淀解决常见表单案例的渲染库,与相关的通用组件,以提升研发效率。 13 | 14 | 内容目录: 15 | 16 | - `Core`: 表单渲染库内核,通过一套简易的 [JSON Schema](./003-schema.md) 构建出一套表单,内核主要处理 Schema 协议、渲染器编排、逻辑联动、校验能力等,支持 SSR(Server-Side Rendering)。 17 | - `FormRender`: 基于 Core + [Antd](https://ant.design) 封装的开箱即用的表单渲染库。 18 | - `Search`:基于 FormRender 封装的开箱即用的条件搜索组件。 19 | - `SearchTable`:基于 Search + Antd Table 封装的条件搜索表格。 20 | 21 | :::info{title=温馨提示} 22 | 目前项目是基于 React 技术栈实现的,所以后缀都加了 `-react` 字眼,后续可能会将 Core 抽离成不依赖于技术栈的纯 JavaScript 项目,以便支持任何技术框架。 23 | ::: 24 | 25 | ## Core 26 | 27 | 渲染内核必须组装 `itemLayout`(表单项布局)、`Renderers`(渲染器集合)来实现一个渲染库,然后通过 JSON Schema 驱动渲染出所需的表单。 28 | 29 | 特点:协议驱动、简洁易用、高可定制、轻量级。 30 | 31 | - `协议驱动`:通过编写一份简易的 JSON Schema 来实现内容的渲染,参考[快速上手](./002-quick-start.md)。 32 | - `简洁易用`:仁者见仁智者见智,笔者以为是比较言简意赅、易用的,如同内部源码一样,化繁为简,无必要不增实体。 33 | - `高可定制`:通过组装 layout、itemLayout、renderers 等可以按需实现一个贴合自身业务的渲染库,如 FormRender。 34 | - `轻量级`:资源包大小仅 17.6k, gzip 6.5k 35 | -------------------------------------------------------------------------------- /examples/core-react/102-prefix-cls.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 样式定制 3 | order: 102 4 | toc: content 5 | --- 6 | 7 | # 类名定制 prefixCls 8 | 9 | 通过 `prefixCls` 定制样式类名前缀 10 | 11 | :::info{title=温馨提示} 12 | 打开 DevTools 面板查看 DOM 元素类名 13 | ::: 14 | 15 | ```tsx 16 | import Core from '@schema-render/core-react' 17 | import Horizontal from './item-layout/Horizontal' 18 | import renderers from './renderers/common' 19 | 20 | const schema = { 21 | renderType: 'Root', 22 | properties: { 23 | width: { 24 | title: '宽度', 25 | renderType: 'InputNumber', 26 | }, 27 | height: { 28 | title: '高度', 29 | renderType: 'InputNumber', 30 | }, 31 | left: { 32 | title: '水平位置', 33 | renderType: 'InputNumber', 34 | }, 35 | top: { 36 | title: '垂直位置', 37 | renderType: 'InputNumber', 38 | }, 39 | }, 40 | } as const 41 | 42 | const Demo = () => { 43 | return ( 44 | 50 | ) 51 | } 52 | 53 | export default Demo 54 | ``` 55 | -------------------------------------------------------------------------------- /examples/core-react/222-renderer-array.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 功能详解 3 | order: 222 4 | toc: content 5 | --- 6 | 7 | # 结构渲染器-数组 Array 8 | 9 | :::info{title=温馨提示} 10 | 还在设计中 11 | ::: 12 | -------------------------------------------------------------------------------- /examples/core-react/230-global-status.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 功能详解 3 | order: 230 4 | toc: content 5 | --- 6 | 7 | # 全局状态控制 8 | 9 | 全局状态控制的出现是为了在最顶层直接控制表单的状态,以降低协议控制编码成本。 10 | 11 | 全局状态控制的权重大于 Schema 协议的状态控制。 12 | 13 | 以下是全局状态控制可用的参数 14 | 15 | - `disabled`: boolean 类型,控制表单项是否`禁用`,禁用时会在根节点增加 `is-disabled` 类名。 16 | - `readonly`: boolean 类型,控制表单项是否`只读`,只读时会在根节点增加 `is-readonly` 类名。 17 | 18 | ## 全局禁用态示例 19 | 20 | ```tsx 21 | /** 22 | * defaultShowCode: true 23 | */ 24 | import Core from '@schema-render/core-react' 25 | import Horizontal from './item-layout/Horizontal' 26 | import renderers from './renderers/common' 27 | 28 | const schema = { 29 | renderType: 'Root', 30 | properties: { 31 | title: { 32 | title: '标题', 33 | renderType: 'InputText', 34 | renderOptions: { 35 | maxLength: 10, 36 | placeholder: '请输入标题,最多10个字符', 37 | }, 38 | }, 39 | content: { 40 | title: '内容', 41 | renderType: 'Textarea', 42 | }, 43 | }, 44 | } as const 45 | 46 | const Demo = () => { 47 | return ( 48 | 55 | ) 56 | } 57 | 58 | export default Demo 59 | ``` 60 | -------------------------------------------------------------------------------- /examples/core-react/300-default-value.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 功能详解 3 | order: 300 4 | toc: content 5 | debug: true 6 | --- 7 | 8 | # 默认值 DefaultValue 9 | 10 | ## Core 默认值 11 | 12 | ```tsx 13 | import Core from '@schema-render/core-react' 14 | import Horizontal from './item-layout/Horizontal' 15 | import renderers from './renderers/common' 16 | 17 | const schema = { 18 | renderType: 'Root', 19 | properties: { 20 | title: { 21 | title: '标题', 22 | renderType: 'InputText', 23 | }, 24 | content: { 25 | title: '内容', 26 | renderType: 'Textarea', 27 | }, 28 | }, 29 | } as const 30 | 31 | const Demo = () => { 32 | return ( 33 | 41 | ) 42 | } 43 | 44 | export default Demo 45 | ``` 46 | 47 | ## Schema 协议默认值 48 | 49 | :::info{title=温馨提示} 50 | 还在设计中 51 | ::: 52 | -------------------------------------------------------------------------------- /examples/core-react/700-locale.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 功能详解 3 | order: 700 4 | --- 5 | 6 | # 国际化 Locale 7 | 8 | `Core` 默认文案是中文,如果需要使用其他语言,只需配置 `locale` 覆盖默认的语言,示例如下。 9 | 10 | ```tsx 11 | /** 12 | * defaultShowCode: true 13 | */ 14 | import Core, { IRootSchema } from '@schema-render/core-react' 15 | import Horizontal from './item-layout/Horizontal' 16 | import renderers from './renderers/common' 17 | 18 | // 引入英文语言包 19 | import enUS from '@schema-render/core-react/dist/esm/locale/en_US' 20 | 21 | const schema: IRootSchema = { 22 | renderType: 'Root', 23 | properties: { 24 | title: { 25 | title: 'Title', 26 | renderType: 'InputText', 27 | renderOptions: { 28 | maxLength: 10, 29 | placeholder: 'Please enter a title with a maximum of 10 characters', 30 | }, 31 | required: true, 32 | }, 33 | content: { 34 | title: 'Content', 35 | renderType: 'TextArea', 36 | renderOptions: { 37 | placeholder: 'Please enter content', 38 | }, 39 | required: true, 40 | }, 41 | }, 42 | } 43 | 44 | const Demo = () => { 45 | return ( 46 | 54 | ) 55 | } 56 | 57 | export default Demo 58 | ``` 59 | 60 | 目前支持以下语言: 61 | 62 | | **语言** | **文件名** | 63 | | -------- | ---------- | 64 | | 中文 | zh_CN | 65 | | 英文 | en_US | 66 | 67 | - 如果找不到你需要的语言包,欢迎参考 [中文语言包](https://github.com/Barrior/schema-render/tree/main/packages/core-react/src/locale/zh_CN.ts) 创建一个新的语言包,并给我们发一个 Pull Request。 68 | - 或者配置相同的语言包数据格式传值给 `locale` 属性。 69 | -------------------------------------------------------------------------------- /examples/core-react/800-api-reference.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: API 手册 4 | order: 800 5 | toc: content 6 | --- 7 | 8 | # API Reference 9 | 10 | ## catchErrorTips 11 | 12 | 参见[错误提示信息](./600-catch-error-tips)。 13 | 14 | ## className 15 | 16 | 根节点类名。 17 | 18 | ## defaultValue 19 | 20 | 表单默认值。 21 | 22 | ## disabled 23 | 24 | 是否全局禁用表单,详情见[全局状态控制](./230-global-status)。 25 | 26 | ## itemLayout 27 | 28 | 表单项布局结构,详情见[布局结构 itemLayout](./101-item-layout)。 29 | 30 | ## layout 31 | 32 | 表单布局结构,详情见[布局结构 layout](./100-layout)。 33 | 34 | ## layoutColumnGap 35 | 36 | 表单项之间的列间距,详情见[布局结构 layout](./100-layout)。 37 | 38 | ## layoutMinMax 39 | 40 | 定义表单项最小宽度与最大宽度,当 layout 为 autoFill、autoFit 时生效,详情见[布局结构 layout](./100-layout)。 41 | 42 | ## layoutRowGap 43 | 44 | 表单项之间的行间距,详情见[布局结构 layout](./100-layout)。 45 | 46 | ## locale 47 | 48 | 国际化,详情见[国际化 Locale](./700-locale)。 49 | 50 | ## onChange 51 | 52 | 数据变化事件,详情见[onChange 事件](./200-change-event#onchange-事件)。 53 | 54 | ## onItemChange 55 | 56 | 数据变化事件,详情见[onItemChange 事件](./200-change-event#onitemchange-事件)。 57 | 58 | ## prefixCls 59 | 60 | 样式类名前缀,默认为 schema-render。 61 | 62 | ## readonly 63 | 64 | 是否全局只读表单,详情见[全局状态控制](./230-global-status)。 65 | 66 | ## renderers 67 | 68 | 注册渲染器集合,详情见[渲染器 Renderer](./220-renderer)。 69 | 70 | ## schema 71 | 72 | Schema 描述协议。 73 | 74 | ## style 75 | 76 | 根节点样式。 77 | 78 | ## userCtx 79 | 80 | 用户全局上下文数据。 81 | 82 | ## validators 83 | 84 | 全局校验器集合,详情见[Global Validators](./223-validator#global-validators)。 85 | 86 | ## value 87 | 88 | 表单数据。 89 | 90 | ## watch 91 | 92 | 监听数据变化处理联动逻辑,详情见[数据联动 watch 方式](./210-relation#watch-方式)。 93 | -------------------------------------------------------------------------------- /examples/core-react/900-dev-tool.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: 辅助工具 4 | order: 900 5 | hide: true 6 | toc: content 7 | debug: true 8 | --- 9 | 10 | # 错误排查工具 Dev Tool 11 | -------------------------------------------------------------------------------- /examples/core-react/910-playground.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 辅助工具 3 | order: 910 4 | debug: true 5 | --- 6 | 7 | # Playground 8 | -------------------------------------------------------------------------------- /examples/core-react/930-traverse-schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 辅助工具 3 | order: 930 4 | toc: content 5 | debug: true 6 | --- 7 | 8 | # Schema 遍历工具 9 | 10 | ## Schema 协议转换 11 | 12 | 见: [Schema Transform](/core-react/810-schema-transform) 13 | -------------------------------------------------------------------------------- /examples/core-react/931-schema-transform.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 辅助工具 3 | order: 931 4 | toc: content 5 | debug: true 6 | --- 7 | 8 | # Schema 协议转换(从其他 JSON Schema 迁移) 9 | -------------------------------------------------------------------------------- /examples/core-react/950-async.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: 仅开发调试 4 | order: 950 5 | toc: content 6 | debug: true 7 | --- 8 | 9 | # 异步数据 async 10 | 11 | ```tsx 12 | import Core from '@schema-render/core-react' 13 | import Horizontal from './item-layout/Horizontal' 14 | import renderers from './renderers/common' 15 | import { useState } from 'react' 16 | import { Button, Space } from 'antd' 17 | 18 | const schema = { 19 | renderType: 'Root', 20 | properties: { 21 | title: { 22 | title: '标题', 23 | renderType: 'InputText', 24 | }, 25 | content: { 26 | title: '内容', 27 | renderType: 'Textarea', 28 | }, 29 | }, 30 | } as const 31 | 32 | const Demo = () => { 33 | const [value, setValue] = useState({}) 34 | 35 | const handleFetchData = () => { 36 | setTimeout(() => { 37 | setValue({ 38 | title: `新标题::${String(Math.random()).slice(2, 10)}`, 39 | content: `新内容::${String(Math.random()).slice(2, 10)}`, 40 | }) 41 | }, 100) 42 | } 43 | 44 | const handleReset = () => setValue({}) 45 | 46 | return ( 47 | <> 48 | setValue(val)} 53 | itemLayout={Horizontal} 54 | renderers={renderers} 55 | /> 56 | 57 | 60 | 61 | 62 | 63 | ) 64 | } 65 | 66 | export default Demo 67 | ``` 68 | -------------------------------------------------------------------------------- /examples/core-react/images/item-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Barrior/schema-render/33f361b4193fc32b4606a33c4b14dbbf17cce236/examples/core-react/images/item-layout.png -------------------------------------------------------------------------------- /examples/core-react/images/renderer-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Barrior/schema-render/33f361b4193fc32b4606a33c4b14dbbf17cce236/examples/core-react/images/renderer-flow.png -------------------------------------------------------------------------------- /examples/core-react/images/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Barrior/schema-render/33f361b4193fc32b4606a33c4b14dbbf17cce236/examples/core-react/images/schema.png -------------------------------------------------------------------------------- /examples/core-react/item-layout/Horizontal/index.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionCircleOutlined } from '@ant-design/icons' 2 | import type { IOpenItemLayoutParams } from '@schema-render/core-react' 3 | import { Popover } from 'antd' 4 | import classNames from 'classnames' 5 | import type { FC } from 'react' 6 | 7 | import * as styles from './styles' 8 | 9 | const Horizontal: FC = ({ 10 | body, 11 | schema, 12 | validator, 13 | required, 14 | prefixClassNames, 15 | }) => { 16 | return ( 17 |
23 |
24 |
25 | {required && ( 26 | 27 | * 28 | 29 | )} 30 | 31 | {schema.title} 32 | 33 | {!!schema.titleDescription && ( 34 | 35 | 41 | 42 | )} 43 |
44 |
45 | {body} 46 |
47 |
48 | 49 |
50 | {validator.status === 'error' && !!validator.message && ( 51 |
54 | {validator.message} 55 |
56 | )} 57 | {!!schema.description && ( 58 |
59 | {schema.description} 60 |
61 | )} 62 |
63 |
64 | ) 65 | } 66 | 67 | export default Horizontal 68 | -------------------------------------------------------------------------------- /examples/core-react/item-layout/Horizontal/styles.ts: -------------------------------------------------------------------------------- 1 | import { cij } from '@examples/utils/cssinjs' 2 | 3 | export const horizontal = cij`` 4 | 5 | export const main = cij` 6 | display: flex; 7 | align-items: center; 8 | ` 9 | 10 | export const header = cij` 11 | flex: 0 0 100px; 12 | justify-content: flex-end; 13 | display: flex; 14 | align-items: center; 15 | margin-right: 15px; 16 | height: max-content; 17 | ` 18 | 19 | export const title = cij` 20 | word-break: break-all; 21 | ` 22 | 23 | export const mark = cij` 24 | margin-right: 4px; 25 | color: #ff4d4f; 26 | position: relative; 27 | top: 2px; 28 | ` 29 | 30 | export const titleTooltip = cij` 31 | margin-left: 4px; 32 | position: relative; 33 | top: 1px; 34 | ` 35 | 36 | export const body = cij` 37 | flex-grow: 1; 38 | ` 39 | 40 | export const footer = cij` 41 | word-break: break-all; 42 | padding-left: 115px; 43 | 44 | &:empty { 45 | display: none; 46 | } 47 | ` 48 | 49 | export const desc = cij` 50 | margin-top: 6px; 51 | color: #999; 52 | ` 53 | 54 | export const errorMsg = cij` 55 | margin-top: 6px; 56 | color: #ff4d4f; 57 | ` 58 | -------------------------------------------------------------------------------- /examples/core-react/item-layout/Vertical/index.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionCircleOutlined } from '@ant-design/icons' 2 | import type { IOpenItemLayoutParams } from '@schema-render/core-react' 3 | import { Popover } from 'antd' 4 | import classNames from 'classnames' 5 | import type { FC } from 'react' 6 | 7 | import * as styles from './styles' 8 | 9 | const Vertical: FC = ({ 10 | body, 11 | schema, 12 | validator, 13 | required, 14 | prefixClassNames, 15 | }) => { 16 | return ( 17 |
18 |
19 | {required && ( 20 | 21 | * 22 | 23 | )} 24 | 25 | {schema.title} 26 | 27 | {!!schema.titleDescription && ( 28 | 29 | 35 | 36 | )} 37 |
38 | 39 |
{body}
40 | 41 |
42 | {validator.status === 'error' && !!validator.message && ( 43 |
46 | {validator.message} 47 |
48 | )} 49 | {!!schema.description && ( 50 |
51 | {schema.description} 52 |
53 | )} 54 |
55 |
56 | ) 57 | } 58 | 59 | export default Vertical 60 | -------------------------------------------------------------------------------- /examples/core-react/item-layout/Vertical/styles.ts: -------------------------------------------------------------------------------- 1 | import { cij } from '@examples/utils/cssinjs' 2 | 3 | export const header = cij` 4 | margin-bottom: 8px; 5 | ` 6 | 7 | export const title = cij` 8 | word-break: break-all; 9 | ` 10 | 11 | export const mark = cij` 12 | margin-right: 4px; 13 | color: #ff4d4f; 14 | position: relative; 15 | top: 2px; 16 | ` 17 | 18 | export const titleTooltip = cij` 19 | margin-left: 4px; 20 | ` 21 | 22 | export const footer = cij` 23 | word-break: break-all; 24 | 25 | &:empty { 26 | display: none; 27 | } 28 | ` 29 | 30 | export const desc = cij` 31 | margin-top: 8px; 32 | color: #999; 33 | ` 34 | 35 | export const errorMsg = cij` 36 | margin-top: 8px; 37 | color: #ff4d4f; 38 | ` 39 | -------------------------------------------------------------------------------- /examples/core-react/renderers/object/ObjectCollapse.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionCircleOutlined } from '@ant-design/icons' 2 | import type { IObjectSchema, IOpenFormItemParams } from '@schema-render/core-react' 3 | import { RendererIterator } from '@schema-render/core-react' 4 | import { Collapse, Popover } from 'antd' 5 | import type { FC } from 'react' 6 | import { useState } from 'react' 7 | 8 | const ObjectCollapse: FC> = ({ 9 | schema, 10 | path, 11 | objectStyle, 12 | }) => { 13 | const [collapsed, setCollapsed] = useState(false) 14 | 15 | const header = ( 16 | <> 17 | {/* 渲染标题 */} 18 | {schema.title} 19 | 20 | {/* 按标准,应该支持 schema 的 titleDescription 协议功能 */} 21 | {!!schema.titleDescription && ( 22 | 23 | 24 | 25 | )} 26 | 27 | ) 28 | 29 | return ( 30 | setCollapsed(!collapsed)} 35 | > 36 | 44 | {/* 重点:调用 RendererIterator 渲染子节点;容器元素需要添加 objectStyle 样式,以实现整体一致的栅格布局 */} 45 |
46 | 47 |
48 |
49 |
50 | ) 51 | } 52 | 53 | export default { 54 | // 通过 formItem 模式注册 55 | formItem: ObjectCollapse, 56 | } 57 | -------------------------------------------------------------------------------- /examples/core-react/renderers/object/ObjectNormal.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionCircleOutlined } from '@ant-design/icons' 2 | import type { IObjectSchema, IOpenFormItemParams } from '@schema-render/core-react' 3 | import { RendererIterator } from '@schema-render/core-react' 4 | import { Popover } from 'antd' 5 | import type { FC } from 'react' 6 | 7 | const style = { 8 | background: '#efefef', 9 | fontWeight: 'bold', 10 | padding: '7px 15px', 11 | borderRadius: 4, 12 | marginBottom: 15, 13 | } 14 | 15 | const ObjectNormal: FC> = ({ 16 | schema, 17 | path, 18 | objectStyle, 19 | }) => { 20 | return ( 21 | <> 22 | {/* 渲染标题块内容 */} 23 |
24 | {/* 渲染标题 */} 25 | {schema.title} 26 | 27 | {/* 按标准,应该支持 schema 的 titleDescription 协议功能 */} 28 | {!!schema.titleDescription && ( 29 | 30 | 31 | 32 | )} 33 |
34 | 35 | {/* 重点:调用 RendererIterator 渲染子节点;容器元素需要添加 objectStyle 样式,以实现整体一致的栅格布局 */} 36 |
37 | 38 |
39 | 40 | ) 41 | } 42 | 43 | export default { 44 | // 通过 formItem 模式注册 45 | formItem: ObjectNormal, 46 | } 47 | -------------------------------------------------------------------------------- /examples/core-react/renderers/object/index.tsx: -------------------------------------------------------------------------------- 1 | import type { IRenderers } from '@schema-render/core-react' 2 | import { Input } from 'antd' 3 | 4 | import ObjectCollapse from './ObjectCollapse' 5 | import Object from './ObjectNormal' 6 | 7 | const renderers: IRenderers = { 8 | // 注册「常规的」对象类型渲染器 9 | Object, 10 | 11 | // 注册「可折叠的」对象类型渲染器 12 | ObjectCollapse, 13 | 14 | InputText: { 15 | component: ({ schema, value, disabled, onChange }) => { 16 | return ( 17 | onChange(e.target.value)} 23 | /> 24 | ) 25 | }, 26 | }, 27 | } 28 | 29 | export default renderers 30 | -------------------------------------------------------------------------------- /examples/core-react/schemas/object.ts: -------------------------------------------------------------------------------- 1 | import type { IRootSchema } from '@schema-render/core-react' 2 | 3 | export default { 4 | renderType: 'Root', 5 | properties: { 6 | title: { 7 | renderType: 'InputText', 8 | title: '一级标题', 9 | titleDescription: '标题的一些说明', 10 | required: true, 11 | }, 12 | content: { 13 | renderType: 'InputText', 14 | title: '一级内容', 15 | }, 16 | o1: { 17 | renderType: 'Object', 18 | title: '仅加粗标题的对象 Object', 19 | titleDescription: '可以加一些说明', 20 | properties: { 21 | title: { 22 | renderType: 'InputText', 23 | title: '二级标题', 24 | titleDescription: '标题的一些说明', 25 | required: true, 26 | }, 27 | content: { 28 | renderType: 'InputText', 29 | title: '二级内容', 30 | }, 31 | }, 32 | }, 33 | o2: { 34 | renderType: 'ObjectCollapse', 35 | title: '可折叠的对象 ObjectCollapse', 36 | titleDescription: '可以加一些说明', 37 | properties: { 38 | title: { 39 | renderType: 'InputText', 40 | title: '二级标题', 41 | titleDescription: '标题的一些说明', 42 | required: true, 43 | }, 44 | content: { 45 | renderType: 'InputText', 46 | title: '二级内容', 47 | }, 48 | }, 49 | }, 50 | }, 51 | } as IRootSchema 52 | -------------------------------------------------------------------------------- /examples/core-react/schemas/validator-shape.ts: -------------------------------------------------------------------------------- 1 | import type { IRootSchema } from '@schema-render/core-react' 2 | 3 | export default { 4 | renderType: 'Root', 5 | title: '校验器 - shape', 6 | properties: { 7 | title: { 8 | renderType: 'InputText', 9 | renderOptions: { 10 | minLength: 8, 11 | maxLength: 20, 12 | placeholder: '请输入标题,8-20个字符', 13 | }, 14 | title: '标题', 15 | required: true, 16 | rules: [ 17 | { min: 8, message: '至少输入 8 个字符' }, 18 | { pattern: '^[a-zA-Z]*$', message: '只能填入英文字母' }, 19 | ], 20 | }, 21 | object: { 22 | renderType: 'Object', 23 | title: '对象类型', 24 | titleDescription: '对象类型 Tooltip', 25 | properties: { 26 | title: { 27 | renderType: 'InputText', 28 | title: '内容', 29 | required: true, 30 | }, 31 | }, 32 | }, 33 | // 实验 Object 数据类型 Rules 规则 34 | objectDataType: { 35 | renderType: 'ComposeODT', 36 | title: '寄件信息', 37 | // required: true, 38 | rules: [ 39 | { type: 'object', required: true, message: '请完善快递信息' }, 40 | { 41 | type: 'object', 42 | shape: { 43 | payType: { type: 'string', required: true, message: '请选择是寄付还是到付' }, 44 | address: { type: 'string', required: true, message: '请完善收件地址' }, 45 | }, 46 | }, 47 | ], 48 | }, 49 | // 实验 Array 数据类型 Rules 规则 50 | arrayDataType: { 51 | renderType: 'ComposeADT', 52 | title: 'Array 信息', 53 | rules: [ 54 | { type: 'array', required: true, message: '请完善列表信息' }, 55 | { type: 'array', min: 2, message: '列表数据最少 2 项' }, 56 | { type: 'array', max: 10, message: '列表数据最多 10 项' }, 57 | { 58 | type: 'array', 59 | shape: { 60 | payType: { type: 'string', required: true, message: '请选择是寄付还是到付' }, 61 | address: { type: 'string', required: true, message: '请完善收件地址' }, 62 | }, 63 | }, 64 | ], 65 | }, 66 | }, 67 | } as IRootSchema 68 | -------------------------------------------------------------------------------- /examples/form-render-react/001-intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 1 3 | toc: content 4 | --- 5 | 6 | # 介绍 7 | 8 | `FormRender` 是基于 Core + Antd 封装的表单渲染库,内置了布局结构以及常用的表单渲染器集合,以达到 **开箱即用** 。 9 | 10 | 特性如下: 11 | 12 | - 纵向与横向布局结构,详见[布局结构 itemLayout](./100-item-layout.md) 13 | - 布局结构可配置 label 宽度等内容,详见[标题设置 label](./200-label.md) 14 | - 行为操作特性,如提交、重置,详见[行为操作 actions](./300-actions.md) 15 | - 常用的表单渲染器集合,详见[内置渲染器 renderers](./500-builtin-renderers.md) 16 | 17 | ## 安装 18 | 19 | ```bash 20 | npm install @schema-render/form-render-react --save 21 | ``` 22 | 23 | 如果没有安装 antd,还需安装它 24 | 25 | ```bash 26 | npm install antd --save 27 | ``` 28 | 29 | ## 使用 30 | 31 | ```jsx 32 | /** 33 | * defaultShowCode: true 34 | */ 35 | import SyntaxHighlighter from '@examples/components/SyntaxHighlighter' 36 | import { useState } from 'react' 37 | import { message } from 'antd' 38 | 39 | // 引入 FormRender 40 | import FormRender from '@schema-render/form-render-react' 41 | 42 | // 定义 Schema 描述 43 | const schema = { 44 | renderType: 'Root', 45 | properties: { 46 | username: { 47 | title: '用户名', 48 | renderType: 'InputText', 49 | description: '用户名最多20个字符', 50 | renderOptions: { 51 | maxLength: 20, 52 | }, 53 | required: true, 54 | }, 55 | password: { 56 | title: '密码', 57 | description: '请输入6~10个字符,只能输入英文字母和数字', 58 | renderType: 'Password', 59 | renderOptions: { 60 | minLength: 6, 61 | maxLength: 20, 62 | }, 63 | required: true, 64 | rules: [ 65 | { min: 6, message: '请输入至少 6 个字符' }, 66 | { max: 20, message: '最多只能 20 个字符' }, 67 | { pattern: '^[A-Za-z0-9]+$', message: '只能输入英文字母和数字' }, 68 | ], 69 | }, 70 | }, 71 | } 72 | 73 | const Demo = () => { 74 | const [value, setValue] = useState({}) 75 | 76 | return ( 77 |
78 | { 84 | message.success('提交成功') 85 | }} 86 | /> 87 | 88 |
89 | ) 90 | } 91 | 92 | export default Demo 93 | ``` 94 | -------------------------------------------------------------------------------- /examples/form-render-react/002-ssr.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 1 3 | toc: content 4 | --- 5 | 6 | # SSR 7 | 8 | ## 默认不支持 9 | 10 | 由于样式原因,FormRender 没有很好的支持 SSR,如果是 Next.js 项目,可以动态导入并配置 ssr 为不启用,如下示例。 11 | 12 | ```jsx | pure 13 | import dynamic from 'next/dynamic' 14 | 15 | const FormRender = dynamic(() => import('@schema-render/form-render-react'), { 16 | ssr: false, 17 | }) 18 | ``` 19 | 20 | ## 支持 SSR 版本 21 | 22 | 为了支持 SSR,FormRender 提供了 `npm tag` 为 `ssr` 的版本,内部样式由 [CSS Modules](https://github.com/css-modules/css-modules) 完成,故需要配置项目编译软件包。 23 | 24 | 以 Next.js 为例,Next.js 默认支持 CSS Modules。 25 | 26 | ### 安装或配置 27 | 28 | 安装 29 | 30 | ```bash 31 | npm install @schema-render/core-react@ssr @schema-render/form-render-react@ssr --save 32 | ``` 33 | 34 | 或者配置 `package.json` 35 | 36 | ```json 37 | "dependencies": { 38 | "@schema-render/core-react": "ssr", 39 | "@schema-render/form-render-react": "ssr" 40 | } 41 | ``` 42 | 43 | ### 配置 next.config.js 44 | 45 | 配置 `next.config.js` 编译软件包。 46 | 47 | ``` 48 | transpilePackages: [ 49 | '@schema-render/core-react', 50 | '@schema-render/form-render-react', 51 | ] 52 | ``` 53 | 54 | ### 配置 Antd 55 | 56 | 同时,需要配置 Antd 支持服务端渲染,可参考 https://ant-design.antgroup.com/docs/react/use-with-next-cn 57 | -------------------------------------------------------------------------------- /examples/form-render-react/700-locale.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 700 3 | toc: content 4 | --- 5 | 6 | # 国际化 locale 7 | 8 | `FormRender` 默认文案是中文,如果需要使用其他语言,只需配置 `locale` 覆盖默认的语言,示例如下。 9 | 10 | ```tsx 11 | /** 12 | * defaultShowCode: true 13 | */ 14 | import FormRender from '@schema-render/form-render-react' 15 | import type { IFormRenderRootSchema } from '@schema-render/form-render-react' 16 | 17 | // 引入英文语言包 18 | import enUS from '@schema-render/form-render-react/dist/esm/locale/en_US' 19 | 20 | const schema: IFormRenderRootSchema = { 21 | renderType: 'Root', 22 | properties: { 23 | username: { 24 | title: 'username', 25 | renderType: 'InputText', 26 | required: true, 27 | }, 28 | password: { 29 | title: 'password', 30 | renderType: 'Password', 31 | required: true, 32 | }, 33 | }, 34 | } 35 | 36 | const Demo = () => { 37 | return ( 38 | 44 | ) 45 | } 46 | 47 | export default Demo 48 | ``` 49 | 50 | 目前支持以下语言: 51 | 52 | | **语言** | **文件名** | 53 | | -------- | ---------- | 54 | | 中文 | zh_CN | 55 | | 英文 | en_US | 56 | 57 | - 如果找不到你需要的语言包,欢迎参考 [中文语言包](https://github.com/Barrior/schema-render/tree/main/packages/form-render-react/src/locale/zh_CN.ts) 创建一个新的语言包,并给我们发一个 Pull Request。 58 | - 或者配置相同的语言包数据格式传值给 `locale` 属性。 59 | -------------------------------------------------------------------------------- /examples/form-render-react/853-validate-on-blur.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: 案例集锦 4 | order: 853 5 | toc: content 6 | --- 7 | 8 | # 失去焦点时校验 onBlur 9 | 10 | 业务场景 11 | 12 | - 内置 InputText、TextArea 等渲染器,存在最小长度校验时,希望在输入的时候不校验数据,在失去焦点(`onBlur`)时才校验数据正确性。 13 | 14 | 实现方案 15 | 16 | - 内置 InputText、TextArea 等渲染器,通过在 renderOptions 上设置 `validateOnBlur: true` 属性可实现上述需求。 17 | - 设置该属性的内置渲染器有: 18 | - `InputText` 19 | - `InputNumber` 20 | - `Password` 21 | - `TextArea` 22 | 23 | ```tsx 24 | import { useState } from 'react' 25 | import SyntaxHighlighter from '@examples/components/SyntaxHighlighter' 26 | import FormRender from '@schema-render/form-render-react' 27 | import type { IFormRenderRootSchema } from '@schema-render/form-render-react' 28 | 29 | const schema: IFormRenderRootSchema = { 30 | renderType: 'Root', 31 | properties: { 32 | title: { 33 | title: '标题', 34 | renderType: 'InputText', 35 | renderOptions: { 36 | validateOnBlur: true, 37 | }, 38 | required: true, 39 | rules: [{ min: 5, message: '至少需要输入 5 个字符' }], 40 | }, 41 | amount: { 42 | title: '总额', 43 | renderType: 'InputNumber', 44 | renderOptions: { 45 | validateOnBlur: true, 46 | }, 47 | required: true, 48 | rules: [{ min: 50, message: '总额不能少 50' }], 49 | }, 50 | content: { 51 | title: '内容', 52 | renderType: 'TextArea', 53 | renderOptions: { 54 | validateOnBlur: true, 55 | }, 56 | required: true, 57 | rules: [{ min: 5, message: '至少需要输入 5 个字符' }], 58 | }, 59 | password: { 60 | title: '密码框', 61 | renderType: 'Password', 62 | renderOptions: { 63 | validateOnBlur: true, 64 | }, 65 | required: true, 66 | rules: [{ min: 5, message: '至少需要输入 5 个字符' }], 67 | }, 68 | }, 69 | } 70 | 71 | const Demo = () => { 72 | const [value, setValue] = useState({}) 73 | 74 | return ( 75 |
76 | 77 | 78 |
79 | ) 80 | } 81 | 82 | export default Demo 83 | ``` 84 | -------------------------------------------------------------------------------- /examples/form-render-react/854-scroll-to-first-error.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: 案例集锦 4 | order: 854 5 | toc: content 6 | --- 7 | 8 | # 滚动到第一个错误项 9 | 10 | 业务场景 11 | 12 | - 当表单项过多时容器存在滚动条,点击提交按钮时应该定位到第一个错误表单项以提醒用户。 13 | 14 | 实现方案 15 | 16 | - 通过 `validate` 校验表单数据,得到错误项数据,再通过 `scrollTo` 滚动到指定位置。 17 | 18 | ```tsx 19 | /** 20 | * defaultShowCode: true 21 | */ 22 | import { useState, useRef } from 'react' 23 | import SyntaxHighlighter from '@examples/components/SyntaxHighlighter' 24 | import FormRender from '@schema-render/form-render-react' 25 | import type { 26 | IFormRenderRootSchema, 27 | IFormRenderRef, 28 | } from '@schema-render/form-render-react' 29 | 30 | function createSchema() { 31 | const schema: IFormRenderRootSchema = { 32 | renderType: 'Root', 33 | properties: {}, 34 | } 35 | 36 | for (let i = 1; i <= 30; i++) { 37 | const field = `field_${i}` 38 | schema.properties[field] = { 39 | title: `标题_${i}`, 40 | renderType: 'InputText', 41 | renderOptions: { 42 | validateOnBlur: true, 43 | }, 44 | required: true, 45 | rules: [{ min: 5, message: '至少需要输入 5 个字符' }], 46 | } 47 | } 48 | 49 | return schema 50 | } 51 | 52 | const schema = createSchema() 53 | 54 | const Demo = () => { 55 | const [value, setValue] = useState({}) 56 | const formRenderRef = useRef(null) 57 | 58 | const handleSubmit = async () => { 59 | const res = await formRenderRef.current?.validate() 60 | if (res?.hasError) { 61 | formRenderRef.current?.scrollTo(res.errorList[0].path, { 62 | // 76 是官网文档页头高度 63 | gap: -76, 64 | positionedElement: window, 65 | }) 66 | } 67 | } 68 | 69 | return ( 70 |
71 | 79 | 80 |
81 | ) 82 | } 83 | 84 | export default Demo 85 | ``` 86 | -------------------------------------------------------------------------------- /examples/form-render-react/881-relation-select-multiple.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: 案例集锦 4 | order: 881 5 | toc: content 6 | --- 7 | 8 | # 联动 - SelectMultiple 选中值 9 | 10 | 与 Checkbox 渲染器一样,不过字段名是 `selectedOptions`。 11 | 12 | ## Schema 方式实现 13 | 14 | 【可选城市】依赖【可选城市范围】的数据 15 | 16 | ```jsx 17 | import { useState } from 'react' 18 | import SyntaxHighlighter from '@examples/components/SyntaxHighlighter' 19 | import FormRender from '@schema-render/form-render-react' 20 | 21 | function createSchema(available_cities_options = []) { 22 | const schema = { 23 | renderType: 'Root', 24 | properties: { 25 | cities: { 26 | title: '设置可选城市范围', 27 | renderType: 'SelectMultiple', 28 | renderOptions: { 29 | options: [ 30 | { label: '成都', value: 'chengdu' }, 31 | { label: '杭州', value: 'hangzhou' }, 32 | { label: '深圳', value: 'shenzhen' }, 33 | { label: '北京', value: 'beijing' }, 34 | ], 35 | }, 36 | }, 37 | available_cities: { 38 | title: '设置可选城市', 39 | renderType: 'Checkbox', 40 | renderOptions: { 41 | options: available_cities_options, 42 | }, 43 | }, 44 | }, 45 | } 46 | return schema 47 | } 48 | 49 | const Demo = () => { 50 | const [value, setValue] = useState({}) 51 | const [schema, setSchema] = useState(() => createSchema()) 52 | 53 | return ( 54 |
55 | { 63 | // 基于下拉选中项,重新创建 schema 渲染 64 | setSchema(createSchema(event.extra.selectedOptions)) 65 | 66 | // 已选中的值不在范围内,则清除 67 | if (!event.value.includes(formData.available_cities)) { 68 | formData.available_cities = undefined 69 | } 70 | }, 71 | }} 72 | /> 73 | 74 |
75 | ) 76 | } 77 | 78 | export default Demo 79 | ``` 80 | -------------------------------------------------------------------------------- /examples/form-render-react/900-renderers-warning.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: 仅开发调试 4 | order: 900 5 | toc: content 6 | debug: true 7 | --- 8 | 9 | # 渲染器校验 10 | 11 | ## 校验状态样式 12 | 13 | 支持 `warning`、`error` 样式 14 | 15 | ```tsx 16 | import { useState } from 'react' 17 | import SyntaxHighlighter from '@examples/components/SyntaxHighlighter' 18 | import FormRender from '@schema-render/form-render-react' 19 | 20 | const schema = { 21 | renderType: 'Root', 22 | properties: { 23 | account: { 24 | title: '用户名', 25 | renderType: 'InputText', 26 | required: true, 27 | }, 28 | password: { 29 | title: '密码', 30 | renderType: 'Password', 31 | required: true, 32 | }, 33 | }, 34 | } as const 35 | 36 | const Demo = () => { 37 | const [value, setValue] = useState({}) 38 | 39 | return ( 40 |
41 | 42 | 43 |
44 | ) 45 | } 46 | 47 | export default Demo 48 | ``` 49 | -------------------------------------------------------------------------------- /examples/form-render-react/renderers/BankSelect.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenComponentParams } from '@schema-render/core-react' 2 | import React from 'react' 3 | 4 | const style = { 5 | display: 'flex', 6 | alignItems: 'center', 7 | padding: '4px 10px', 8 | height: 32, 9 | boxSizing: 'border-box', 10 | border: '1px solid #ececec', 11 | borderRadius: 6, 12 | cursor: 'pointer', 13 | fontSize: 14, 14 | } 15 | 16 | const BankSelect: React.FC> = ({ 17 | value, 18 | sPath, 19 | onChange, 20 | }) => { 21 | const handleBankSelect = () => { 22 | const random = String(Math.random()).slice(2, 6) 23 | const bankData = { 24 | bank_id: `id_${random}`, 25 | bank_account: `111222333${random}`, 26 | bank_name: `招财猫银行${random}`, 27 | bank_branch_name: `招财猫成都${random}支行`, 28 | } 29 | // 银行账户数据通过 extra 属性透传到外部用于联动处理 30 | onChange(bankData[sPath as never], { 31 | extra: bankData, 32 | }) 33 | } 34 | 35 | return ( 36 |
37 | {/* 展示当前表单项值 */} 38 | {value ?
{value}
:
选择银行账户
} 39 |
40 | ) 41 | } 42 | 43 | export default { 44 | component: BankSelect, 45 | } 46 | -------------------------------------------------------------------------------- /examples/form-render-react/renderers/index.tsx: -------------------------------------------------------------------------------- 1 | import BankSelect from './BankSelect' 2 | 3 | export default { 4 | BankSelect, 5 | } 6 | -------------------------------------------------------------------------------- /examples/form-render-react/styles/index.ts: -------------------------------------------------------------------------------- 1 | import { cij } from '@examples/utils/cssinjs' 2 | 3 | export const builtinRenderers = cij` 4 | min-width: 580px; 5 | font-size: 15px !important; 6 | 7 | .schema-render-form-actions { 8 | margin-left: 0 !important; 9 | display: flex; 10 | justify-content: center; 11 | } 12 | ` 13 | -------------------------------------------------------------------------------- /examples/global.scss: -------------------------------------------------------------------------------- 1 | @use './dumi'; 2 | 3 | .example-layout-cols-2 { 4 | display: flex; 5 | 6 | > * { 7 | width: 50%; 8 | height: max-content; 9 | 10 | &:last-child { 11 | margin-left: 20px; 12 | } 13 | } 14 | } 15 | 16 | .api-table-col-3 { 17 | table { 18 | th:nth-child(1) { 19 | width: 150px; 20 | } 21 | 22 | th:nth-child(3) { 23 | width: 150px; 24 | } 25 | } 26 | } 27 | 28 | .section-title { 29 | display: flex; 30 | align-items: center; 31 | margin: 10px 0 15px; 32 | line-height: 1; 33 | font-weight: bold; 34 | 35 | &::before { 36 | content: ''; 37 | display: inline-block; 38 | width: 4px; 39 | height: 1em; 40 | margin-right: 10px; 41 | background-color: #1677ff; 42 | } 43 | } 44 | 45 | // 应用场景有:滚动到指定位置 46 | .focus-out { 47 | animation: focus-out 1s ease-out; 48 | border-radius: 6px; 49 | } 50 | 51 | @keyframes focus-out { 52 | 0% { 53 | padding: 20px; 54 | background: rgb(254 238 189); 55 | } 56 | 57 | 100% { 58 | padding: 0; 59 | background: transparent; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/search-react/002-ssr.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 1 3 | toc: content 4 | --- 5 | 6 | # SSR 7 | 8 | ## 默认不支持 9 | 10 | 由于样式原因,Search 没有很好的支持 SSR,如果是 Next.js 项目,可以动态导入并配置 ssr 为不启用,如下示例。 11 | 12 | ```jsx | pure 13 | import dynamic from 'next/dynamic' 14 | 15 | const Search = dynamic(() => import('@schema-render/search-react'), { 16 | ssr: false, 17 | }) 18 | ``` 19 | 20 | ## 支持 SSR 版本 21 | 22 | 为了支持 SSR,Search 提供了 `npm tag` 为 `ssr` 的版本,内部样式由 [CSS Modules](https://github.com/css-modules/css-modules) 完成,故需要配置项目编译软件包。 23 | 24 | 以 Next.js 为例,Next.js 默认支持 CSS Modules。 25 | 26 | ### 安装或配置 27 | 28 | 安装 29 | 30 | ```bash 31 | npm install \ 32 | @schema-render/core-react@ssr \ 33 | @schema-render/form-render-react@ssr \ 34 | @schema-render/search-react@ssr --save 35 | ``` 36 | 37 | 或者配置 `package.json` 38 | 39 | ```json 40 | "dependencies": { 41 | "@schema-render/core-react": "ssr", 42 | "@schema-render/form-render-react": "ssr", 43 | "@schema-render/search-react": "ssr", 44 | } 45 | ``` 46 | 47 | ### 配置 next.config.js 48 | 49 | 配置 `next.config.js` 编译软件包。 50 | 51 | ```js 52 | transpilePackages: [ 53 | '@schema-render/core-react', 54 | '@schema-render/form-render-react', 55 | '@schema-render/search-react', 56 | ] 57 | ``` 58 | 59 | ### 配置 Antd 60 | 61 | 同时,需要配置 Antd 支持服务端渲染,可参考 https://ant-design.antgroup.com/docs/react/use-with-next-cn 62 | -------------------------------------------------------------------------------- /examples/search-react/700-locale.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 700 3 | toc: content 4 | --- 5 | 6 | # 国际化 locale 7 | 8 | `Search` 默认文案是中文,如果需要使用其他语言,只需配置 `locale` 覆盖默认的语言,示例如下。 9 | 10 | ```tsx 11 | /** 12 | * defaultShowCode: true 13 | */ 14 | import Search from '@schema-render/search-react' 15 | import type { IFormRenderRootSchema } from '@schema-render/form-render-react' 16 | 17 | // 引入英文语言包 18 | import enUS from '@schema-render/search-react/dist/esm/locale/en_US' 19 | 20 | const schema: IFormRenderRootSchema = { 21 | renderType: 'Root', 22 | properties: { 23 | supplier_name: { 24 | title: 'Supplier Name', 25 | renderType: 'InputText', 26 | }, 27 | supplier_code: { 28 | title: 'Supplier Code', 29 | renderType: 'InputText', 30 | }, 31 | bill_no: { 32 | title: 'Bill No', 33 | renderType: 'InputText', 34 | }, 35 | bill_date: { 36 | title: 'Bill Date', 37 | renderType: 'DateRangePicker', 38 | }, 39 | bill_status: { 40 | title: 'Bill Status', 41 | renderType: 'Select', 42 | renderOptions: { 43 | options: [ 44 | { label: 'submitted', value: 1 }, 45 | { label: 'pending submit', value: 2 }, 46 | { label: 'pending approval', value: 3 }, 47 | ], 48 | }, 49 | }, 50 | operator: { 51 | title: 'Operator', 52 | renderType: 'InputText', 53 | }, 54 | goods_name: { 55 | title: 'Goods Name', 56 | renderType: 'InputText', 57 | }, 58 | goods_code: { 59 | title: 'Goods Code', 60 | renderType: 'InputText', 61 | }, 62 | }, 63 | } 64 | 65 | const Demo = () => { 66 | return ( 67 | 73 | ) 74 | } 75 | 76 | export default Demo 77 | ``` 78 | 79 | 目前支持以下语言: 80 | 81 | | **语言** | **文件名** | 82 | | -------- | ---------- | 83 | | 中文 | zh_CN | 84 | | 英文 | en_US | 85 | 86 | - 如果找不到你需要的语言包,欢迎参考 [中文语言包](https://github.com/Barrior/schema-render/tree/main/packages/search-react/src/locale/zh_CN.ts) 创建一个新的语言包,并给我们发一个 Pull Request。 87 | - 或者配置相同的语言包数据格式传值给 `locale` 属性。 88 | -------------------------------------------------------------------------------- /examples/search-react/800-api-reference.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: API 手册 4 | order: 800 5 | toc: content 6 | --- 7 | 8 | # API Reference 9 | 10 | `Search` 除了支持 `FormRender` 的 API 外,还支持以下 API。 11 | 12 | | **参数** | **描述** | **类型** | **默认值** | 13 | | ----------------------- | ---------------------------------------------------------- | ------------------------------------- | --------------------------------- | 14 | | **locale** | 国际化语言数据 | `object` | - | 15 | | **actions** | 配置操作按钮展示项及其顺序 | `string[]` | `["reset", "submit", "collapse"]` | 16 | | **defaultCollapsed** | 是否默认折叠 | `boolean` | `true` | 17 | | **collapsedRows** | 折叠行数 | `number` | `2` | 18 | | **calcCollapsedNumber** | 计算折叠时展示的表单项个数的算法,`container` 是根节点元素 | `(container?: HTMLElement) => number` | - | 19 | | **onToggleCollapsed** | 「折叠/展开」切换事件 | `(isCollapsed: boolean) => void` | - | 20 | -------------------------------------------------------------------------------- /examples/search-react/helpers/columns.ts: -------------------------------------------------------------------------------- 1 | import type { TableProps } from 'antd' 2 | 3 | const columns: TableProps['columns'] = [ 4 | { 5 | title: '供应商名称', 6 | dataIndex: 'supplier_name', 7 | align: 'center', 8 | }, 9 | { 10 | title: '供应商编码', 11 | dataIndex: 'supplier_code', 12 | align: 'center', 13 | }, 14 | { 15 | title: '单据编号', 16 | dataIndex: 'bill_no', 17 | align: 'center', 18 | }, 19 | { 20 | title: '单据类型', 21 | dataIndex: 'bill_type', 22 | align: 'center', 23 | }, 24 | { 25 | title: '单据日期', 26 | dataIndex: 'bill_date', 27 | align: 'center', 28 | }, 29 | { 30 | title: '单据状态', 31 | dataIndex: 'bill_status', 32 | align: 'center', 33 | }, 34 | { 35 | title: '制单人', 36 | dataIndex: 'operator', 37 | align: 'center', 38 | }, 39 | { 40 | title: '商品名称', 41 | dataIndex: 'goods_name', 42 | align: 'center', 43 | }, 44 | { 45 | title: '商品编码', 46 | dataIndex: 'goods_code', 47 | align: 'center', 48 | }, 49 | { 50 | title: '商品分类', 51 | dataIndex: 'goods_category', 52 | align: 'center', 53 | }, 54 | { 55 | title: '商品日期', 56 | dataIndex: 'goods_date', 57 | align: 'center', 58 | }, 59 | ] 60 | 61 | export default columns 62 | -------------------------------------------------------------------------------- /examples/search-react/helpers/createDataSource.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | 3 | export default function createDataSource() { 4 | const dataSource: object[] = [] 5 | 6 | for (let i = 0; i < 10; i++) { 7 | const random = String(Math.random()).slice(2, 6) 8 | const goods_category = Math.random() > 0.5 ? 1 : 2 9 | const date = dayjs().format('YYYY-MM-DD HH:mm:ss') 10 | 11 | dataSource.push({ 12 | key: Math.random(), 13 | supplier_name: `${random}有限公司`, 14 | supplier_code: random, 15 | bill_no: `B${Date.now()}`, 16 | bill_type: Math.random() > 0.5 ? '采购单' : '退货单', 17 | bill_date: date, 18 | bill_status: Math.random() > 0.5 ? '已提交' : '待提交', 19 | operator: Math.random() > 0.5 ? '张三' : '李四', 20 | goods_name: goods_category === 1 ? '红玫瑰10枝' : '鲜活鲈鱼', 21 | goods_code: `G${Date.now()}`, 22 | goods_category: goods_category === 1 ? '水果鲜花' : '海鲜水产', 23 | goods_date: date, 24 | }) 25 | } 26 | 27 | return dataSource 28 | } 29 | -------------------------------------------------------------------------------- /examples/search-react/renderers/BankSelect.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenComponentParams } from '@schema-render/core-react' 2 | import React from 'react' 3 | 4 | const style = { 5 | display: 'flex', 6 | alignItems: 'center', 7 | padding: '4px 10px', 8 | height: 32, 9 | boxSizing: 'border-box', 10 | border: '1px solid #d9d9d9', 11 | borderRadius: 6, 12 | cursor: 'pointer', 13 | fontSize: 14, 14 | } 15 | 16 | const BankSelect: React.FC> = ({ 17 | value, 18 | sPath, 19 | onChange, 20 | }) => { 21 | const handleBankSelect = () => { 22 | const random = String(Math.random()).slice(2, 6) 23 | const bankData = { 24 | bank_id: `id_${random}`, 25 | bank_account: `111222333${random}`, 26 | bank_name: `招财猫银行${random}`, 27 | bank_branch_name: `招财猫成都${random}支行`, 28 | } 29 | // 银行账户数据通过 extra 属性透传到外部用于联动处理 30 | onChange(bankData[sPath as never], { 31 | extra: bankData, 32 | }) 33 | } 34 | 35 | return ( 36 |
37 | {/* 展示当前表单项值 */} 38 | {value ?
{value}
:
选择银行账户
} 39 |
40 | ) 41 | } 42 | 43 | export default { 44 | component: BankSelect, 45 | } 46 | -------------------------------------------------------------------------------- /examples/search-react/renderers/ObjectSectionTitle.tsx: -------------------------------------------------------------------------------- 1 | import type { IObjectSchema, IOpenFormItemParams } from '@schema-render/core-react' 2 | import { RendererIterator } from '@schema-render/core-react' 3 | import type { FC } from 'react' 4 | 5 | const ObjectSectionTitle: FC> = ({ 6 | schema, 7 | path, 8 | objectStyle, 9 | }) => { 10 | return ( 11 | <> 12 |
{schema.title}
13 |
14 | 15 |
16 | 17 | ) 18 | } 19 | 20 | export default { 21 | formItem: ObjectSectionTitle as never, 22 | } 23 | -------------------------------------------------------------------------------- /examples/search-react/renderers/SectionTitle.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenFormItemParams } from '@schema-render/core-react' 2 | import type { FC } from 'react' 3 | 4 | const SectionTitle: FC> = ({ schema }) => { 5 | return
{schema.title}
6 | } 7 | 8 | export default { 9 | formItem: SectionTitle as never, 10 | } 11 | -------------------------------------------------------------------------------- /examples/search-table-react/002-ssr.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 1 3 | toc: content 4 | --- 5 | 6 | # SSR 7 | 8 | ## 默认不支持 9 | 10 | 由于样式原因,SearchTable 没有很好的支持 SSR,如果是 Next.js 项目,可以动态导入并配置 ssr 为不启用,如下示例。 11 | 12 | ```jsx | pure 13 | import dynamic from 'next/dynamic' 14 | 15 | const SearchTable = dynamic(() => import('@schema-render/search-table-react'), { 16 | ssr: false, 17 | }) 18 | ``` 19 | 20 | ## 支持 SSR 版本 21 | 22 | 为了支持 SSR,SearchTable 提供了 `npm tag` 为 `ssr` 的版本,内部样式由 [CSS Modules](https://github.com/css-modules/css-modules) 完成,故需要配置项目编译软件包。 23 | 24 | 以 Next.js 为例,Next.js 默认支持 CSS Modules。 25 | 26 | ### 安装或配置 27 | 28 | 安装 29 | 30 | ```bash 31 | npm install \ 32 | @schema-render/core-react@ssr \ 33 | @schema-render/form-render-react@ssr \ 34 | @schema-render/search-react@ssr \ 35 | @schema-render/search-table-react@ssr --save 36 | ``` 37 | 38 | 或者配置 `package.json` 39 | 40 | ```json 41 | "dependencies": { 42 | "@schema-render/core-react": "ssr", 43 | "@schema-render/form-render-react": "ssr", 44 | "@schema-render/search-react": "ssr", 45 | "@schema-render/search-table-react": "ssr", 46 | } 47 | ``` 48 | 49 | ### 配置 next.config.js 50 | 51 | 配置 `next.config.js` 编译软件包。 52 | 53 | ```js 54 | transpilePackages: [ 55 | '@schema-render/core-react', 56 | '@schema-render/form-render-react', 57 | '@schema-render/search-react', 58 | '@schema-render/search-table-react', 59 | ] 60 | ``` 61 | 62 | ### 配置 Antd 63 | 64 | 同时,需要配置 Antd 支持服务端渲染,可参考 https://ant-design.antgroup.com/docs/react/use-with-next-cn 65 | -------------------------------------------------------------------------------- /examples/search-table-react/700-locale.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 700 3 | toc: content 4 | --- 5 | 6 | # 国际化 locale 7 | 8 | `SearchTable` 默认文案是中文,如果需要使用其他语言,只需配置 `locale` 覆盖默认的语言,示例如下。 9 | 10 | ```tsx 11 | /** 12 | * defaultShowCode: true 13 | */ 14 | import schemaEn from './helpers/schema-en' 15 | import columnsEn from './helpers/columns-en' 16 | import createDataSourceEn from './helpers/createDataSource-en' 17 | import SearchTable from '@schema-render/search-table-react' 18 | import { sleep } from '@examples/utils' 19 | 20 | // 引入英文语言包 21 | import enUS from '@schema-render/search-table-react/dist/esm/locale/en_US' 22 | 23 | const Demo = () => { 24 | return ( 25 | [ 41 | { text: 'Edit' }, 42 | { text: 'View' }, 43 | { text: 'Delete', danger: true }, 44 | ], 45 | actionItemsColumnData: { 46 | width: 150, 47 | }, 48 | summaryText: 'Total', 49 | }} 50 | request={async (searchParams) => { 51 | // 模拟请求接口获取表格数据 52 | await sleep() 53 | const data = createDataSourceEn(searchParams.pageSize) 54 | 55 | // 计算商品合计总价 56 | const totalPrice = data 57 | .reduce((total, item) => total + item.goods_price, 0) 58 | .toFixed(2) 59 | 60 | // 返回表格数据渲染 61 | return { 62 | // 表格数据 63 | data, 64 | // 数据总数,用于分页 65 | total: 100, 66 | // 合计栏数据 67 | summaryData: { 68 | // 对应「商品价格」 69 | goods_price: totalPrice, 70 | }, 71 | } 72 | }} 73 | /> 74 | ) 75 | } 76 | 77 | export default Demo 78 | ``` 79 | 80 | 目前支持以下语言: 81 | 82 | | **语言** | **文件名** | 83 | | -------- | ---------- | 84 | | 中文 | zh_CN | 85 | | 英文 | en_US | 86 | 87 | - 如果找不到你需要的语言包,欢迎参考 [中文语言包](https://github.com/Barrior/schema-render/tree/main/packages/search-table-react/src/locale/zh_CN.ts) 创建一个新的语言包,并给我们发一个 Pull Request。 88 | - 或者配置相同的语言包数据格式传值给 `locale` 属性。 89 | -------------------------------------------------------------------------------- /examples/search-table-react/801-open-api.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: API 手册 4 | order: 801 5 | toc: content 6 | --- 7 | 8 | # Ref API 9 | 10 | ## 总览 11 | 12 | 内核通过 `ref` 属性开放的 API 方法如下: 13 | 14 | - `refresh`: 刷新表格内容,将调用 `request` 重新拉取数据。 15 | - `getRootElement`: 获取根节点 DOM 元素。 16 | - `getSearchRef`: 获取搜索栏实例。 17 | - `getDataSource`: 获取表格列表数据。 18 | - `setDataSource`: 设置表格列表数据。 19 | - `setSummaryData`: 设置合计栏数据。 20 | - `getSearchValue`: 获取搜索参数。 21 | - `setSearchValue`: 设置搜索数据。 22 | - `clearSearchValue`: 清除搜索数据。 23 | - `getRequestParams`: 获取请求的参数,“导出”时常用。 24 | - `getRequestExtraParams`: 获取请求额外参数,如排序参数等。 25 | - `updateScrollY`: 更新表格高度以达到“一屏显示”效果。 26 | - `openSettingModal`: 打开列设置弹窗。 27 | 28 | ## refresh 29 | 30 | 刷新表格内容,将调用 `request` 重新拉取数据。 31 | 32 | 类型:`(params?: IRequestParams, options?: IRequestOptions) => Promise` 33 | 34 | ### 入参 35 | 36 | | **名称** | **描述** | **类型** | 37 | | ----------- | ---------------------- | ----------------- | 38 | | **params** | 搜索及分页参数 | `IRequestParams` | 39 | | **options** | 刷新选项,控制刷新行为 | `IRequestOptions` | 40 | 41 | `IRequestOptions` 42 | 43 | | **名称** | **描述** | **类型** | 44 | | ------------- | --------------------------------------------- | --------- | 45 | | **overwrite** | 是否覆盖参数方式刷新,默认 `false` 即合并入参 | `boolean` | 46 | -------------------------------------------------------------------------------- /examples/search-table-react/902-table-action-permission.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: 案例集锦 4 | order: 902 5 | toc: content 6 | --- 7 | 8 | # 操作按钮权限封装 9 | 10 | 参考[操作按钮单页跳转](./901-table-action-link.md) 11 | -------------------------------------------------------------------------------- /examples/search-table-react/910-no-search.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: 案例集锦 4 | order: 910 5 | toc: content 6 | --- 7 | 8 | # 隐藏搜索区域 9 | 10 | 设置 `search: false` 可隐藏搜索区域 11 | 12 | ```tsx 13 | /** 14 | * defaultShowCode: true 15 | */ 16 | import { sleep } from '@examples/utils' 17 | import columns from './helpers/columns' 18 | import createDataSource from './helpers/createDataSource' 19 | import SearchTable from '@schema-render/search-table-react' 20 | 21 | const Demo = () => { 22 | return ( 23 | { 27 | // 模拟请求接口获取表格数据 28 | await sleep() 29 | const data = createDataSource() 30 | return { data, total: 100 } 31 | }} 32 | /> 33 | ) 34 | } 35 | 36 | export default Demo 37 | ``` 38 | -------------------------------------------------------------------------------- /examples/search-table-react/helpers/columns-en.ts: -------------------------------------------------------------------------------- 1 | import type { IColumnType } from '@schema-render/search-table-react' 2 | 3 | const columns: IColumnType[] = [ 4 | { 5 | title: 'Supplier Name', 6 | dataIndex: 'supplier_name', 7 | width: 150, 8 | fixed: 'left', 9 | }, 10 | { 11 | title: 'Supplier Code', 12 | dataIndex: 'supplier_code', 13 | width: 150, 14 | }, 15 | { 16 | title: 'Description', 17 | dataIndex: 'description', 18 | valueType: () => ({ 19 | type: 'long-text-modal', 20 | maxLength: 20, 21 | }), 22 | }, 23 | { 24 | title: 'Bill No', 25 | dataIndex: 'bill_no', 26 | width: 130, 27 | }, 28 | { 29 | title: 'Bill Type', 30 | dataIndex: 'bill_type', 31 | }, 32 | { 33 | title: 'Bill Date', 34 | dataIndex: 'bill_date', 35 | width: 120, 36 | }, 37 | { 38 | title: 'Bill Status', 39 | dataIndex: 'bill_status', 40 | }, 41 | { 42 | title: 'Operator', 43 | dataIndex: 'operator', 44 | }, 45 | { 46 | title: 'Goods Name', 47 | dataIndex: 'goods_name', 48 | width: 130, 49 | }, 50 | { 51 | title: 'Goods Price', 52 | dataIndex: 'goods_price', 53 | }, 54 | { 55 | title: 'Goods Code', 56 | dataIndex: 'goods_code', 57 | width: 130, 58 | }, 59 | { 60 | title: 'Goods Category', 61 | dataIndex: 'goods_category', 62 | }, 63 | { 64 | title: 'Goods Date', 65 | dataIndex: 'goods_date', 66 | width: 120, 67 | fixed: 'right', 68 | }, 69 | ] 70 | 71 | export default columns 72 | -------------------------------------------------------------------------------- /examples/search-table-react/helpers/columns-tree.ts: -------------------------------------------------------------------------------- 1 | import type { IColumnType } from '@schema-render/search-table-react' 2 | 3 | const columns: IColumnType[] = [ 4 | { 5 | title: '供应商名称', 6 | dataIndex: 'supplier_name', 7 | width: 130, 8 | }, 9 | { 10 | title: '供应商编码', 11 | dataIndex: 'supplier_code', 12 | }, 13 | { 14 | title: '单据编号', 15 | dataIndex: 'bill_no', 16 | width: 130, 17 | }, 18 | { 19 | title: '单据信息', 20 | children: [ 21 | { 22 | title: '单据类型', 23 | dataIndex: 'bill_type', 24 | }, 25 | { 26 | title: '单据日期', 27 | dataIndex: 'bill_date', 28 | width: 120, 29 | }, 30 | { 31 | title: '单据状态', 32 | dataIndex: 'bill_status', 33 | }, 34 | ], 35 | }, 36 | { 37 | title: '制单人', 38 | dataIndex: 'operator', 39 | }, 40 | { 41 | title: '商品信息', 42 | children: [ 43 | { 44 | title: '商品名称', 45 | dataIndex: 'goods_name', 46 | width: 130, 47 | }, 48 | { 49 | title: '商品价格(元)', 50 | dataIndex: 'goods_price', 51 | }, 52 | { 53 | title: '商品编码', 54 | dataIndex: 'goods_code', 55 | width: 130, 56 | }, 57 | { 58 | title: '商品分类', 59 | dataIndex: 'goods_category', 60 | }, 61 | { 62 | title: '商品日期', 63 | dataIndex: 'goods_date', 64 | width: 120, 65 | }, 66 | ], 67 | }, 68 | ] 69 | 70 | export default columns 71 | -------------------------------------------------------------------------------- /examples/search-table-react/helpers/columns-value-type.ts: -------------------------------------------------------------------------------- 1 | import type { IColumnType } from '@schema-render/search-table-react' 2 | 3 | const columns: IColumnType[] = [ 4 | { 5 | title: '常规标题', 6 | dataIndex: 'title', 7 | }, 8 | { 9 | title: '评级(rate)', 10 | dataIndex: 'rate', 11 | valueType: 'rate', 12 | }, 13 | { 14 | title: '代码块(code)', 15 | dataIndex: 'code', 16 | valueType: 'code', 17 | }, 18 | { 19 | title: '千分位数字(comma-number)', 20 | dataIndex: 'comma_number', 21 | valueType: 'comma-number', 22 | }, 23 | { 24 | title: '百分比(percent)', 25 | dataIndex: 'percent', 26 | valueType: 'percent', 27 | }, 28 | { 29 | title: '长文案(long-text)', 30 | dataIndex: 'long_text', 31 | valueType: (_record, index) => ({ 32 | type: 'long-text', 33 | maxLength: 20, 34 | color: index % 2 ? '#1677ff' : '#000', 35 | }), 36 | width: 270, 37 | }, 38 | { 39 | title: '长文案(long-text-modal)', 40 | dataIndex: 'long_text_modal', 41 | valueType: () => ({ 42 | type: 'long-text-modal', 43 | maxLength: 20, 44 | width: 500, 45 | }), 46 | width: 270, 47 | }, 48 | { 49 | title: '状态开关(switch)', 50 | dataIndex: 'switch_status', 51 | valueType: 'switch', 52 | }, 53 | { 54 | title: '标签(tags)', 55 | dataIndex: 'tags', 56 | valueType: 'tags', 57 | width: 140, 58 | }, 59 | { 60 | title: '图片(images)', 61 | dataIndex: 'image_list', 62 | valueType: 'images', 63 | }, 64 | ] 65 | 66 | export default columns 67 | -------------------------------------------------------------------------------- /examples/search-table-react/helpers/columns.ts: -------------------------------------------------------------------------------- 1 | import type { IColumnType } from '@schema-render/search-table-react' 2 | 3 | const columns: IColumnType[] = [ 4 | { 5 | title: '供应商名称', 6 | dataIndex: 'supplier_name', 7 | width: 130, 8 | }, 9 | { 10 | title: '供应商编码', 11 | dataIndex: 'supplier_code', 12 | }, 13 | { 14 | title: '单据编号', 15 | dataIndex: 'bill_no', 16 | width: 130, 17 | }, 18 | { 19 | title: '单据类型', 20 | dataIndex: 'bill_type', 21 | }, 22 | { 23 | title: '单据日期', 24 | dataIndex: 'bill_date', 25 | width: 120, 26 | }, 27 | { 28 | title: '单据状态', 29 | dataIndex: 'bill_status', 30 | }, 31 | { 32 | title: '制单人', 33 | dataIndex: 'operator', 34 | }, 35 | { 36 | title: '商品名称', 37 | dataIndex: 'goods_name', 38 | width: 130, 39 | }, 40 | { 41 | title: '商品价格(元)', 42 | dataIndex: 'goods_price', 43 | }, 44 | { 45 | title: '商品编码', 46 | dataIndex: 'goods_code', 47 | width: 130, 48 | }, 49 | { 50 | title: '商品分类', 51 | dataIndex: 'goods_category', 52 | }, 53 | { 54 | title: '商品日期', 55 | dataIndex: 'goods_date', 56 | width: 120, 57 | }, 58 | ] 59 | 60 | export default columns 61 | -------------------------------------------------------------------------------- /examples/search-table-react/helpers/createDataSource-en.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | 3 | export default function createDataSourceEn(count = 10) { 4 | const dataSource = [] 5 | 6 | for (let i = 0; i < count; i++) { 7 | const random = String(Math.random()).slice(2, 6) 8 | const goods_category = Math.random() > 0.5 ? 1 : 2 9 | const date = dayjs().format('DD/MM/YYYY HH:mm:ss') 10 | const id = `id_${i}` 11 | 12 | dataSource.push({ 13 | key: id, 14 | id, 15 | supplier_name: `${random} Co.,Ltd.`, 16 | supplier_code: random, 17 | bill_no: `B${Date.now()}`, 18 | bill_type: Math.random() > 0.5 ? 'Purchase Order' : 'Return Order', 19 | bill_date: date, 20 | bill_status: Math.random() > 0.5 ? 'Submitted' : 'To be submitted', 21 | operator: Math.random() > 0.5 ? 'Tom' : 'Lily', 22 | goods_name: goods_category === 1 ? '10 red roses' : 'Live bass', 23 | goods_code: `G${Date.now()}`, 24 | goods_category: goods_category === 1 ? 'Fruits & Flowers' : 'Aquatic Products', 25 | goods_date: date, 26 | goods_price: goods_category === 1 ? 19.9 : 22.8, 27 | description: 28 | 'SearchTable is a conditional search table component based on the Search + Antd Table encapsulation; It is often used for data retrieval, display and operation of background management systems.', 29 | }) 30 | } 31 | 32 | return dataSource 33 | } 34 | -------------------------------------------------------------------------------- /examples/search-table-react/helpers/createDataSource-vt.ts: -------------------------------------------------------------------------------- 1 | function rangeNumber(min: number, max: number, toInt = true) { 2 | const val = Math.random() * (max - min) + min 3 | return toInt ? Math.floor(val) : val 4 | } 5 | 6 | export default function createDataSource(count = 10) { 7 | const dataSource = [] 8 | 9 | for (let i = 0; i < count; i++) { 10 | const id = `id_${i}` 11 | dataSource.push({ 12 | key: id, 13 | id, 14 | title: `${String(Math.random()).slice(2, 6)}标题`, 15 | rate: rangeNumber(0, 5), 16 | code: ` 17 | function rangeNumber(min, max) { 18 | return Math.random() * (max - min) + min 19 | } 20 | `.trim(), 21 | comma_number: 22 | Math.random() > 0.4 ? rangeNumber(1000, 100000) : rangeNumber(0, 1000), 23 | percent: rangeNumber(0, 100).toFixed(2), 24 | switch_status: Math.random() > 0.5, 25 | tags: Math.random() > 0.5 ? '张三' : ['李四', '王五'], 26 | image_list: 27 | Math.random() > 0.5 28 | ? 'https://raw.githubusercontent.com/Barrior/assets/main/chrome-logo.svg' 29 | : [ 30 | 'https://raw.githubusercontent.com/Barrior/assets/main/smiling-face.gif', 31 | 'https://raw.githubusercontent.com/Barrior/assets/main/gift.png', 32 | ], 33 | deploy_status: rangeNumber(0, 3), 34 | long_text: 35 | 'SearchTable 是基于 Search + Antd Table 封装的条件搜索表格组件;常用于后台管理系统数据检索、显示与操作。', 36 | long_text_modal: 37 | 'SearchTable 是基于 Search + Antd Table 封装的条件搜索表格组件;常用于后台管理系统数据检索、显示与操作。', 38 | }) 39 | } 40 | 41 | return dataSource 42 | } 43 | -------------------------------------------------------------------------------- /examples/search-table-react/helpers/createDataSource.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | 3 | export default function createDataSource( 4 | count = 10, 5 | sorter?: { sort_field: string; sort_order: 'ascend' | 'descend' | null } 6 | ) { 7 | const dataSource = [] 8 | 9 | for (let i = 0; i < count; i++) { 10 | const random = String(Math.random()).slice(2, 6) 11 | const goods_category = Math.random() > 0.5 ? 1 : 2 12 | const date = dayjs().format('YYYY-MM-DD HH:mm:ss') 13 | const id = `id_${i}` 14 | 15 | dataSource.push({ 16 | key: id, 17 | id, 18 | supplier_name: `${random}有限公司`, 19 | supplier_code: random, 20 | bill_no: `B${Date.now()}`, 21 | bill_type: Math.random() > 0.5 ? '采购单' : '退货单', 22 | bill_date: date, 23 | bill_status: Math.random() > 0.5 ? '已提交' : '待提交', 24 | operator: Math.random() > 0.5 ? '张三' : '李四', 25 | goods_name: goods_category === 1 ? '红玫瑰10枝' : '鲜活鲈鱼', 26 | goods_code: `G${Date.now()}`, 27 | goods_category: goods_category === 1 ? '水果鲜花' : '海鲜水产', 28 | goods_date: date, 29 | goods_price: goods_category === 1 ? 19.9 : 22.8, 30 | }) 31 | } 32 | 33 | if (sorter) { 34 | dataSource.sort((a: any, b: any) => { 35 | const aValue = String(a[sorter.sort_field]) 36 | const bValue = String(b[sorter.sort_field]) 37 | return sorter.sort_order === 'ascend' 38 | ? aValue.localeCompare(bValue) 39 | : bValue.localeCompare(aValue) 40 | }) 41 | } 42 | 43 | return dataSource 44 | } 45 | -------------------------------------------------------------------------------- /examples/search-table-react/helpers/schema-en.ts: -------------------------------------------------------------------------------- 1 | import type { IFormRenderRootSchema } from '@schema-render/form-render-react' 2 | 3 | // 定义 Schema 4 | const schema: IFormRenderRootSchema = { 5 | renderType: 'Root', 6 | properties: { 7 | supplier_name: { 8 | title: 'Supplier Name', 9 | renderType: 'InputText', 10 | }, 11 | supplier_code: { 12 | title: 'Supplier Code', 13 | renderType: 'InputText', 14 | }, 15 | bill_no: { 16 | title: 'Bill No', 17 | renderType: 'InputText', 18 | }, 19 | bill_type: { 20 | title: 'Bill Type', 21 | renderType: 'Select', 22 | renderOptions: { 23 | options: [ 24 | { label: 'Purchase Order', value: 1 }, 25 | { label: 'Inbound Order', value: 2 }, 26 | { label: 'Return Order', value: 3 }, 27 | ], 28 | }, 29 | }, 30 | bill_date: { 31 | title: 'Bill Date', 32 | renderType: 'DateRangePicker', 33 | }, 34 | bill_status: { 35 | title: 'Bill Status', 36 | renderType: 'Select', 37 | renderOptions: { 38 | options: [ 39 | { label: 'submitted', value: 1 }, 40 | { label: 'pending submit', value: 2 }, 41 | { label: 'pending approval', value: 3 }, 42 | ], 43 | }, 44 | }, 45 | operator: { 46 | title: 'Operator', 47 | renderType: 'InputText', 48 | }, 49 | goods_name: { 50 | title: 'Goods Name', 51 | renderType: 'InputText', 52 | }, 53 | goods_code: { 54 | title: 'Goods Code', 55 | renderType: 'InputText', 56 | }, 57 | goods_category: { 58 | title: 'Goods Category', 59 | renderType: 'Select', 60 | renderOptions: { 61 | options: [ 62 | { label: 'Fruits & Flowers', value: 1 }, 63 | { label: 'Aquatic Products', value: 2 }, 64 | { label: 'Grains & Oils & Seasoning', value: 3 }, 65 | ], 66 | }, 67 | }, 68 | }, 69 | } 70 | 71 | export default schema 72 | -------------------------------------------------------------------------------- /examples/search-table-react/helpers/schema.ts: -------------------------------------------------------------------------------- 1 | import type { IFormRenderRootSchema } from '@schema-render/form-render-react' 2 | 3 | // 定义 Schema 4 | const schema: IFormRenderRootSchema = { 5 | renderType: 'Root', 6 | properties: { 7 | supplier_name: { 8 | title: '供应商名称', 9 | renderType: 'InputText', 10 | }, 11 | supplier_code: { 12 | title: '供应商编码', 13 | renderType: 'InputText', 14 | }, 15 | bill_no: { 16 | title: '单据编号', 17 | renderType: 'InputText', 18 | }, 19 | bill_type: { 20 | title: '单据类型', 21 | renderType: 'Select', 22 | renderOptions: { 23 | options: [ 24 | { label: '采购单', value: 1 }, 25 | { label: '入库单', value: 2 }, 26 | { label: '退货单', value: 3 }, 27 | ], 28 | }, 29 | }, 30 | bill_date: { 31 | title: '单据日期', 32 | renderType: 'DateRangePicker', 33 | }, 34 | bill_status: { 35 | title: '单据状态', 36 | renderType: 'Select', 37 | renderOptions: { 38 | options: [ 39 | { label: '已提交', value: 1 }, 40 | { label: '待提交', value: 2 }, 41 | { label: '待审批', value: 3 }, 42 | ], 43 | }, 44 | }, 45 | operator: { 46 | title: '制单人', 47 | renderType: 'InputText', 48 | }, 49 | goods_name: { 50 | title: '商品名称', 51 | renderType: 'InputText', 52 | }, 53 | goods_code: { 54 | title: '商品编码', 55 | renderType: 'InputText', 56 | }, 57 | goods_category: { 58 | title: '商品分类', 59 | renderType: 'Select', 60 | renderOptions: { 61 | options: [ 62 | { label: '水果鲜花', value: 1 }, 63 | { label: '海鲜水产', value: 2 }, 64 | { label: '粮油调味', value: 3 }, 65 | ], 66 | }, 67 | }, 68 | }, 69 | } 70 | 71 | export default schema 72 | -------------------------------------------------------------------------------- /examples/utils/cssinjs.ts: -------------------------------------------------------------------------------- 1 | import createEmotion from '@emotion/css/create-instance' 2 | 3 | export const { 4 | // flush, 5 | // hydrate, 6 | // cx, 7 | // merge, 8 | // getRegisteredStyles, 9 | // injectGlobal, 10 | // keyframes, 11 | css: cij, 12 | // sheet, 13 | // cache, 14 | } = createEmotion({ key: 'schema-render' }) 15 | -------------------------------------------------------------------------------- /examples/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function sleep(time = 1000) { 2 | return new Promise((resolve) => setTimeout(resolve, time)) 3 | } 4 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types' 2 | 3 | const config: Config.InitialOptions = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'jsdom', 6 | moduleNameMapper: { 7 | '^@test/(.*)': '/test/$1', 8 | '^@core-react/(.*)': '/packages/core-react/src/$1', 9 | '^@form-render-react/(.*)': '/packages/form-render-react/src/$1', 10 | '^@search-react/(.*)': '/packages/search-react/src/$1', 11 | '^@search-table-react/(.*)': '/packages/search-table-react/src/$1', 12 | }, 13 | roots: ['/test/'], 14 | // 排查 test 目录以 @ 符号开头的目录,如 @helpers、core-react/@helpers 15 | testPathIgnorePatterns: ['/test/(.+/)?@.+/'], 16 | // 覆盖率同样的规则 17 | coveragePathIgnorePatterns: ['/test/(.+/)?@.+/'], 18 | setupFilesAfterEnv: ['/test/@helpers/jest-setup.ts'], 19 | clearMocks: true, 20 | maxConcurrency: 10, 21 | } 22 | 23 | export default config 24 | -------------------------------------------------------------------------------- /packages/core-react/.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father' 2 | 3 | export default defineConfig({ 4 | // more father config: https://github.com/umijs/father/blob/master/docs/config.md 5 | esm: { 6 | input: 'src', 7 | output: 'dist/esm', 8 | transformer: 'swc', 9 | platform: 'node', 10 | }, 11 | cjs: { 12 | input: 'src', 13 | output: 'dist/cjs', 14 | transformer: 'swc', 15 | platform: 'browser', 16 | }, 17 | umd: { 18 | name: 'SchemaRenderCore', 19 | entry: { 20 | 'src/index': {}, 21 | }, 22 | output: 'dist/umd', 23 | platform: 'browser', 24 | externals: { 25 | react: 'react', 26 | }, 27 | }, 28 | }) 29 | -------------------------------------------------------------------------------- /packages/core-react/README.md: -------------------------------------------------------------------------------- 1 | # @schema-render/core-react 2 | 3 | 渲染内核:协议驱动、简洁易用、高可定制、轻量级。 4 | 5 | 官网:https://schema-render.js.org/core-react/001-intro 6 | 7 | --- 8 | 9 | Rendering core: protocol driven, simple and easy to use, highly customizable, lightweight. 10 | 11 | Office Documentation: https://schema-render.js.org/core-react/001-intro 12 | -------------------------------------------------------------------------------- /packages/core-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@schema-render/core-react", 3 | "version": "1.10.1", 4 | "description": "Through a set of simple JSON Schema, efficiently build a set of forms.", 5 | "keywords": [ 6 | "SchemaRender", 7 | "FormRender" 8 | ], 9 | "bugs": { 10 | "url": "https://github.com/Barrior/schema-render/issues" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/Barrior/schema-render.git" 15 | }, 16 | "license": "MIT", 17 | "sideEffects": false, 18 | "main": "dist/cjs/index.js", 19 | "module": "dist/esm/index.js", 20 | "types": "dist/esm/index.d.ts", 21 | "files": [ 22 | "dist/cjs", 23 | "dist/esm", 24 | "dist/umd" 25 | ], 26 | "scripts": { 27 | "build": "father build" 28 | }, 29 | "dependencies": {}, 30 | "peerDependencies": { 31 | "react": ">=16.9.0", 32 | "react-dom": ">=16.9.0" 33 | }, 34 | "publishConfig": { 35 | "access": "public", 36 | "registry": "https://registry.npmjs.org/" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/core-react/src/MemoCore.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react' 2 | 3 | import Core from './Core' 4 | 5 | const MemoCore = memo(Core, (prevProps, nextProps) => { 6 | const propKeys = Object.keys(nextProps) as Array 7 | let shouldUpdate = false 8 | 9 | for (const key of propKeys) { 10 | if (typeof nextProps[key] !== 'function' && nextProps[key] !== prevProps[key]) { 11 | shouldUpdate = true 12 | break 13 | } 14 | } 15 | 16 | // Don't re-render 17 | if (!shouldUpdate) { 18 | return true 19 | } 20 | 21 | return false 22 | }) 23 | 24 | export default MemoCore 25 | -------------------------------------------------------------------------------- /packages/core-react/src/RootContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | 3 | import type { IRootContext } from './typings/rootContext' 4 | 5 | const RootContext = createContext>({} as unknown as IRootContext) 6 | 7 | export default RootContext 8 | -------------------------------------------------------------------------------- /packages/core-react/src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from 'react' 2 | import { Component } from 'react' 3 | 4 | import type { ICore } from '../typings/core' 5 | import { isFunction } from '../utils/checking' 6 | 7 | interface IErrorBoundaryState { 8 | hasError: boolean 9 | error: Error 10 | } 11 | 12 | interface IErrorBoundaryProps { 13 | catchErrorTips?: ICore['catchErrorTips'] 14 | } 15 | 16 | export default class ErrorBoundary extends Component< 17 | PropsWithChildren, 18 | IErrorBoundaryState 19 | > { 20 | static getDerivedStateFromError(error: Error) { 21 | return { 22 | hasError: true, 23 | error, 24 | } 25 | } 26 | 27 | state = { 28 | hasError: false, 29 | error: Error(), 30 | } 31 | 32 | getFallback() { 33 | const { error } = this.state 34 | const { catchErrorTips } = this.props 35 | 36 | if (catchErrorTips === 'silent') { 37 | return null 38 | } 39 | 40 | if (isFunction(catchErrorTips)) { 41 | return catchErrorTips(error) 42 | } 43 | 44 | return ( 45 |
[ErrorBoundary] {error?.stack ?? String(error)}
46 | ) 47 | } 48 | 49 | render() { 50 | if (this.state.hasError) { 51 | return this.getFallback() 52 | } 53 | return this.props.children 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/core-react/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 校验状态枚举值 3 | */ 4 | export enum EValidationStatus { 5 | success = 'success', 6 | warning = 'warning', 7 | error = 'error', 8 | } 9 | 10 | export type IValidationStatus = keyof typeof EValidationStatus 11 | 12 | export const VALID_VALIDATION_STATUS = [ 13 | EValidationStatus.success, 14 | EValidationStatus.warning, 15 | EValidationStatus.error, 16 | ] 17 | 18 | /** 19 | * 整体表单布局枚举值 20 | */ 21 | export enum ELayout { 22 | normal = 'normal', 23 | autoFill = 'autoFill', 24 | autoFit = 'autoFit', 25 | } 26 | 27 | export type ILayout = keyof typeof ELayout 28 | -------------------------------------------------------------------------------- /packages/core-react/src/hooks/useAsyncEffect.ts: -------------------------------------------------------------------------------- 1 | import type { DependencyList } from 'react' 2 | import { useEffect } from 'react' 3 | 4 | import type { IMaybePromise } from '../typings/common' 5 | import useLatest from './useLatest' 6 | 7 | const useAsyncEffect = ( 8 | fn: () => IMaybePromise, 9 | deps?: DependencyList 10 | ) => { 11 | const fnRef = useLatest(fn) 12 | 13 | useEffect(() => { 14 | fnRef.current() 15 | // eslint-disable-next-line react-hooks/exhaustive-deps 16 | }, deps) 17 | } 18 | 19 | export default useAsyncEffect 20 | -------------------------------------------------------------------------------- /packages/core-react/src/hooks/useDebounceFn.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Fork from https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useDebounceFn/index.ts 3 | */ 4 | import { useMemo } from 'react' 5 | 6 | import { debounce } from '../utils/tinyLodash' 7 | import useLatest from './useLatest' 8 | import useUnmount from './useUnmount' 9 | 10 | export interface DebounceOptions { 11 | wait?: number 12 | } 13 | 14 | function useDebounceFn any>( 15 | fn: T, 16 | options?: DebounceOptions 17 | ) { 18 | const fnRef = useLatest(fn) 19 | const wait = options?.wait ?? 1000 20 | 21 | const debounced = useMemo( 22 | () => 23 | debounce((...args) => { 24 | return fnRef.current(...args) 25 | }, wait), 26 | // eslint-disable-next-line react-hooks/exhaustive-deps 27 | [] 28 | ) 29 | 30 | useUnmount(() => debounced.cancel()) 31 | 32 | return { 33 | run: debounced, 34 | cancel: debounced.cancel, 35 | } 36 | } 37 | 38 | export default useDebounceFn 39 | -------------------------------------------------------------------------------- /packages/core-react/src/hooks/useDevTool.ts: -------------------------------------------------------------------------------- 1 | import type { MutableRefObject } from 'react' 2 | import { useEffect, useState } from 'react' 3 | 4 | import type { ICore } from '..' 5 | import type { IObjectAny } from '../typings/common' 6 | import { generateUID } from '../utils/base' 7 | import useLatest from './useLatest' 8 | 9 | interface IDevTool { 10 | exportValue?: () => IObjectAny 11 | } 12 | 13 | declare global { 14 | interface Window { 15 | __schema_render_tool__: { 16 | [uid: string]: IDevTool 17 | } 18 | } 19 | } 20 | 21 | const DEV_TOOL_KEY = '__schema_render_tool__' 22 | 23 | interface IUseDevToolParams { 24 | valueRef: MutableRefObject 25 | props: ICore 26 | } 27 | 28 | export default function useDevTool({ valueRef, props }: IUseDevToolParams) { 29 | const [uid] = useState(() => generateUID()) 30 | const propsRef = useLatest(props) 31 | 32 | useEffect(() => { 33 | if (!window[DEV_TOOL_KEY]) { 34 | window[DEV_TOOL_KEY] = {} 35 | } 36 | 37 | if (!window[DEV_TOOL_KEY][uid]) { 38 | window[DEV_TOOL_KEY][uid] = {} 39 | } 40 | 41 | Object.assign(window[DEV_TOOL_KEY][uid], { 42 | exportProps: () => propsRef.current, 43 | exportSchema: () => propsRef.current?.schema, 44 | exportValue: () => valueRef.current, 45 | }) 46 | 47 | return () => { 48 | delete window[DEV_TOOL_KEY][uid] 49 | } 50 | }, [uid, propsRef, valueRef]) 51 | 52 | return { uid } 53 | } 54 | -------------------------------------------------------------------------------- /packages/core-react/src/hooks/useForceUpdate.ts: -------------------------------------------------------------------------------- 1 | import { useReducer } from 'react' 2 | 3 | export default function useForceUpdate() { 4 | const [_ignored, forceUpdate] = useReducer((x) => x + 1, 0) 5 | return { forceUpdate } 6 | } 7 | -------------------------------------------------------------------------------- /packages/core-react/src/hooks/useLatest.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | 3 | /** 4 | * fork from https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useLatest/index.ts 5 | */ 6 | function useLatest(value: T) { 7 | const ref = useRef(value) 8 | ref.current = value 9 | 10 | return ref 11 | } 12 | 13 | export default useLatest 14 | -------------------------------------------------------------------------------- /packages/core-react/src/hooks/useLayoutStyle.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | 3 | import { ELayout } from '../constants' 4 | import type { ICore } from '../typings/core' 5 | import { normalizeStyleValue } from '../utils/misc' 6 | 7 | export const LAYOUT_MIN_MAX = ['320px', '1fr'] as const 8 | 9 | /** 10 | * 计算 grid 布局样式值 11 | * @returns 根节点样式值 12 | */ 13 | export default function useLayoutStyle({ 14 | layout, 15 | layoutMinMax, 16 | layoutColumnGap, 17 | layoutRowGap, 18 | }: Pick) { 19 | const min = layoutMinMax?.[0] 20 | const max = layoutMinMax?.[1] 21 | 22 | const gridTemplateColumns = useMemo(() => { 23 | if (layout === ELayout.normal) { 24 | return 'repeat(24, 1fr)' 25 | } 26 | 27 | const autoMode = layout === ELayout.autoFill ? 'auto-fill' : 'auto-fit' 28 | const minValue = normalizeStyleValue(min ?? LAYOUT_MIN_MAX[0]) 29 | const maxValue = normalizeStyleValue(max ?? LAYOUT_MIN_MAX[1]) 30 | 31 | return `repeat(${autoMode}, minmax(${minValue}, ${maxValue}))` 32 | }, [layout, min, max]) 33 | 34 | // 设置布局样式 35 | const layoutStyle = { 36 | display: 'grid', 37 | gridTemplateColumns, 38 | columnGap: layoutColumnGap, 39 | rowGap: layoutRowGap, 40 | } 41 | 42 | return layoutStyle 43 | } 44 | -------------------------------------------------------------------------------- /packages/core-react/src/hooks/useMemoizedFn.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * fork from https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useMemoizedFn/index.ts 3 | */ 4 | import { useMemo, useRef } from 'react' 5 | 6 | type noop = (this: any, ...args: any[]) => any 7 | 8 | type PickFunction = ( 9 | this: ThisParameterType, 10 | ...args: Parameters 11 | ) => ReturnType 12 | 13 | export default function useMemoizedFn(fn: T) { 14 | const fnRef = useRef(fn) 15 | const memoizedFn = useRef>() 16 | 17 | // why not write `fnRef.current = fn`? 18 | // https://github.com/alibaba/hooks/issues/728 19 | fnRef.current = useMemo(() => fn, [fn]) 20 | 21 | if (!memoizedFn.current) { 22 | memoizedFn.current = function (this, ...args) { 23 | return fnRef.current.apply(this, args) 24 | } 25 | } 26 | 27 | return memoizedFn.current as T 28 | } 29 | -------------------------------------------------------------------------------- /packages/core-react/src/hooks/useMounted.ts: -------------------------------------------------------------------------------- 1 | import type { EffectCallback } from 'react' 2 | import { useEffect } from 'react' 3 | 4 | const useMounted = (effect: EffectCallback) => { 5 | useEffect( 6 | () => effect(), 7 | // eslint-disable-next-line react-hooks/exhaustive-deps 8 | [] 9 | ) 10 | } 11 | 12 | export default useMounted 13 | -------------------------------------------------------------------------------- /packages/core-react/src/hooks/useRootContext.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | 3 | import RootContext from '../RootContext' 4 | 5 | export default function useRootContext() { 6 | const rootCtx = useContext(RootContext) 7 | return rootCtx 8 | } 9 | -------------------------------------------------------------------------------- /packages/core-react/src/hooks/useUnmount.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | import useLatest from './useLatest' 4 | 5 | /** 6 | * fork from https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useUnmount/index.ts 7 | */ 8 | const useUnmount = (fn: () => void) => { 9 | const fnRef = useLatest(fn) 10 | 11 | useEffect( 12 | () => () => { 13 | fnRef.current() 14 | }, 15 | // eslint-disable-next-line react-hooks/exhaustive-deps 16 | [] 17 | ) 18 | } 19 | 20 | export default useUnmount 21 | -------------------------------------------------------------------------------- /packages/core-react/src/hooks/useUnmountedRef.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | /** 4 | * fork from https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useUnmountedRef/index.tsx 5 | */ 6 | const useUnmountedRef = () => { 7 | const unmountedRef = useRef(false) 8 | 9 | useEffect(() => { 10 | unmountedRef.current = false 11 | return () => { 12 | unmountedRef.current = true 13 | } 14 | }, []) 15 | 16 | return unmountedRef 17 | } 18 | 19 | export default useUnmountedRef 20 | -------------------------------------------------------------------------------- /packages/core-react/src/index.tsx: -------------------------------------------------------------------------------- 1 | import Core from './Core' 2 | 3 | export default Core 4 | 5 | export { default as useAsyncEffect } from './hooks/useAsyncEffect' 6 | export { default as useCoreValue } from './hooks/useCoreValue' 7 | export { default as useDebounceFn } from './hooks/useDebounceFn' 8 | export { default as useForceUpdate } from './hooks/useForceUpdate' 9 | export { default as useLatest } from './hooks/useLatest' 10 | export { default as useMemoizedFn } from './hooks/useMemoizedFn' 11 | export { default as useMounted } from './hooks/useMounted' 12 | export { default as useUnmount } from './hooks/useUnmount' 13 | export { default as RendererDistributor } from './services/RendererDistributor' 14 | export { default as RendererExecutor } from './services/RendererExecutor' 15 | export { default as RendererIterator } from './services/RendererIterator' 16 | export type { 17 | IDeepWriteable, 18 | IMaybePromise, 19 | IObjectAny, 20 | IOptional, 21 | IPartPartial, 22 | IPartRequired, 23 | IPath, 24 | IWriteable, 25 | } from './typings/common.d' 26 | export type * from './typings/core.d' 27 | export type * from './typings/schema.d' 28 | export * as utils from './utils/index.export' 29 | -------------------------------------------------------------------------------- /packages/core-react/src/locale/en_US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | localeName: 'en-us', 3 | validation: { 4 | required: '${label} is required', 5 | typeError: 'Data format error, expect ${type} type', 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /packages/core-react/src/locale/zh_CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | localeName: 'zh-cn', 3 | validation: { 4 | required: '${label}是必填项', 5 | typeError: '数据格式错误,期望 ${type} 类型', 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /packages/core-react/src/services/RendererDistributor.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import useRootContext from '../hooks/useRootContext' 4 | import type { ICommonProps } from '../typings/common' 5 | import assert from '../utils/assert' 6 | import { matchRenderer } from '../utils/renderer' 7 | import RendererExecutor from './RendererExecutor' 8 | 9 | const RendererDistributor: FC = (props) => { 10 | const rootCtx = useRootContext() 11 | const { schema } = props 12 | 13 | assert.falsy(schema, 'schema is required in RendererDistributor Props.') 14 | 15 | const renderer = matchRenderer(rootCtx.renderers, schema.renderType) 16 | 17 | if (!renderer) { 18 | return null 19 | } 20 | 21 | return 22 | } 23 | 24 | export default RendererDistributor 25 | -------------------------------------------------------------------------------- /packages/core-react/src/typings/common.d.ts: -------------------------------------------------------------------------------- 1 | import type { ISchema } from './schema' 2 | 3 | export type IObjectAny = IDictionary 4 | 5 | export type IWindow = typeof window 6 | 7 | export interface IDictionary { 8 | [index: string]: T 9 | } 10 | 11 | /** 12 | * 可以是同步状态值,也可以是 Promise 异步值 13 | */ 14 | export type IMaybePromise = T | Promise 15 | 16 | /** 17 | * 让对象属性部分必填 18 | */ 19 | export type IPartRequired = Omit & Required> 20 | 21 | /** 22 | * 让对象属性部分可选 23 | */ 24 | export type IPartPartial = Omit & Partial> 25 | 26 | /** 27 | * 让对象属性部分可选 28 | * @deprecated please use IPartPartial 29 | */ 30 | export type IOptional = IPartPartial 31 | 32 | /** 33 | * 让对象属性去掉只读限制 34 | */ 35 | export type IWriteable = { -readonly [P in keyof T]: T[P] } 36 | 37 | /** 38 | * 让对象属性去掉只读限制,深递归处理 39 | */ 40 | export type IDeepWriteable = { -readonly [P in keyof T]: IDeepWriteable } 41 | 42 | /** 43 | * 让对象属性变为只读,深递归处理 44 | */ 45 | export type IDeepReadonly = { 46 | readonly [P in keyof T]: IDeepReadonly 47 | } 48 | 49 | export type IPath = Array 50 | 51 | export type IApiPath = string | IPath 52 | 53 | export interface ICommonProps { 54 | // 渲染器 schema 55 | schema: S 56 | // 渲染器路径 57 | path: IPath 58 | } 59 | -------------------------------------------------------------------------------- /packages/core-react/src/typings/rootContext.d.ts: -------------------------------------------------------------------------------- 1 | import type { IClassNamesParams } from '../utils/classnames' 2 | import type { IDictionary, IObjectAny, IPath } from './common' 3 | import type { ICore, IOpenFunctionValidatorResult } from './core' 4 | import type { IRootSchema } from './schema' 5 | 6 | export interface IChangeEvent { 7 | path: IPath 8 | value: any 9 | extra?: any 10 | } 11 | 12 | export interface IRendererInstance { 13 | setValidatorState: (state: IOpenFunctionValidatorResult) => void 14 | getRootElement: () => HTMLDivElement | null 15 | } 16 | 17 | export interface IRootContext { 18 | // 根 schema 19 | rootSchema: IRootSchema 20 | // 全量配置数据 21 | rootValue: IObjectAny 22 | // 是否禁用 23 | disabled: boolean 24 | // 是否只读 25 | readonly: boolean 26 | // 样式类名前缀 27 | prefixCls: string 28 | // 对象类型样式 29 | objectStyle: IObjectAny 30 | // 渲染器列表 31 | renderers: ICore['renderers'] 32 | // 储存展示的渲染器 33 | rendererStorage: IDictionary 34 | // 全局校验器 35 | validators: Exclude 36 | // 全局上下文数据 37 | userCtx: ICore['userCtx'] 38 | // 表单布局 39 | layout: ICore['layout'] 40 | // 表单项布局 41 | itemLayout: ICore['itemLayout'] 42 | // 国际化 43 | locale: Exclude 44 | // 错误提示 45 | catchErrorTips?: ICore['catchErrorTips'] 46 | 47 | /* 事件 */ 48 | // value 值改变事件 49 | onChange: (e: IChangeEvent) => void 50 | 51 | /* 方法 */ 52 | // 加入 prefixCls 前缀的样式方法 53 | prefixClassNames: (...args: IClassNamesParams) => string 54 | } 55 | -------------------------------------------------------------------------------- /packages/core-react/src/utils/assert.ts: -------------------------------------------------------------------------------- 1 | function truthy(value: any, message?: string) { 2 | value && fail(message) 3 | } 4 | 5 | function falsy(value: any, message?: string) { 6 | !value && fail(message) 7 | } 8 | 9 | function fail(message?: string) { 10 | throw new Error(`[AssertionError]: ${message ?? 'Failed'}`) 11 | } 12 | 13 | export default { truthy, falsy, fail } 14 | -------------------------------------------------------------------------------- /packages/core-react/src/utils/base.ts: -------------------------------------------------------------------------------- 1 | import type { IObjectAny } from '../typings/common' 2 | import { isString } from './checking' 3 | import type { IClassNamesParams } from './classnames' 4 | import classNames from './classnames' 5 | import logger from './logger' 6 | 7 | // 生成 Unique ID 8 | export function generateUID() { 9 | return `${Date.now()}${Math.random().toString().substring(2, 5)}`.replace( 10 | /^(\d{4})(\d{4})(\d{4})(\d{4})/g, 11 | '$1-$2-$3-$4' 12 | ) 13 | } 14 | 15 | /** 16 | * 通过正则字符串创建正则表达式对象 17 | */ 18 | export function createRegExpByString(regexp: any): RegExp | null { 19 | if (!isString(regexp)) { 20 | return null 21 | } 22 | 23 | try { 24 | return new RegExp(regexp) 25 | } catch (e) { 26 | logger.warn(e) 27 | } 28 | 29 | return null 30 | } 31 | 32 | /** 33 | * 编译模板字符串,支持 ${} 语法的模板 34 | */ 35 | export function templateCompiled(tpl?: string, data: IObjectAny = {}) { 36 | if (!tpl) { 37 | return '' 38 | } 39 | return tpl.replace(/\$\{((\s|.)+?)?\}/g, function (...args) { 40 | const field = args[1]?.trim() 41 | if (!field) { 42 | return '' 43 | } 44 | const replacement = data[field] 45 | return replacement ?? '' 46 | }) 47 | } 48 | 49 | /** 50 | * 创建带前缀的类名 51 | * @param prefix 类名前缀 52 | * @param args classNames 库参数 53 | * @returns 带前缀的类名 54 | */ 55 | export function classNamesWithPrefix(prefix: string, ...args: IClassNamesParams): string { 56 | if (!args.length) { 57 | return '' 58 | } 59 | 60 | const classString = classNames(...args).trim() 61 | if (!classString) { 62 | return '' 63 | } 64 | 65 | const result = classString.split(' ').map((name) => `${prefix}-${name}`) 66 | return result.join(' ') 67 | } 68 | 69 | export function hasOwnProperty(obj: object, property: PropertyKey) { 70 | return Object.prototype.hasOwnProperty.call(obj, property) 71 | } 72 | 73 | export function sleep(time = 1000) { 74 | return new Promise((resolve) => setTimeout(resolve, time)) 75 | } 76 | -------------------------------------------------------------------------------- /packages/core-react/src/utils/classnames.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /** 4 | * fork from https://github.com/JedWatson/classnames/blob/main/index.js 5 | */ 6 | const hasOwn = {}.hasOwnProperty 7 | 8 | type Value = string | number | boolean | undefined | null 9 | type Mapping = Record 10 | type Argument = Value | Mapping | ArgumentArray | ReadonlyArgumentArray 11 | 12 | interface ReadonlyArgumentArray extends ReadonlyArray {} 13 | interface ArgumentArray extends Array {} 14 | 15 | type IClassNames = (...args: ArgumentArray) => string 16 | 17 | export type IClassNamesParams = ArgumentArray 18 | 19 | function classNames() { 20 | const classes = [] 21 | 22 | for (let i = 0; i < arguments.length; i++) { 23 | const arg = arguments[i] 24 | if (!arg) continue 25 | 26 | const argType = typeof arg 27 | 28 | if (argType === 'string' || argType === 'number') { 29 | classes.push(arg) 30 | } else if (Array.isArray(arg)) { 31 | if (arg.length) { 32 | const inner = classNames.apply(null, arg) 33 | if (inner) { 34 | classes.push(inner) 35 | } 36 | } 37 | } else if (argType === 'object') { 38 | if ( 39 | arg.toString !== Object.prototype.toString && 40 | !arg.toString.toString().includes('[native code]') 41 | ) { 42 | classes.push(arg.toString()) 43 | continue 44 | } 45 | 46 | for (const key in arg) { 47 | if (hasOwn.call(arg, key) && arg[key]) { 48 | classes.push(key) 49 | } 50 | } 51 | } 52 | } 53 | 54 | return classes.join(' ') 55 | } 56 | 57 | export default classNames as IClassNames 58 | -------------------------------------------------------------------------------- /packages/core-react/src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | import type { IWindow } from '../typings/common' 2 | 3 | /** 4 | * 计算「指定元素」到指定的「定位的祖先元素」的距离 5 | * https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/offsetParent 6 | * @param child 子元素 7 | * @param ancestor 祖先元素 8 | */ 9 | export function positionedOffset( 10 | child?: HTMLElement | null, 11 | ancestor?: HTMLElement | IWindow | null 12 | ) { 13 | if (!child || !ancestor) { 14 | return { left: 0, top: 0 } 15 | } 16 | 17 | let left = 0 18 | let top = 0 19 | let current: HTMLElement | null = child 20 | 21 | while (current) { 22 | if (current === ancestor) { 23 | break 24 | } 25 | left += current.offsetLeft 26 | top += current.offsetTop 27 | current = current.offsetParent as HTMLElement | null 28 | } 29 | 30 | return { left, top } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core-react/src/utils/index.export.ts: -------------------------------------------------------------------------------- 1 | export { classNamesWithPrefix, hasOwnProperty, templateCompiled } from './base' 2 | export * from './checking' 3 | export { default as classNames } from './classnames' 4 | export { positionedOffset } from './dom' 5 | export { default as logger } from './logger' 6 | export * from './tinyLodash' 7 | export { default as traverseSchema } from './traverseSchema' 8 | -------------------------------------------------------------------------------- /packages/core-react/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const logger = { 4 | warn(...args: any[]) { 5 | console.warn(...args) 6 | }, 7 | } 8 | 9 | export default logger 10 | -------------------------------------------------------------------------------- /packages/core-react/src/utils/misc.ts: -------------------------------------------------------------------------------- 1 | import type { IPath } from '../typings/common' 2 | import { isNumber } from './checking' 3 | 4 | /** 5 | * 字符串化路径 6 | */ 7 | export function stringifyPath(path: IPath) { 8 | return path.join('.') 9 | } 10 | 11 | /** 12 | * 将 string 或 number 类型的样式值标准化为 string 类型样式值 13 | * @param value 样式值 14 | */ 15 | export function normalizeStyleValue(value: string | number) { 16 | return isNumber(value) ? `${String(value)}px` : value 17 | } 18 | -------------------------------------------------------------------------------- /packages/core-react/src/utils/renderer.ts: -------------------------------------------------------------------------------- 1 | import type { IRenderer, IRenderers } from '../typings/core' 2 | import type { ISchema } from '../typings/schema' 3 | 4 | export function matchRenderer( 5 | renderers: IRenderers = {}, 6 | renderType?: string 7 | ): IRenderer | undefined { 8 | if (!renderType) { 9 | return 10 | } 11 | 12 | const renderer = renderers[renderType.toLowerCase()] 13 | 14 | if (renderer && (renderer.formItem || renderer.component)) { 15 | return renderer as IRenderer 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core-react/src/utils/statement.ts: -------------------------------------------------------------------------------- 1 | import type { IObjectAny, IPath } from '../typings/common' 2 | import { dropRight, get } from '../utils/tinyLodash' 3 | import { isBoolean, isString } from './checking' 4 | import logger from './logger' 5 | 6 | // 执行 hidden、required、disabled 语句 7 | export function performStatement({ 8 | statement, 9 | parentValue = {}, 10 | rootValue = {}, 11 | userCtx = {}, 12 | }: { 13 | statement: boolean | string | undefined 14 | parentValue: IObjectAny | undefined 15 | rootValue: IObjectAny | undefined 16 | userCtx: IObjectAny | undefined 17 | }): boolean { 18 | if (isBoolean(statement)) { 19 | return statement 20 | } 21 | 22 | if (isString(statement)) { 23 | try { 24 | const fn = new Function('$', '$root', '$userCtx', `return ${statement}`) 25 | return !!fn(parentValue, rootValue, userCtx) 26 | } catch (e) { 27 | logger.warn(e) 28 | } 29 | } 30 | 31 | return false 32 | } 33 | 34 | export function performStatementWithPath({ 35 | statement, 36 | path, 37 | rootValue, 38 | userCtx, 39 | }: { 40 | statement: boolean | string | undefined 41 | path: IPath 42 | rootValue: IObjectAny 43 | userCtx: IObjectAny | undefined 44 | }): boolean { 45 | const parentPath = dropRight(path) 46 | return performStatement({ 47 | statement, 48 | parentValue: parentPath.length ? get(rootValue, parentPath) : rootValue, 49 | rootValue, 50 | userCtx, 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /packages/form-render-react/.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father' 2 | 3 | export default defineConfig({ 4 | // more father config: https://github.com/umijs/father/blob/master/docs/config.md 5 | esm: { 6 | input: 'src', 7 | output: 'dist/esm', 8 | transformer: 'swc', 9 | platform: 'node', 10 | }, 11 | cjs: { 12 | input: 'src', 13 | output: 'dist/cjs', 14 | transformer: 'swc', 15 | platform: 'browser', 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /packages/form-render-react/README.md: -------------------------------------------------------------------------------- 1 | # @schema-render/form-render-react 2 | 3 | `FormRender` 是基于 `Core` + `Antd` 封装的表单渲染库,内置了布局结构以及常用的表单渲染器集合,以达到开箱即用。 4 | 5 | 官网:https://schema-render.js.org/form-render-react/001-intro 6 | 7 | --- 8 | 9 | `FormRender` is a form rendering library based on `Core` and `Antd`. It has a built-in layout structure and a collection of commonly used form renderers for out-of-the-box use. 10 | 11 | Office Documentation: https://schema-render.js.org/form-render-react/001-intro 12 | -------------------------------------------------------------------------------- /packages/form-render-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@schema-render/form-render-react", 3 | "version": "1.10.1", 4 | "description": "Out-of-the-box form rendering library based on Core and Antd.", 5 | "keywords": [ 6 | "SchemaRender", 7 | "FormRender" 8 | ], 9 | "bugs": { 10 | "url": "https://github.com/Barrior/schema-render/issues" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/Barrior/schema-render.git" 15 | }, 16 | "license": "MIT", 17 | "sideEffects": false, 18 | "main": "dist/cjs/index.js", 19 | "module": "dist/esm/index.js", 20 | "types": "dist/esm/index.d.ts", 21 | "files": [ 22 | "dist" 23 | ], 24 | "scripts": { 25 | "build": "father build" 26 | }, 27 | "dependencies": { 28 | "@emotion/css": "^11.11.2", 29 | "@schema-render/core-react": "^1.10.1", 30 | "dayjs": "^1.11.0" 31 | }, 32 | "peerDependencies": { 33 | "@ant-design/icons": "^5.0.0", 34 | "antd": "^5.0.0", 35 | "react": ">=16.9.0", 36 | "react-dom": ">=16.9.0" 37 | }, 38 | "publishConfig": { 39 | "access": "public", 40 | "registry": "https://registry.npmjs.org/" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/form-render-react/src/FormRenderContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | 3 | import type { IFormRenderContext } from './typings' 4 | 5 | const FormRenderContext = createContext>( 6 | {} as unknown as IFormRenderContext 7 | ) 8 | 9 | export default FormRenderContext 10 | -------------------------------------------------------------------------------- /packages/form-render-react/src/components/Description.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, ReactNode } from 'react' 2 | 3 | import useFormRenderContext from '../hooks/useFormRenderContext' 4 | import { isEmpty } from '../utils' 5 | 6 | const Description: FC<{ children: ReactNode }> = ({ children }) => { 7 | const { readonlyPlaceholder } = useFormRenderContext() 8 | return <>{isEmpty(children) ? readonlyPlaceholder : children} 9 | } 10 | 11 | export default Description 12 | -------------------------------------------------------------------------------- /packages/form-render-react/src/constants/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 内置 Action 名称 3 | */ 4 | export const ACTIONS = { 5 | submit: 'submit', 6 | reset: 'reset', 7 | } as const 8 | 9 | /** 10 | * 内置表单行为 11 | */ 12 | export type IAction = keyof typeof ACTIONS | string 13 | 14 | /** 15 | * 操作行为渲染模式 16 | * normal: 标准渲染,作为独立节点渲染在 Form 底部 17 | * formItem: 作为表单项渲染,提供给 @schema-render/search-react 用 18 | */ 19 | export const ACTIONS_RENDER_MODE = { 20 | normal: 'normal', 21 | formItem: 'formItem', 22 | } as const 23 | 24 | export type IActionsRenderMode = keyof typeof ACTIONS_RENDER_MODE 25 | 26 | /** 27 | * FormRender 默认参数 28 | */ 29 | export const FORM_RENDER_DEFAULT_PROPS = { 30 | prefixCls: 'schema-render', 31 | itemLayout: 'horizontal', 32 | readonlyPlaceholder: '-', 33 | labelWidth: 100, 34 | labelGap: 15, 35 | layoutColumnGap: 10, 36 | layoutRowGap: 15, 37 | actions: [ACTIONS.submit, ACTIONS.reset], 38 | actionsRenderMode: ACTIONS_RENDER_MODE.normal, 39 | disableFormOnActionLoading: true, 40 | validateFormOnSubmit: true, 41 | } as const 42 | 43 | /** 44 | * Actions loading 默认状态 45 | */ 46 | export const ACTIONS_DEFAULT_LOADING_STATE = { 47 | [ACTIONS.submit]: false, 48 | [ACTIONS.reset]: false, 49 | } 50 | 51 | export type IActionsLoading = typeof ACTIONS_DEFAULT_LOADING_STATE 52 | 53 | /** 54 | * Actions 渲染器名称 55 | */ 56 | export const ACTIONS_RENDER_TYPE = '__FORM_RENDER_ACTIONS__' 57 | 58 | /** 59 | * 默认时间展示格式 60 | */ 61 | export const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD' 62 | export const DEFAULT_DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss' 63 | -------------------------------------------------------------------------------- /packages/form-render-react/src/cssinjs/index.ts: -------------------------------------------------------------------------------- 1 | import createEmotion from '@emotion/css/create-instance' 2 | 3 | // ref https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript 4 | const randomAlphabeticalCharacters = Math.random() 5 | .toString(36) 6 | .substring(7) 7 | .replace(/\d/g, 'x') 8 | 9 | // 创建随机类名 key 10 | export const { css: cij } = createEmotion({ 11 | key: `schema-render-${randomAlphabeticalCharacters}`, 12 | }) 13 | -------------------------------------------------------------------------------- /packages/form-render-react/src/hooks/useFormRenderContext.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | 3 | import FormRenderContext from '../FormRenderContext' 4 | 5 | export default function useFormRenderContext() { 6 | const formRenderCtx = useContext(FormRenderContext) 7 | return formRenderCtx 8 | } 9 | -------------------------------------------------------------------------------- /packages/form-render-react/src/hooks/useItemLayout.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | 3 | import Horizontal from '../itemLayouts/Horizontal' 4 | import Vertical from '../itemLayouts/Vertical' 5 | import type { IInnerFormRenderProps } from '../typings' 6 | 7 | interface IUseItemLayoutParams { 8 | itemLayout: IInnerFormRenderProps['itemLayout'] 9 | } 10 | 11 | export default function useItemLayout({ itemLayout }: IUseItemLayoutParams) { 12 | // 表单项布局结构处理 13 | const ItemLayout = useMemo(() => { 14 | // 没有 itemLayout 或者声明垂直布局时,使用垂直布局 15 | if (!itemLayout || itemLayout === 'horizontal') { 16 | return Horizontal 17 | } 18 | 19 | if (itemLayout === 'vertical') { 20 | return Vertical 21 | } 22 | 23 | // 自定义 itemLayout 布局 24 | return itemLayout 25 | }, [itemLayout]) 26 | 27 | return ItemLayout 28 | } 29 | -------------------------------------------------------------------------------- /packages/form-render-react/src/hooks/useSchema.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | 3 | import { ACTIONS_RENDER_MODE } from '../constants' 4 | import type { IInnerFormRenderProps } from '../typings' 5 | import { addActionsSchema } from '../utils' 6 | 7 | type IUseSchemaParams = Pick< 8 | IInnerFormRenderProps, 9 | 'schema' | 'actionsRestSchema' | 'actionsRenderMode' 10 | > 11 | 12 | /** 13 | * 处理 Actions 为 Schema 渲染器模式 14 | */ 15 | export default function useSchema({ 16 | schema, 17 | actionsRestSchema, 18 | actionsRenderMode, 19 | }: IUseSchemaParams) { 20 | return useMemo(() => { 21 | return actionsRenderMode === ACTIONS_RENDER_MODE.normal 22 | ? schema 23 | : addActionsSchema(schema, actionsRestSchema) 24 | }, [schema, actionsRestSchema, actionsRenderMode]) 25 | } 26 | -------------------------------------------------------------------------------- /packages/form-render-react/src/index.style.ts: -------------------------------------------------------------------------------- 1 | import { cij } from './cssinjs' 2 | 3 | export const formRender = cij` 4 | --schema-render-color-description: #999; 5 | --schema-render-color-required-mark: #ff4d4f; 6 | --schema-render-color-warning: #faad14; 7 | --schema-render-color-error: #ff4d4f; 8 | 9 | font-size: 16px; 10 | font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,'Noto Sans',sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Noto Color Emoji'; 11 | ` 12 | -------------------------------------------------------------------------------- /packages/form-render-react/src/index.tsx: -------------------------------------------------------------------------------- 1 | import FormRender from './FormRender' 2 | export { cij } from './cssinjs' 3 | 4 | export default FormRender 5 | 6 | export type { 7 | IFormRenderProps, 8 | IFormRenderRef, 9 | IFormRenderRootSchema, 10 | ILabelRender, 11 | IRegisterActions, 12 | IRegisterActionsFn, 13 | IRegisterActionsFnParams, 14 | } from './typings/index.d' 15 | -------------------------------------------------------------------------------- /packages/form-render-react/src/itemLayouts/Horizontal/styles.ts: -------------------------------------------------------------------------------- 1 | import { cij } from '../../cssinjs' 2 | 3 | export const main = cij` 4 | display: flex; 5 | align-items: center; 6 | ` 7 | 8 | export const header = cij` 9 | flex: 0 0 100px; 10 | justify-content: flex-end; 11 | display: flex; 12 | align-items: center; 13 | margin-right: 15px; 14 | height: max-content; 15 | ` 16 | 17 | export const title = cij` 18 | word-break: break-all; 19 | ` 20 | 21 | export const mark = cij` 22 | margin-right: 4px; 23 | color: var(--schema-render-color-required-mark); 24 | position: relative; 25 | top: 2px; 26 | ` 27 | 28 | export const titleTooltip = cij` 29 | margin-left: 4px; 30 | position: relative; 31 | top: 1px; 32 | ` 33 | 34 | export const body = cij` 35 | flex-grow: 1; 36 | ` 37 | 38 | export const footer = cij` 39 | word-break: break-all; 40 | padding-left: 115px; 41 | 42 | &:empty { 43 | display: none; 44 | } 45 | ` 46 | 47 | export const description = cij` 48 | margin-top: 6px; 49 | color: var(--schema-render-color-description); 50 | ` 51 | 52 | export const errorMsg = cij` 53 | margin-top: 6px; 54 | color: var(--schema-render-color-error); 55 | ` 56 | 57 | export const warningMsg = cij` 58 | color: var(--schema-render-color-warning); 59 | ` 60 | -------------------------------------------------------------------------------- /packages/form-render-react/src/itemLayouts/Vertical/styles.ts: -------------------------------------------------------------------------------- 1 | import { cij } from '../../cssinjs' 2 | 3 | export const header = cij` 4 | margin-bottom: 8px; 5 | ` 6 | 7 | export const title = cij` 8 | word-break: break-all; 9 | ` 10 | 11 | export const mark = cij` 12 | margin-right: 4px; 13 | color: var(--schema-render-color-required-mark); 14 | position: relative; 15 | top: 2px; 16 | ` 17 | 18 | export const titleTooltip = cij` 19 | margin-left: 4px; 20 | ` 21 | 22 | export const footer = cij` 23 | word-break: break-all; 24 | 25 | &:empty { 26 | display: none; 27 | } 28 | ` 29 | 30 | export const description = cij` 31 | margin-top: 8px; 32 | color: var(--schema-render-color-description); 33 | ` 34 | 35 | export const errorMsg = cij` 36 | margin-top: 8px; 37 | color: var(--schema-render-color-error); 38 | ` 39 | 40 | export const warningMsg = cij` 41 | color: var(--schema-render-color-warning); 42 | ` 43 | -------------------------------------------------------------------------------- /packages/form-render-react/src/locale/en_US.ts: -------------------------------------------------------------------------------- 1 | import en_US from '@schema-render/core-react/dist/esm/locale/en_US' 2 | 3 | export default { 4 | ...en_US, 5 | FormRender: { 6 | submit: 'Submit', 7 | reset: 'Reset', 8 | placeholderInput: 'Please enter ${title}', 9 | placeholderSelect: 'Please select ${title}', 10 | comma: ',', 11 | displayDateRange: '${start} to ${end}', 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /packages/form-render-react/src/locale/zh_CN.ts: -------------------------------------------------------------------------------- 1 | import zh_CN from '@schema-render/core-react/dist/esm/locale/zh_CN' 2 | 3 | export default { 4 | ...zh_CN, 5 | FormRender: { 6 | submit: '提交', 7 | reset: '重置', 8 | placeholderInput: '请输入${title}', 9 | placeholderSelect: '请选择${title}', 10 | comma: ',', 11 | displayDateRange: '${start} 至 ${end}', 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /packages/form-render-react/src/renderers/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenComponentParams } from '@schema-render/core-react' 2 | import { useMemoizedFn } from '@schema-render/core-react' 3 | import { Checkbox as AntCheckbox } from 'antd' 4 | import React from 'react' 5 | 6 | import Description from '../components/Description' 7 | import { getCheckedOptions, getOptionsLabels } from '../utils' 8 | 9 | type IValue = Array 10 | type IProps = React.FC> 11 | 12 | /** 13 | * 编辑与禁用态组件 14 | */ 15 | const Checkbox: IProps = ({ schema, disabled, value, onChange }) => { 16 | const handleChange = useMemoizedFn((checkedValue: IValue) => { 17 | onChange(checkedValue, { 18 | extra: { 19 | checkedOptions: getCheckedOptions(schema.renderOptions?.options, checkedValue), 20 | }, 21 | }) 22 | }) 23 | 24 | return ( 25 | 31 | ) 32 | } 33 | 34 | /** 35 | * 只读态组件 36 | */ 37 | const ReadonlyCheckbox: IProps = ({ schema, value, locale }) => { 38 | const labels = getOptionsLabels(schema.renderOptions?.options, value) 39 | return {labels.join(locale.FormRender.comma)} 40 | } 41 | 42 | export default { 43 | component: Checkbox, 44 | readonlyComponent: ReadonlyCheckbox, 45 | } 46 | -------------------------------------------------------------------------------- /packages/form-render-react/src/renderers/DatePicker.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenComponentParams } from '@schema-render/core-react' 2 | import { utils } from '@schema-render/core-react' 3 | import { DatePicker as AntDatePicker } from 'antd' 4 | import dayjs from 'dayjs' 5 | import React, { useMemo } from 'react' 6 | 7 | import Description from '../components/Description' 8 | import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_TIME_FORMAT } from '../constants' 9 | 10 | type IProps = React.FC> 11 | 12 | /** 13 | * 编辑与禁用态组件 14 | */ 15 | const DatePicker: IProps = ({ schema, value, onChange, disabled, locale, validator }) => { 16 | const placeholder = useMemo( 17 | () => 18 | utils.templateCompiled(locale.FormRender.placeholderSelect, { 19 | title: schema.title, 20 | }), 21 | [schema.title, locale.FormRender.placeholderSelect] 22 | ) 23 | 24 | return ( 25 | onChange(val ? dayjs(val).toISOString() : undefined)} 33 | disabled={disabled} 34 | /> 35 | ) 36 | } 37 | 38 | /** 39 | * 只读态组件 40 | */ 41 | const ReadonlyDatePicker: IProps = ({ schema, value }) => { 42 | return ( 43 | 44 | {value 45 | ? dayjs(value).format( 46 | schema.renderOptions?.format || 47 | (schema.renderOptions?.showTime 48 | ? DEFAULT_DATE_TIME_FORMAT 49 | : DEFAULT_DATE_FORMAT) 50 | ) 51 | : ''} 52 | 53 | ) 54 | } 55 | 56 | export default { 57 | component: DatePicker, 58 | readonlyComponent: ReadonlyDatePicker, 59 | } 60 | -------------------------------------------------------------------------------- /packages/form-render-react/src/renderers/Description.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenComponentParams } from '@schema-render/core-react' 2 | import React from 'react' 3 | 4 | import InternalDescription from '../components/Description' 5 | 6 | const Description: React.FC> = ({ value }) => { 7 | return {value} 8 | } 9 | 10 | export default { 11 | component: Description, 12 | } 13 | -------------------------------------------------------------------------------- /packages/form-render-react/src/renderers/FromRenderActions.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenFormItemParams } from '@schema-render/core-react' 2 | import React from 'react' 3 | 4 | import Actions from '../components/Actions' 5 | 6 | const FormRenderActions: React.FC> = ({ disabled }) => { 7 | return 8 | } 9 | 10 | export default { 11 | formItem: FormRenderActions as never, 12 | } 13 | -------------------------------------------------------------------------------- /packages/form-render-react/src/renderers/InputNumber.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenComponentParams } from '@schema-render/core-react' 2 | import { useMemoizedFn, utils } from '@schema-render/core-react' 3 | import { InputNumber as AntInputNumber } from 'antd' 4 | import React, { useMemo } from 'react' 5 | 6 | import Description from '../components/Description' 7 | 8 | type IProps = React.FC> 9 | 10 | /** 11 | * 编辑与禁用态组件 12 | */ 13 | const InputNumber: IProps = ({ 14 | schema, 15 | disabled, 16 | value, 17 | onChange, 18 | validator, 19 | locale, 20 | }) => { 21 | const { validateOnBlur, ...restProps } = schema.renderOptions || {} 22 | 23 | const placeholder = useMemo( 24 | () => 25 | utils.templateCompiled(locale.FormRender.placeholderInput, { 26 | title: schema.title, 27 | }), 28 | [schema.title, locale.FormRender.placeholderInput] 29 | ) 30 | 31 | const handleChange = useMemoizedFn((val: number | null) => { 32 | onChange(val ?? undefined, { triggerValidator: !validateOnBlur }) 33 | }) 34 | 35 | const handleBlur = useMemoizedFn(() => { 36 | onChange(value, { triggerValidator: true }) 37 | }) 38 | 39 | return ( 40 | 50 | ) 51 | } 52 | 53 | /** 54 | * 只读态组件 55 | */ 56 | const ReadonlyInputNumber: IProps = ({ value }) => { 57 | return {value} 58 | } 59 | 60 | export default { 61 | component: InputNumber, 62 | readonlyComponent: ReadonlyInputNumber, 63 | } 64 | -------------------------------------------------------------------------------- /packages/form-render-react/src/renderers/InputText.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenComponentParams } from '@schema-render/core-react' 2 | import { useMemoizedFn, utils } from '@schema-render/core-react' 3 | import { Input } from 'antd' 4 | import React, { useMemo } from 'react' 5 | 6 | import Description from '../components/Description' 7 | 8 | type IProps = React.FC> 9 | 10 | /** 11 | * 编辑与禁用态组件 12 | */ 13 | const InputText: IProps = ({ schema, disabled, value, onChange, validator, locale }) => { 14 | const { validateOnBlur, ...restProps } = schema.renderOptions || {} 15 | 16 | const placeholder = useMemo( 17 | () => 18 | utils.templateCompiled(locale.FormRender.placeholderInput, { 19 | title: schema.title, 20 | }), 21 | [schema.title, locale.FormRender.placeholderInput] 22 | ) 23 | 24 | const handleChange = useMemoizedFn((e: React.ChangeEvent) => { 25 | const val = e.target.value 26 | 27 | /* istanbul ignore else */ 28 | if (val !== value) { 29 | onChange(val, { triggerValidator: !validateOnBlur }) 30 | } 31 | }) 32 | 33 | const handleBlur = useMemoizedFn(() => { 34 | onChange(value, { triggerValidator: true }) 35 | }) 36 | 37 | return ( 38 | 47 | ) 48 | } 49 | 50 | /** 51 | * 只读态组件 52 | */ 53 | const ReadonlyInputText: IProps = ({ value }) => { 54 | return {value} 55 | } 56 | 57 | export default { 58 | component: InputText, 59 | readonlyComponent: ReadonlyInputText, 60 | } 61 | -------------------------------------------------------------------------------- /packages/form-render-react/src/renderers/Object.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionCircleOutlined } from '@ant-design/icons' 2 | import type { IObjectSchema, IOpenFormItemParams } from '@schema-render/core-react' 3 | import { RendererIterator } from '@schema-render/core-react' 4 | import { Collapse, Popover } from 'antd' 5 | import type { FC } from 'react' 6 | import { useState } from 'react' 7 | 8 | /** 9 | * 不能命名为 Object,否则编译为 commonjs 文件后与关键字 Object 对象冲突 10 | */ 11 | const ObjectRenderer: FC> = ({ 12 | schema, 13 | path, 14 | objectStyle, 15 | }) => { 16 | const [collapsed, setCollapsed] = useState(false) 17 | 18 | const header = ( 19 | <> 20 | {schema.title} 21 | {!!schema.titleDescription && ( 22 | 23 | 24 | 25 | )} 26 | 27 | ) 28 | 29 | return ( 30 | setCollapsed(!collapsed)} 34 | > 35 | 36 |
37 | 38 |
39 |
40 |
41 | ) 42 | } 43 | 44 | export default { 45 | formItem: ObjectRenderer as never, 46 | } 47 | -------------------------------------------------------------------------------- /packages/form-render-react/src/renderers/ObjectNull.tsx: -------------------------------------------------------------------------------- 1 | import type { IObjectSchema, IOpenFormItemParams } from '@schema-render/core-react' 2 | import { RendererIterator } from '@schema-render/core-react' 3 | import type { FC } from 'react' 4 | 5 | /** 6 | * 只渲染子节点,无对象结构界面,应用场景:步骤表单 7 | */ 8 | const ObjectNull: FC> = ({ 9 | schema, 10 | path, 11 | objectStyle, 12 | }) => { 13 | return ( 14 |
15 | 16 |
17 | ) 18 | } 19 | 20 | export default { 21 | formItem: ObjectNull as never, 22 | } 23 | -------------------------------------------------------------------------------- /packages/form-render-react/src/renderers/Password.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenComponentParams } from '@schema-render/core-react' 2 | import { useMemoizedFn, utils } from '@schema-render/core-react' 3 | import { Input } from 'antd' 4 | import React, { useMemo } from 'react' 5 | 6 | import Description from '../components/Description' 7 | 8 | type IProps = React.FC> 9 | 10 | /** 11 | * 编辑与禁用态组件 12 | */ 13 | const Password: IProps = ({ schema, disabled, value, onChange, validator, locale }) => { 14 | const { validateOnBlur, ...restProps } = schema.renderOptions || {} 15 | 16 | const placeholder = useMemo( 17 | () => 18 | utils.templateCompiled(locale.FormRender.placeholderInput, { 19 | title: schema.title, 20 | }), 21 | [schema.title, locale.FormRender.placeholderInput] 22 | ) 23 | 24 | const handleChange = useMemoizedFn((e: React.ChangeEvent) => { 25 | const val = e.target.value 26 | 27 | /* istanbul ignore else */ 28 | if (val !== value) { 29 | onChange(val, { triggerValidator: !validateOnBlur }) 30 | } 31 | }) 32 | 33 | const handleBlur = useMemoizedFn(() => { 34 | onChange(value, { triggerValidator: true }) 35 | }) 36 | 37 | return ( 38 | 47 | ) 48 | } 49 | 50 | /** 51 | * 只读态组件 52 | */ 53 | const ReadonlyPassword: IProps = ({ value }) => { 54 | return {value} 55 | } 56 | 57 | export default { 58 | component: Password, 59 | readonlyComponent: ReadonlyPassword, 60 | } 61 | -------------------------------------------------------------------------------- /packages/form-render-react/src/renderers/Radio.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenComponentParams } from '@schema-render/core-react' 2 | import { Radio as AntRadio } from 'antd' 3 | import React from 'react' 4 | 5 | import Description from '../components/Description' 6 | import { getOptionsLabels } from '../utils' 7 | 8 | type IProps = React.FC> 9 | 10 | /** 11 | * 编辑与禁用态组件 12 | */ 13 | const Radio: IProps = ({ schema, disabled, value, onChange }) => { 14 | return ( 15 | onChange(e.target.value)} 19 | disabled={disabled} 20 | /> 21 | ) 22 | } 23 | 24 | /** 25 | * 只读态组件 26 | */ 27 | const ReadonlyRadio: IProps = ({ schema, value }) => { 28 | const labels = getOptionsLabels(schema.renderOptions?.options, [value]) 29 | return {labels[0]} 30 | } 31 | 32 | export default { 33 | component: Radio, 34 | readonlyComponent: ReadonlyRadio, 35 | } 36 | -------------------------------------------------------------------------------- /packages/form-render-react/src/renderers/Rate.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenComponentParams } from '@schema-render/core-react' 2 | import { Rate as AntRate } from 'antd' 3 | 4 | const Rate: React.FC> = ({ 5 | schema, 6 | disabled, 7 | readonly, 8 | value, 9 | onChange, 10 | }) => { 11 | return ( 12 | 19 | ) 20 | } 21 | 22 | export default { 23 | component: Rate, 24 | } 25 | -------------------------------------------------------------------------------- /packages/form-render-react/src/renderers/Select.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenComponentParams } from '@schema-render/core-react' 2 | import { utils } from '@schema-render/core-react' 3 | import { Select as AntSelect } from 'antd' 4 | import React, { useMemo } from 'react' 5 | 6 | import Description from '../components/Description' 7 | import { getOptionsLabels } from '../utils' 8 | 9 | type IProps = React.FC> 10 | 11 | /** 12 | * 编辑与禁用态组件 13 | */ 14 | const Select: IProps = ({ schema, disabled, value, onChange, locale, validator }) => { 15 | const placeholder = useMemo( 16 | () => 17 | utils.templateCompiled(locale.FormRender.placeholderSelect, { 18 | title: schema.title, 19 | }), 20 | [schema.title, locale.FormRender.placeholderSelect] 21 | ) 22 | 23 | return ( 24 | onChange(val)} 33 | disabled={disabled} 34 | /> 35 | ) 36 | } 37 | 38 | /** 39 | * 只读态组件 40 | */ 41 | const ReadonlySelect: IProps = ({ schema, value, locale }) => { 42 | const labels = getOptionsLabels(schema.renderOptions?.options, [value]) 43 | return {labels.join(locale.FormRender?.comma)} 44 | } 45 | 46 | export default { 47 | component: Select, 48 | readonlyComponent: ReadonlySelect, 49 | } 50 | -------------------------------------------------------------------------------- /packages/form-render-react/src/renderers/SelectMultiple.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenComponentParams } from '@schema-render/core-react' 2 | import { useMemoizedFn, utils } from '@schema-render/core-react' 3 | import { Select as AntSelect } from 'antd' 4 | import type { ComponentProps } from 'react' 5 | import React, { useMemo } from 'react' 6 | 7 | import Description from '../components/Description' 8 | import { getOptionsLabels } from '../utils' 9 | 10 | type IProps = React.FC> 11 | 12 | type IHandleChange = ComponentProps['onChange'] 13 | 14 | /** 15 | * 编辑与禁用态组件 16 | */ 17 | const SelectMultiple: IProps = ({ 18 | schema, 19 | disabled, 20 | value, 21 | onChange, 22 | validator, 23 | locale, 24 | }) => { 25 | const placeholder = useMemo( 26 | () => 27 | utils.templateCompiled(locale.FormRender.placeholderSelect, { 28 | title: schema.title, 29 | }), 30 | [schema.title, locale.FormRender.placeholderSelect] 31 | ) 32 | 33 | const handleChange: IHandleChange = useMemoizedFn((selectedValues, options) => { 34 | onChange(selectedValues, { 35 | extra: { selectedOptions: options }, 36 | }) 37 | }) 38 | 39 | return ( 40 | 51 | ) 52 | } 53 | 54 | /** 55 | * 只读态组件 56 | */ 57 | const ReadonlySelectMultiple: IProps = ({ schema, value, locale }) => { 58 | const labels = getOptionsLabels(schema.renderOptions?.options, value) 59 | return {labels.join(locale.FormRender?.comma)} 60 | } 61 | 62 | export default { 63 | component: SelectMultiple, 64 | readonlyComponent: ReadonlySelectMultiple, 65 | } 66 | -------------------------------------------------------------------------------- /packages/form-render-react/src/renderers/Switch.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenComponentParams } from '@schema-render/core-react' 2 | import { Switch as AntSwitch } from 'antd' 3 | import React from 'react' 4 | 5 | const Switch: React.FC> = ({ 6 | schema, 7 | disabled, 8 | readonly, 9 | value, 10 | onChange, 11 | }) => { 12 | return ( 13 | onChange(val)} 17 | // 禁用态、只读态都表现为禁用 18 | disabled={disabled || readonly} 19 | /> 20 | ) 21 | } 22 | 23 | export default { 24 | component: Switch, 25 | } 26 | -------------------------------------------------------------------------------- /packages/form-render-react/src/renderers/SwitchBox.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenComponentParams } from '@schema-render/core-react' 2 | import { Checkbox } from 'antd' 3 | import React from 'react' 4 | 5 | type IProps = React.FC> 6 | 7 | /** 8 | * 编辑、禁用与只读态组件 9 | */ 10 | const SwitchBox: IProps = ({ schema, disabled, readonly, value, onChange }) => { 11 | return ( 12 | onChange(e.target.checked)} 16 | disabled={disabled || readonly} 17 | > 18 | {schema.renderOptions?.text} 19 | 20 | ) 21 | } 22 | 23 | export default { 24 | component: SwitchBox, 25 | } 26 | -------------------------------------------------------------------------------- /packages/form-render-react/src/renderers/TextArea.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenComponentParams } from '@schema-render/core-react' 2 | import { useMemoizedFn, utils } from '@schema-render/core-react' 3 | import { Input } from 'antd' 4 | import React, { useMemo } from 'react' 5 | 6 | import Description from '../components/Description' 7 | 8 | type IProps = React.FC> 9 | 10 | /** 11 | * 编辑与禁用态组件 12 | */ 13 | const TextArea: IProps = ({ schema, disabled, value, onChange, validator, locale }) => { 14 | const { validateOnBlur, ...restProps } = schema.renderOptions || {} 15 | 16 | const placeholder = useMemo( 17 | () => 18 | utils.templateCompiled(locale.FormRender.placeholderInput, { 19 | title: schema.title, 20 | }), 21 | [schema.title, locale.FormRender.placeholderInput] 22 | ) 23 | 24 | const handleChange = useMemoizedFn((e: React.ChangeEvent) => { 25 | const val = e.target.value 26 | 27 | /* istanbul ignore else */ 28 | if (val !== value) { 29 | onChange(val, { triggerValidator: !validateOnBlur }) 30 | } 31 | }) 32 | 33 | const handleBlur = useMemoizedFn(() => { 34 | onChange(value, { triggerValidator: true }) 35 | }) 36 | 37 | return ( 38 | 48 | ) 49 | } 50 | 51 | /** 52 | * 只读态组件 53 | */ 54 | const ReadonlyTextArea: IProps = ({ value }) => { 55 | return {value} 56 | } 57 | 58 | export default { 59 | component: TextArea, 60 | readonlyComponent: ReadonlyTextArea, 61 | } 62 | -------------------------------------------------------------------------------- /packages/form-render-react/src/renderers/index.tsx: -------------------------------------------------------------------------------- 1 | import { ACTIONS_RENDER_TYPE } from '../constants' 2 | import Checkbox from './Checkbox' 3 | import DatePicker from './DatePicker' 4 | import DateRangePicker from './DateRangePicker' 5 | import Description from './Description' 6 | import FromRenderActions from './FromRenderActions' 7 | import InputNumber from './InputNumber' 8 | import InputText from './InputText' 9 | import ObjectRenderer from './Object' 10 | import ObjectNull from './ObjectNull' 11 | import Password from './Password' 12 | import Radio from './Radio' 13 | import Rate from './Rate' 14 | import Select from './Select' 15 | import SelectMultiple from './SelectMultiple' 16 | import Switch from './Switch' 17 | import SwitchBox from './SwitchBox' 18 | import TextArea from './TextArea' 19 | 20 | const renderers = { 21 | [ACTIONS_RENDER_TYPE]: FromRenderActions, 22 | 23 | /** 24 | * 结构渲染器 25 | */ 26 | Object: ObjectRenderer, 27 | ObjectNull, 28 | 29 | /** 30 | * 常规渲染器 31 | */ 32 | InputText, 33 | InputNumber, 34 | Password, 35 | TextArea, 36 | Radio, 37 | Checkbox, 38 | Switch, 39 | SwitchBox, 40 | DatePicker, 41 | DateRangePicker, 42 | Description, 43 | Rate, 44 | Select, 45 | SelectMultiple, 46 | } 47 | 48 | export type IBuiltinRenderers = Exclude< 49 | keyof typeof renderers, 50 | typeof ACTIONS_RENDER_TYPE 51 | > 52 | 53 | export default renderers 54 | -------------------------------------------------------------------------------- /packages/search-react/.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father' 2 | 3 | export default defineConfig({ 4 | // more father config: https://github.com/umijs/father/blob/master/docs/config.md 5 | esm: { 6 | input: 'src', 7 | output: 'dist/esm', 8 | transformer: 'swc', 9 | platform: 'node', 10 | }, 11 | cjs: { 12 | input: 'src', 13 | output: 'dist/cjs', 14 | transformer: 'swc', 15 | platform: 'browser', 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /packages/search-react/README.md: -------------------------------------------------------------------------------- 1 | # @schema-render/search-react 2 | 3 | `Search` 是基于 `FormRender` 封装的开箱即用的条件搜索组件。 4 | 5 | 官网:https://schema-render.js.org/search-react/001-intro 6 | 7 | --- 8 | 9 | `Search` is an out-of-the-box conditional search component based on `FormRender`. 10 | 11 | Office Documentation: https://schema-render.js.org/search-react/001-intro 12 | -------------------------------------------------------------------------------- /packages/search-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@schema-render/search-react", 3 | "version": "1.10.1", 4 | "description": "Conditional search component based on FormRender.", 5 | "keywords": [ 6 | "SchemaRender", 7 | "FormRender", 8 | "Search" 9 | ], 10 | "bugs": { 11 | "url": "https://github.com/Barrior/schema-render/issues" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Barrior/schema-render.git" 16 | }, 17 | "license": "MIT", 18 | "sideEffects": false, 19 | "main": "dist/cjs/index.js", 20 | "module": "dist/esm/index.js", 21 | "types": "dist/esm/index.d.ts", 22 | "files": [ 23 | "dist" 24 | ], 25 | "scripts": { 26 | "build": "father build" 27 | }, 28 | "dependencies": { 29 | "@schema-render/form-render-react": "^1.10.1" 30 | }, 31 | "peerDependencies": { 32 | "@ant-design/icons": "^5.0.0" 33 | }, 34 | "publishConfig": { 35 | "access": "public", 36 | "registry": "https://registry.npmjs.org/" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/search-react/src/constants/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 内置 Action 名称 3 | */ 4 | export const ACTIONS = { 5 | submit: 'submit', 6 | reset: 'reset', 7 | collapse: 'collapse', 8 | } as const 9 | 10 | /** 11 | * 默认参数 12 | */ 13 | export const DEFAULT_SEARCH_PROPS = { 14 | prefixCls: 'schema-render', 15 | layoutColumnGap: 10, 16 | layoutRowGap: 15, 17 | actions: [ACTIONS.reset, ACTIONS.submit, ACTIONS.collapse], 18 | defaultCollapsed: true, 19 | collapsedRows: 2, 20 | } 21 | 22 | export type ISearchDefaultProps = typeof DEFAULT_SEARCH_PROPS 23 | -------------------------------------------------------------------------------- /packages/search-react/src/hooks/useResize.ts: -------------------------------------------------------------------------------- 1 | import { useLatest } from '@schema-render/core-react' 2 | import type { IFormRenderRef } from '@schema-render/form-render-react' 3 | import type { RefObject } from 'react' 4 | import { useEffect, useRef } from 'react' 5 | 6 | /** 7 | * 容器元素窗口尺寸变化 hooks 8 | */ 9 | export default function useResize( 10 | formRenderRef: RefObject, 11 | fn: (element: HTMLElement) => void 12 | ) { 13 | const fnRef = useLatest(fn) 14 | 15 | // 设置 -1,保证第一次 ResizeObserver 会执行 16 | const prevWidth = useRef(-1) 17 | 18 | useEffect(() => { 19 | const element = formRenderRef.current?.getRootElement() 20 | 21 | if (!element) { 22 | return 23 | } 24 | 25 | const resizeObserver = new ResizeObserver((entries) => { 26 | const width = entries[0].contentRect.width 27 | 28 | // 只有元素宽度变化才触发回调 29 | if (prevWidth.current !== width) { 30 | prevWidth.current = width 31 | fnRef.current(element) 32 | } 33 | }) 34 | 35 | resizeObserver.observe(element) 36 | 37 | return () => resizeObserver.disconnect() 38 | 39 | // eslint-disable-next-line react-hooks/exhaustive-deps 40 | }, []) 41 | } 42 | -------------------------------------------------------------------------------- /packages/search-react/src/index.tsx: -------------------------------------------------------------------------------- 1 | import Search from './Search' 2 | 3 | export default Search 4 | 5 | export type { ISearchProps, ISearchRef } from './typings/index.d' 6 | -------------------------------------------------------------------------------- /packages/search-react/src/locale/en_US.ts: -------------------------------------------------------------------------------- 1 | import en_US from '@schema-render/form-render-react/dist/esm/locale/en_US' 2 | 3 | export default { 4 | ...en_US, 5 | FormRender: { 6 | ...en_US.FormRender, 7 | submit: 'Search', 8 | }, 9 | Search: { 10 | collapse: 'Collapse', 11 | expand: 'Expand', 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /packages/search-react/src/locale/zh_CN.ts: -------------------------------------------------------------------------------- 1 | import zh_CN from '@schema-render/form-render-react/dist/esm/locale/zh_CN' 2 | 3 | export default { 4 | ...zh_CN, 5 | FormRender: { 6 | ...zh_CN.FormRender, 7 | submit: '查询', 8 | }, 9 | Search: { 10 | collapse: '收起', 11 | expand: '展开', 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /packages/search-react/src/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { IPartRequired } from '@schema-render/core-react' 2 | import type { IFormRenderProps, IFormRenderRef } from '@schema-render/form-render-react' 3 | 4 | import type { ISearchDefaultProps } from '../constants' 5 | import type zh_CN from '../locale/zh_CN' 6 | 7 | export type ILocale = typeof zh_CN 8 | 9 | /** 10 | * Search 参数配置 11 | */ 12 | export interface ISearchProps extends Omit { 13 | /** 14 | * 国际化 15 | */ 16 | locale?: ILocale 17 | 18 | /** 19 | * 是否默认折叠 20 | */ 21 | defaultCollapsed?: boolean 22 | 23 | /** 24 | * 是否折叠,受控模式 25 | */ 26 | // collapsed?: boolean 27 | 28 | /** 29 | * 折叠行数,默认 2 行 30 | */ 31 | collapsedRows?: number 32 | 33 | /** 34 | * 计算折叠时展示的表单项个数的算法 35 | * @param container 表单容器 36 | * @returns 折叠时展示的表单项个数,必须是整数 37 | */ 38 | calcCollapsedNumber?: (container?: HTMLElement | null) => number 39 | 40 | /** 41 | * 「折叠/展开」切换事件 42 | * @param isCollapsed 是否折叠 43 | */ 44 | onToggleCollapsed?: (isCollapsed: boolean) => void 45 | } 46 | 47 | /** 48 | * 合并默认值后的 Search 参数 49 | */ 50 | export type IInnerSearchProps = IPartRequired 51 | 52 | /** 53 | * Search Ref 54 | */ 55 | export type ISearchRef = IFormRenderRef 56 | -------------------------------------------------------------------------------- /packages/search-react/src/utils/actions.ts: -------------------------------------------------------------------------------- 1 | import { utils } from '@schema-render/core-react' 2 | 3 | export function createActionsResetSchema(prefixCls: string) { 4 | return { 5 | className: utils.classNamesWithPrefix(prefixCls, 'form-actions'), 6 | style: { 7 | gridColumnStart: -2, 8 | textAlign: 'end', 9 | }, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/search-react/src/utils/collapsedNumber.ts: -------------------------------------------------------------------------------- 1 | import type { ISearchProps } from '../typings' 2 | 3 | /** 4 | * 根据容器尺寸计算折叠表单项个数 5 | * @param container 容器 DOM 元素 6 | * @param collapsedRows 折叠行数 7 | * @param customAlgo 自定义算法 8 | * @returns 折叠个数 9 | */ 10 | export function calcCollapsedNumber( 11 | container: HTMLElement | null | undefined, 12 | collapsedRows: number, 13 | customAlgo?: ISearchProps['calcCollapsedNumber'] 14 | ) { 15 | // 采用自定义算法 16 | if (customAlgo) { 17 | return customAlgo(container) 18 | } 19 | 20 | // 容器不存在,返回兜底默认值 21 | if (!container) { 22 | return 5 23 | } 24 | 25 | // 容器宽度 26 | const containerWidth = container.clientWidth 27 | 28 | // 子节点宽度,取最大值,即 Bounding 宽度 29 | const childWidth = container.firstElementChild?.getBoundingClientRect()?.width || 320 30 | 31 | // 计算折叠个数算法,以第一个子节点宽度作为计算基数 32 | return Math.floor(containerWidth / childWidth) * collapsedRows - 1 33 | } 34 | -------------------------------------------------------------------------------- /packages/search-react/src/utils/createDisplayedSchema.ts: -------------------------------------------------------------------------------- 1 | import type { IFormRenderRootSchema } from '@schema-render/form-render-react' 2 | 3 | type IRootSchema = IFormRenderRootSchema 4 | 5 | /** 6 | * 创建折叠的 Schema 7 | * @param rawSchema 原始 Schema 8 | * @param collapsedNumber 折叠的个数 9 | * @returns 折叠的 Schema 10 | */ 11 | function createCollapsedSchema( 12 | rawSchema: IRootSchema, 13 | collapsedNumber: number 14 | ): IRootSchema { 15 | const collapsedProps: IRootSchema['properties'] = {} 16 | 17 | const displayedKeys = Object.keys(rawSchema.properties).slice(0, collapsedNumber) 18 | for (const key of displayedKeys) { 19 | collapsedProps[key] = rawSchema.properties[key] 20 | } 21 | 22 | return { 23 | renderType: 'Root', 24 | properties: collapsedProps, 25 | } 26 | } 27 | 28 | interface ICreateSearchSchema { 29 | /** 30 | * 是否折叠 31 | */ 32 | isCollapsed: boolean 33 | /** 34 | * 原始 Schema 35 | */ 36 | rawSchema: IRootSchema 37 | /** 38 | * 折叠的个数 39 | */ 40 | collapsedNumber: number 41 | } 42 | 43 | /** 44 | * 根据折叠个数,创建展示的 Schema 45 | */ 46 | export default function createDisplayedSchema({ 47 | isCollapsed, 48 | rawSchema, 49 | collapsedNumber, 50 | }: ICreateSearchSchema) { 51 | return isCollapsed ? createCollapsedSchema(rawSchema, collapsedNumber) : rawSchema 52 | } 53 | -------------------------------------------------------------------------------- /packages/search-react/src/utils/tinyLodash.ts: -------------------------------------------------------------------------------- 1 | import { utils } from '@schema-render/core-react' 2 | 3 | /** 4 | * ref https://lodash.com/docs/4.17.15#size 5 | */ 6 | export function size(value: unknown) { 7 | if (utils.isPlainObject(value)) { 8 | return Object.keys(value).length 9 | } 10 | 11 | if (utils.isArray(value)) { 12 | return value.length 13 | } 14 | 15 | if (utils.isString(value)) { 16 | return value.length 17 | } 18 | 19 | return 0 20 | } 21 | -------------------------------------------------------------------------------- /packages/search-table-react/.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father' 2 | 3 | export default defineConfig({ 4 | // more father config: https://github.com/umijs/father/blob/master/docs/config.md 5 | esm: { 6 | input: 'src', 7 | output: 'dist/esm', 8 | transformer: 'swc', 9 | platform: 'node', 10 | }, 11 | cjs: { 12 | input: 'src', 13 | output: 'dist/cjs', 14 | transformer: 'swc', 15 | platform: 'browser', 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /packages/search-table-react/README.md: -------------------------------------------------------------------------------- 1 | # @schema-render/search-table-react 2 | 3 | `SearchTable` 是基于 `Search` + `Antd Table` 封装的条件搜素表格组件。 4 | 5 | 官网:https://schema-render.js.org/search-table-react/001-intro 6 | 7 | --- 8 | 9 | `SearchTable` is a conditional search table component based on `Search` + `Antd Table`. 10 | 11 | Office Documentation: https://schema-render.js.org/search-table-react/001-intro 12 | -------------------------------------------------------------------------------- /packages/search-table-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@schema-render/search-table-react", 3 | "version": "1.10.1", 4 | "description": "Conditional search table component.", 5 | "keywords": [ 6 | "SchemaRender", 7 | "FormRender", 8 | "Search", 9 | "SearchTable" 10 | ], 11 | "bugs": { 12 | "url": "https://github.com/Barrior/schema-render/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/Barrior/schema-render.git" 17 | }, 18 | "license": "MIT", 19 | "sideEffects": false, 20 | "main": "dist/cjs/index.js", 21 | "module": "dist/esm/index.js", 22 | "types": "dist/esm/index.d.ts", 23 | "files": [ 24 | "dist" 25 | ], 26 | "scripts": { 27 | "build": "father build" 28 | }, 29 | "dependencies": { 30 | "@dnd-kit/core": "6.1.0", 31 | "@dnd-kit/modifiers": "7.0.0", 32 | "@dnd-kit/sortable": "8.0.0", 33 | "@dnd-kit/utilities": "3.2.2", 34 | "@schema-render/search-react": "^1.10.1" 35 | }, 36 | "peerDependencies": { 37 | "@ant-design/icons": "^5.0.0", 38 | "antd": "^5.0.0" 39 | }, 40 | "publishConfig": { 41 | "access": "public", 42 | "registry": "https://registry.npmjs.org/" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/search-table-react/src/RootContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | 3 | import type { IRootContext } from './typings/rootContext' 4 | 5 | const RootContext = createContext>({} as unknown as IRootContext) 6 | 7 | export default RootContext 8 | -------------------------------------------------------------------------------- /packages/search-table-react/src/components/ButtonLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import type { IMaybePromise } from '@schema-render/core-react' 2 | import { useMemoizedFn, utils } from '@schema-render/core-react' 3 | import type { ButtonProps } from 'antd' 4 | import { Button } from 'antd' 5 | import type { MouseEvent } from 'react' 6 | import { useState } from 'react' 7 | 8 | const { logger } = utils 9 | 10 | type IButtonEvent = MouseEvent 11 | 12 | interface IButtonLoadingProps extends Omit { 13 | /** 14 | * 按钮内容 15 | */ 16 | children: React.ReactNode 17 | /** 18 | * 点击事件处理 19 | */ 20 | onClick?: (e: IButtonEvent) => IMaybePromise 21 | /** 22 | * 不要 loading 效果 23 | */ 24 | noLoading?: boolean 25 | } 26 | 27 | /** 28 | * 内置加载 loading 的按钮 29 | * click 事件返回 promise 即可 30 | */ 31 | export default function ButtonLoading({ 32 | children, 33 | onClick, 34 | noLoading = false, 35 | ...restProps 36 | }: IButtonLoadingProps) { 37 | const [loading, setLoading] = useState(false) 38 | 39 | const handleClick = useMemoizedFn(async (e: IButtonEvent) => { 40 | if (noLoading) { 41 | onClick?.(e) 42 | return 43 | } 44 | 45 | setLoading(true) 46 | try { 47 | await onClick?.(e) 48 | } catch (err) { 49 | logger.warn(err) 50 | } 51 | setLoading(false) 52 | }) 53 | 54 | return ( 55 | 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /packages/search-table-react/src/components/ColumnSettingContent/index.column.tsx: -------------------------------------------------------------------------------- 1 | import { HolderOutlined } from '@ant-design/icons' 2 | import { Button, InputNumber, Switch, Tag } from 'antd' 3 | import type { ReactNode } from 'react' 4 | 5 | import type { ILocale } from '../../typings' 6 | 7 | interface IColumn { 8 | title: string 9 | dataIndex: string 10 | width: number 11 | render?: (value: any, onChange: (val: any) => void) => ReactNode 12 | algin?: 'center' 13 | } 14 | 15 | export function createColumns({ locale }: { locale: ILocale }) { 16 | const titleEnum = locale.SearchTable.settingModalColumnEnum 17 | const sortEnum = locale.SearchTable.settingModalColumnSortEnum 18 | 19 | const columns: IColumn[] = [ 20 | { 21 | title: titleEnum[0], 22 | dataIndex: 'name', 23 | width: 200, 24 | }, 25 | { 26 | title: titleEnum[1], 27 | dataIndex: 'hidden', 28 | width: 90, 29 | render: (value: boolean, onChange) => { 30 | return onChange(!val)} /> 31 | }, 32 | }, 33 | { 34 | title: titleEnum[2], 35 | dataIndex: 'width', 36 | width: 100, 37 | render: (value: number, onChange) => { 38 | return ( 39 | { 43 | val ? onChange(val) : undefined 44 | }} 45 | /> 46 | ) 47 | }, 48 | }, 49 | { 50 | title: titleEnum[3], 51 | dataIndex: 'fixed', 52 | width: 60, 53 | algin: 'center', 54 | render: (value?: 'left' | 'right') => { 55 | const text = value ? (value === 'left' ? sortEnum[0] : sortEnum[1]) : sortEnum[2] 56 | const color = value ? (value === 'left' ? 'orange' : 'blue') : undefined 57 | return ( 58 | 59 | {text} 60 | 61 | ) 62 | }, 63 | }, 64 | { 65 | title: titleEnum[4], 66 | dataIndex: 'sort', 67 | width: 50, 68 | algin: 'center', 69 | render: () => ( 70 | 26 | setIsOpen(false)} 33 | > 34 | {text} 35 | 36 | 37 | ) : ( 38 | <>{text} 39 | ) 40 | } 41 | 42 | export default LongTextModal 43 | -------------------------------------------------------------------------------- /packages/search-table-react/src/valueTypes/index.tsx: -------------------------------------------------------------------------------- 1 | import { utils } from '@schema-render/core-react' 2 | import { Rate, Switch, Tag } from 'antd' 3 | 4 | import ImagesPreview from '../components/ImagesPreview' 5 | import { STYLE_CODE } from '../constants/style' 6 | import type { ITableProps } from '../typings/table' 7 | import { isEmpty } from '../utils/common' 8 | import CommaNumber from './CommaNumber' 9 | import LongText from './LongText' 10 | import LongTextModal from './LongTextModal' 11 | 12 | const { isArray } = utils 13 | 14 | export const BUILT_IN_VALUE_TYPES: ITableProps['registerValueType'] = { 15 | /** 16 | * 代码块 17 | */ 18 | code: ({ value, options }) => { 19 | return ( 20 |
21 |         {value}
22 |       
23 | ) 24 | }, 25 | /** 26 | * 百分比 27 | */ 28 | percent: ({ value }) => (isEmpty(value) ? '-' : `${value}%`), 29 | /** 30 | * 状态开关 31 | */ 32 | switch: ({ value, options }) => , 33 | /** 34 | * 标签 35 | */ 36 | tags: ({ value, options }) => { 37 | const data = isArray(value) ? value : [value] 38 | return data.map((text, i) => ( 39 | 40 | {text} 41 | 42 | )) 43 | }, 44 | /** 45 | * 评分 46 | */ 47 | rate: ({ value, options }) => ( 48 | 49 | ), 50 | /** 51 | * 数字千分位 52 | */ 53 | 'comma-number': (props) => , 54 | /** 55 | * 图片显示 56 | */ 57 | images: ({ value, options }) => { 58 | // 排除空数据 59 | if (isEmpty(value)) { 60 | return '-' 61 | } 62 | const imgList = isArray(value) ? value : [value] 63 | const { groupProps, ...imgProps } = options 64 | return 65 | }, 66 | /** 67 | * 长文案 Tooltips 方式显示 68 | */ 69 | 'long-text': (props) => , 70 | /** 71 | * 长文案点击弹窗方式显示 72 | */ 73 | 'long-text-modal': (props) => , 74 | } 75 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Barrior/schema-render/33f361b4193fc32b4606a33c4b14dbbf17cce236/public/favicon.png -------------------------------------------------------------------------------- /public/wechat-praise.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Barrior/schema-render/33f361b4193fc32b4606a33c4b14dbbf17cce236/public/wechat-praise.jpg -------------------------------------------------------------------------------- /scripts/build-cli.mjs: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer' 2 | import path from 'path' 3 | import { fileURLToPath } from 'url' 4 | 5 | import buildProject from './build.mjs' 6 | 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 8 | const packages = path.resolve(__dirname, '../packages') 9 | 10 | inquirer 11 | .prompt([ 12 | { 13 | type: 'checkbox', 14 | message: '选择需要构建的项目', 15 | name: 'packages', 16 | choices: packages, 17 | validate(answer) { 18 | if (answer.length < 1) { 19 | return '至少选择一个' 20 | } 21 | return true 22 | }, 23 | }, 24 | ]) 25 | .then((answers) => { 26 | buildProject({ packages: answers }) 27 | }) 28 | -------------------------------------------------------------------------------- /scripts/config/swc.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "target": "es5", 4 | "loose": true, 5 | "parser": { 6 | "syntax": "typescript", 7 | "tsx": false, 8 | "decorators": false 9 | }, 10 | "externalHelpers": false 11 | }, 12 | "module": { 13 | "type": "commonjs", 14 | "strict": false, 15 | "strictMode": true, 16 | "lazy": false, 17 | "noInterop": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /scripts/config/swc.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "target": "es2016", 4 | "loose": false, 5 | "externalHelpers": false, 6 | "parser": { 7 | "syntax": "typescript", 8 | "tsx": true, 9 | "decorators": true 10 | }, 11 | "transform": { 12 | "legacyDecorator": true, 13 | "decoratorMetadata": true 14 | } 15 | }, 16 | "module": { 17 | "type": "es6", 18 | "strict": false, 19 | "strictMode": false, 20 | "lazy": false, 21 | "noInterop": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /scripts/logger.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import chalk from 'chalk' 3 | 4 | class Logger { 5 | tips(...args) { 6 | console.log(chalk.hex('#666')('TIPS'), ...args) 7 | } 8 | 9 | debug(...args) { 10 | console.debug(chalk.blue('DEBUG'), ...args) 11 | } 12 | 13 | log(...args) { 14 | console.log(chalk.blue('INFO'), ...args) 15 | } 16 | 17 | info(...args) { 18 | console.log(chalk.blue('INFO'), ...args) 19 | } 20 | 21 | warn(...args) { 22 | console.warn(chalk.yellow('WARN'), ...args) 23 | } 24 | 25 | error(...args) { 26 | console.error(chalk.red('ERROR'), ...args) 27 | } 28 | } 29 | 30 | export default new Logger() 31 | -------------------------------------------------------------------------------- /test/@helpers/dom.ts: -------------------------------------------------------------------------------- 1 | export function prettyDOMByInnerHTML(innerHTML: string) { 2 | const testingNode = document.createElement('div') 3 | testingNode.innerHTML = innerHTML 4 | return testingNode 5 | } 6 | 7 | interface ICreateElementParams { 8 | rect?: Partial 9 | className?: string 10 | } 11 | 12 | export function createElement(params?: ICreateElementParams) { 13 | const element = document.createElement('div') 14 | document.body.appendChild(element) 15 | 16 | if (params?.className) { 17 | element.classList.add(params.className) 18 | } 19 | 20 | if (params?.rect) { 21 | element.getBoundingClientRect = () => params.rect as DOMRect 22 | } 23 | 24 | return element 25 | } 26 | 27 | /** 28 | * 获取 DOM 元素样式 29 | * @param container 容器 30 | * @param selector 选择器 31 | * @returns 样式对象 32 | */ 33 | export function getElementStyle(container: HTMLElement, selector: string) { 34 | const elem = container.querySelector(selector) 35 | if (elem) { 36 | return window.getComputedStyle(elem) 37 | } 38 | return {} as Partial 39 | } 40 | -------------------------------------------------------------------------------- /test/@helpers/jest-setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | 3 | import React from 'react' 4 | 5 | // Define global React to solve "ReferenceError: React is not defined in jest tests" 6 | // https://stackoverflow.com/questions/58980934/referenceerror-react-is-not-defined-in-jest-tests 7 | global.React = React 8 | 9 | // 不打印 console 错误信息 10 | jest.spyOn(console, 'error').mockImplementation(jest.fn()) 11 | -------------------------------------------------------------------------------- /test/@helpers/schema.ts: -------------------------------------------------------------------------------- 1 | import type { IRootSchema } from '@core-react/index' 2 | 3 | export function wrapRootSchema(properties: IRootSchema['properties'] = {}) { 4 | return { 5 | renderType: 'Root', 6 | properties, 7 | } as const 8 | } 9 | -------------------------------------------------------------------------------- /test/@helpers/sleep.ts: -------------------------------------------------------------------------------- 1 | export default function sleep(time: number) { 2 | return new Promise((resolve) => { 3 | setTimeout(resolve, time) 4 | }) 5 | } 6 | -------------------------------------------------------------------------------- /test/@mock/mockWindowAttrs.ts: -------------------------------------------------------------------------------- 1 | window.prompt = jest.fn() 2 | window.alert = jest.fn() 3 | window.confirm = jest.fn() 4 | window.requestIdleCallback = jest.fn() 5 | -------------------------------------------------------------------------------- /test/core-react/@helpers/CompletedCore.tsx: -------------------------------------------------------------------------------- 1 | import type { ICore, ICoreRef, IPartPartial } from '@core-react/index' 2 | import Core from '@core-react/index' 3 | import type { Ref } from 'react' 4 | import { forwardRef, useMemo } from 'react' 5 | 6 | import renderers from './renderers' 7 | import Vertical from './Vertical' 8 | 9 | type IProps = IPartPartial 10 | 11 | const CompletedCore = (props: IProps, ref?: Ref) => { 12 | // 渲染器集合处理 13 | const innerRenderers = useMemo( 14 | () => ({ ...renderers, ...props.renderers }), 15 | [props.renderers] 16 | ) 17 | 18 | return ( 19 | 25 | ) 26 | } 27 | 28 | export default forwardRef(CompletedCore) 29 | -------------------------------------------------------------------------------- /test/core-react/@helpers/Vertical.tsx: -------------------------------------------------------------------------------- 1 | import type { IOpenItemLayoutParams } from '@core-react/index' 2 | import type { FC } from 'react' 3 | 4 | const Vertical: FC = ({ body, schema, validator, required }) => { 5 | return ( 6 |
7 |
8 | {required && *} 9 | 10 | {schema.title} 11 | 12 | {!!schema.titleDescription && ( 13 | {schema.titleDescription} 14 | )} 15 |
16 | 17 |
{body}
18 | 19 |
20 | {validator.status === 'error' && !!validator.message && ( 21 |
{validator.message}
22 | )} 23 | 24 | {validator.status === 'warning' && !!validator.message && ( 25 |
{validator.message}
26 | )} 27 | 28 | {!!schema.description && ( 29 |
{schema.description}
30 | )} 31 |
32 |
33 | ) 34 | } 35 | 36 | export default Vertical 37 | -------------------------------------------------------------------------------- /test/core-react/components/ErrorBoundary.test.tsx: -------------------------------------------------------------------------------- 1 | import ErrorBoundary from '@core-react/components/ErrorBoundary' 2 | import { render, screen } from '@testing-library/react' 3 | 4 | const ERROR_TEXT = '_ERROR_FROM_COMPONENT_' 5 | 6 | const ErrorComponent = () => { 7 | throw new Error(ERROR_TEXT) 8 | } 9 | 10 | describe('ErrorBoundary 验证', () => { 11 | test('组件抛出错误应该被捕获并展示出来', async () => { 12 | render( 13 | 14 | 15 | 16 | ) 17 | const node = screen.queryByText(new RegExp(ERROR_TEXT)) 18 | expect(node).toBeInTheDocument() 19 | }) 20 | 21 | test('设置为 silent 模式时,组件错误不应该展示出来', async () => { 22 | render( 23 | 24 | 25 | 26 | ) 27 | const node = screen.queryByText(new RegExp(ERROR_TEXT)) 28 | expect(node).not.toBeInTheDocument() 29 | }) 30 | 31 | test('自定义错误时,应该捕获错误并展示自定义的错误信息', async () => { 32 | const CUSTOM_ERROR_TEXT = 'CUSTOM_ERROR_TIPS' 33 | render( 34 | CUSTOM_ERROR_TEXT}> 35 | 36 | 37 | ) 38 | const node = screen.queryByText(new RegExp(CUSTOM_ERROR_TEXT)) 39 | expect(node).toBeInTheDocument() 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /test/core-react/disabled.test.tsx: -------------------------------------------------------------------------------- 1 | import { wrapRootSchema } from '@test/@helpers/schema' 2 | import { render, screen } from '@testing-library/react' 3 | 4 | import CompletedCore from './@helpers/CompletedCore' 5 | 6 | const schema = wrapRootSchema({ 7 | width: { 8 | title: '宽度', 9 | renderType: 'InputNumber', 10 | renderOptions: { 11 | 'data-testid': 'tid-width', 12 | }, 13 | disabled: true, 14 | }, 15 | height: { 16 | title: '高度', 17 | renderType: 'InputNumber', 18 | renderOptions: { 19 | 'data-testid': 'tid-height', 20 | }, 21 | }, 22 | nested: { 23 | title: 'object-title', 24 | renderType: 'Object', 25 | properties: { 26 | left: { 27 | title: '水平位置', 28 | renderType: 'InputNumber', 29 | renderOptions: { 30 | 'data-testid': 'tid-left', 31 | }, 32 | }, 33 | }, 34 | }, 35 | }) 36 | 37 | describe('disabled 验证', () => { 38 | test('局部只读态,应该只影响自身', async () => { 39 | render() 40 | const elem = screen.queryByTestId('tid-width') 41 | expect(elem).toBeInTheDocument() 42 | expect(elem?.classList.contains('is-disabled')).toBeTruthy() 43 | }) 44 | 45 | test('全局只读态,应该影响所有的表单项', async () => { 46 | render() 47 | const formItems = ['width', 'height', 'left'] 48 | formItems.forEach((name) => { 49 | const elem = screen.queryByTestId(`tid-${name}`) 50 | expect(elem?.classList.contains('is-disabled')).toBeTruthy() 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /test/core-react/hooks/useDebounceFn.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * fork from https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useDebounceFn/__tests__/index.test.ts 3 | */ 4 | import useDebounceFn from '@core-react/hooks/useDebounceFn' 5 | import sleep from '@test/@helpers/sleep' 6 | import type { RenderHookResult } from '@testing-library/react' 7 | import { act, renderHook } from '@testing-library/react' 8 | 9 | interface ISetUpParams { 10 | fn: (...arg: any[]) => any 11 | wait: number 12 | } 13 | 14 | let count = 0 15 | const debounceFn = (gap: number) => { 16 | count += gap 17 | } 18 | 19 | const setUp = ({ fn, wait }: ISetUpParams) => 20 | renderHook(() => useDebounceFn(fn, { wait })) 21 | 22 | let hook: RenderHookResult, null> 23 | 24 | describe('useDebounceFn 验证', () => { 25 | test('run, cancel and flush should work', async () => { 26 | act(() => { 27 | hook = setUp({ 28 | fn: debounceFn, 29 | wait: 200, 30 | }) 31 | }) 32 | 33 | await act(async () => { 34 | hook.result.current.run(2) 35 | hook.result.current.run(2) 36 | hook.result.current.run(2) 37 | hook.result.current.run(2) 38 | expect(count).toBe(0) 39 | await sleep(300) 40 | expect(count).toBe(2) 41 | 42 | hook.result.current.run(4) 43 | expect(count).toBe(2) 44 | await sleep(300) 45 | expect(count).toBe(6) 46 | 47 | hook.result.current.run(4) 48 | expect(count).toBe(6) 49 | hook.result.current.cancel() 50 | expect(count).toBe(6) 51 | await sleep(300) 52 | expect(count).toBe(6) 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /test/core-react/hooks/useDevTool.test.tsx: -------------------------------------------------------------------------------- 1 | import useDevTool from '@core-react/hooks/useDevTool' 2 | import { renderHook } from '@testing-library/react' 3 | import { useRef } from 'react' 4 | 5 | describe('useDevTool 验证', () => { 6 | test('exportValue 导出数据应该符合预期', () => { 7 | const value = { title: '标题' } 8 | const { result } = renderHook(() => { 9 | const valueRef = useRef(value) 10 | return useDevTool({ valueRef } as never) 11 | }) 12 | const tool = window.__schema_render_tool__[result.current.uid] 13 | expect(tool.exportValue?.()).toEqual(value) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/core-react/hooks/useLatest.test.tsx: -------------------------------------------------------------------------------- 1 | import useLatest from '@core-react/hooks/useLatest' 2 | import { renderHook } from '@testing-library/react' 3 | 4 | const setUp = (val: any) => renderHook((state) => useLatest(state), { initialProps: val }) 5 | 6 | describe('useLatest 验证', () => { 7 | test('useLatest with basic variable should work', () => { 8 | const { result, rerender } = setUp(0) 9 | 10 | rerender(1) 11 | expect(result.current.current).toBe(1) 12 | 13 | rerender(2) 14 | expect(result.current.current).toBe(2) 15 | 16 | rerender(3) 17 | expect(result.current.current).toBe(3) 18 | }) 19 | 20 | test('useLatest with reference variable should work', () => { 21 | const { result, rerender } = setUp({}) 22 | 23 | expect(result.current.current).toEqual({}) 24 | 25 | rerender([]) 26 | expect(result.current.current).toEqual([]) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/core-react/hooks/useLayoutStyle.test.tsx: -------------------------------------------------------------------------------- 1 | import useLayoutStyle, { LAYOUT_MIN_MAX } from '@core-react/hooks/useLayoutStyle' 2 | import { renderHook } from '@testing-library/react' 3 | 4 | describe('useLayoutStyle 验证', () => { 5 | test('layout 模式为 normal 时,应该返回 24 栅格布局值', () => { 6 | const { result } = renderHook(() => useLayoutStyle({ layout: 'normal' })) 7 | expect(result.current.gridTemplateColumns).toEqual('repeat(24, 1fr)') 8 | }) 9 | 10 | test('layout 模式为 autoFill 时,没有设置最小最大值则返回默认值', () => { 11 | const { result } = renderHook(() => useLayoutStyle({ layout: 'autoFill' })) 12 | expect(result.current.gridTemplateColumns).toEqual( 13 | `repeat(auto-fill, minmax(${LAYOUT_MIN_MAX[0]}, ${LAYOUT_MIN_MAX[1]}))` 14 | ) 15 | }) 16 | 17 | test('layout 模式为 autoFill 时,设置最小最大值则返回设置的值', () => { 18 | const { result } = renderHook(() => 19 | useLayoutStyle({ 20 | layout: 'autoFill', 21 | layoutMinMax: ['100px', '200px'], 22 | }) 23 | ) 24 | expect(result.current.gridTemplateColumns).toEqual( 25 | 'repeat(auto-fill, minmax(100px, 200px))' 26 | ) 27 | }) 28 | 29 | test('layout 模式为 autoFill 时,数字类型最小最大值应该正确', () => { 30 | const { result } = renderHook(() => 31 | useLayoutStyle({ 32 | layout: 'autoFill', 33 | layoutMinMax: [100, 200], 34 | }) 35 | ) 36 | expect(result.current.gridTemplateColumns).toEqual( 37 | 'repeat(auto-fill, minmax(100px, 200px))' 38 | ) 39 | }) 40 | 41 | test('layout 模式为 autoFit 时,应该返回设置的最小最大值', () => { 42 | const { result } = renderHook(() => 43 | useLayoutStyle({ 44 | layout: 'autoFit', 45 | layoutMinMax: [100, 200], 46 | }) 47 | ) 48 | expect(result.current.gridTemplateColumns).toEqual( 49 | 'repeat(auto-fit, minmax(100px, 200px))' 50 | ) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /test/core-react/hooks/useMemoizedFn.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * fork from https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useMemoizedFn/__tests__/index.test.ts 3 | */ 4 | import useMemoizedFn from '@core-react/hooks/useMemoizedFn' 5 | import type { RenderHookResult } from '@testing-library/react' 6 | import { act, renderHook } from '@testing-library/react' 7 | import { useState } from 'react' 8 | 9 | const useCount = () => { 10 | const [count, setCount] = useState(0) 11 | 12 | const addCount = () => { 13 | setCount((c) => c + 1) 14 | } 15 | 16 | const memoizedFn = useMemoizedFn(() => count) 17 | 18 | return { addCount, memoizedFn } 19 | } 20 | 21 | let hook!: RenderHookResult, any> 22 | 23 | describe('useMemoizedFn', () => { 24 | it('useMemoizedFn should work', () => { 25 | act(() => { 26 | hook = renderHook(() => useCount()) 27 | }) 28 | const currentFn = hook.result.current.memoizedFn 29 | expect(hook.result.current.memoizedFn()).toBe(0) 30 | 31 | act(() => { 32 | hook.result.current.addCount() 33 | }) 34 | 35 | expect(currentFn).toEqual(hook.result.current.memoizedFn) 36 | expect(hook.result.current.memoizedFn()).toBe(1) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/core-react/hooks/useUnmount.test.tsx: -------------------------------------------------------------------------------- 1 | import useUnmount from '@core-react/hooks/useUnmount' 2 | import { renderHook } from '@testing-library/react' 3 | 4 | describe('useUnmount 验证', () => { 5 | test('useUnmount should work', () => { 6 | const fn = jest.fn() 7 | const hook = renderHook(() => useUnmount(fn)) 8 | expect(fn).toBeCalledTimes(0) 9 | hook.rerender() 10 | expect(fn).toBeCalledTimes(0) 11 | hook.unmount() 12 | expect(fn).toBeCalledTimes(1) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /test/core-react/hooks/useUnmountedRef.test.tsx: -------------------------------------------------------------------------------- 1 | import useUnmountedRef from '@core-react/hooks/useUnmountedRef' 2 | import { renderHook } from '@testing-library/react' 3 | 4 | /** 5 | * fork from https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useUnmountedRef/__tests__/index.test.ts 6 | */ 7 | describe('useUnmountedRef', () => { 8 | it('should work', async () => { 9 | const hook = renderHook(() => useUnmountedRef()) 10 | expect(hook.result.current.current).toBe(false) 11 | hook.rerender() 12 | expect(hook.result.current.current).toBe(false) 13 | hook.unmount() 14 | expect(hook.result.current.current).toBe(true) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /test/core-react/readonly.test.tsx: -------------------------------------------------------------------------------- 1 | import { wrapRootSchema } from '@test/@helpers/schema' 2 | import { render, screen } from '@testing-library/react' 3 | 4 | import CompletedCore from './@helpers/CompletedCore' 5 | 6 | const schema = wrapRootSchema({ 7 | width: { 8 | title: '宽度', 9 | renderType: 'InputNumber', 10 | renderOptions: { 11 | 'data-testid': 'tid-width', 12 | }, 13 | readonly: true, 14 | }, 15 | height: { 16 | title: '高度', 17 | renderType: 'InputNumber', 18 | renderOptions: { 19 | 'data-testid': 'tid-height', 20 | }, 21 | }, 22 | nested: { 23 | title: 'object-title', 24 | renderType: 'Object', 25 | properties: { 26 | left: { 27 | title: '水平位置', 28 | renderType: 'InputNumber', 29 | renderOptions: { 30 | 'data-testid': 'tid-left', 31 | }, 32 | }, 33 | }, 34 | }, 35 | }) 36 | 37 | describe('readonly 验证', () => { 38 | test('局部只读态,应该只影响自身', async () => { 39 | render() 40 | const elem = screen.queryByTestId('tid-width') 41 | expect(elem).toBeInTheDocument() 42 | expect(elem?.classList.contains('is-readonly')).toBeTruthy() 43 | }) 44 | 45 | test('全局只读态,应该影响所有的表单项', async () => { 46 | render() 47 | const formItems = ['width', 'height', 'left'] 48 | formItems.forEach((name) => { 49 | const elem = screen.queryByTestId(`tid-${name}`) 50 | expect(elem?.classList.contains('is-readonly')).toBeTruthy() 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /test/core-react/rootClassNames.test.tsx: -------------------------------------------------------------------------------- 1 | import { wrapRootSchema } from '@test/@helpers/schema' 2 | import { render } from '@testing-library/react' 3 | 4 | import CompletedCore from './@helpers/CompletedCore' 5 | 6 | describe('根节点类名验证', () => { 7 | const schema = wrapRootSchema() 8 | 9 | test('设置类名前缀为 my-sr, 根节点应该包含该类名', async () => { 10 | const { container } = render() 11 | const rootElem = container.querySelector('.my-sr') 12 | expect(rootElem).toBeInTheDocument() 13 | }) 14 | 15 | test('设置类名为 my-class, 根节点应该包含该类名', async () => { 16 | const { container } = render() 17 | const rootElem = container.querySelector('.my-class') 18 | expect(rootElem).toBeInTheDocument() 19 | }) 20 | 21 | test('禁用时, 根节点应该包含该 is-disabled 类名', async () => { 22 | const { container } = render() 23 | const rootElem = container.querySelector('.is-disabled') 24 | expect(rootElem).toBeInTheDocument() 25 | }) 26 | 27 | test('只读时, 根节点应该包含该 is-readonly 类名', async () => { 28 | const { container } = render() 29 | const rootElem = container.querySelector('.is-readonly') 30 | expect(rootElem).toBeInTheDocument() 31 | }) 32 | 33 | test('各类状态下,类名应该都包含', async () => { 34 | const { container } = render( 35 | 42 | ) 43 | 44 | const rootElem = container.querySelector('.my-class') 45 | expect(rootElem).toBeInTheDocument() 46 | 47 | const list = ['my-sr', 'my-class', 'is-disabled', 'is-readonly'] 48 | list.forEach((className) => { 49 | expect(rootElem?.classList.contains(className)).toBeTruthy() 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /test/core-react/utils/assert.test.ts: -------------------------------------------------------------------------------- 1 | import assert from '@core-react/utils/assert' 2 | 3 | describe('assert.truthy 验证', () => { 4 | test('truthy 值应该抛出错误', () => { 5 | expect(() => assert.truthy(true)).toThrow() 6 | expect(() => assert.truthy(1)).toThrow() 7 | expect(() => assert.truthy('ok')).toThrow() 8 | expect(() => assert.truthy({})).toThrow() 9 | }) 10 | 11 | test('非 truthy 值不会抛出错误,如空字符串等', () => { 12 | expect(() => { 13 | assert.truthy('') 14 | assert.truthy(0) 15 | assert.truthy(false) 16 | assert.truthy(null) 17 | assert.truthy(undefined) 18 | }).not.toThrow() 19 | }) 20 | }) 21 | 22 | describe('assert.falsy 验证', () => { 23 | test('falsy 值应该抛出错误,如空字符串等', () => { 24 | expect(() => assert.falsy('')).toThrow() 25 | expect(() => assert.falsy(0)).toThrow() 26 | expect(() => assert.falsy(false)).toThrow() 27 | expect(() => assert.falsy(null)).toThrow() 28 | expect(() => assert.falsy(undefined)).toThrow() 29 | }) 30 | 31 | test('非 falsy 值不会抛出错误', () => { 32 | expect(() => { 33 | assert.falsy('ok') 34 | assert.falsy(1) 35 | assert.falsy(true) 36 | assert.falsy({}) 37 | assert.falsy(Date) 38 | }).not.toThrow() 39 | }) 40 | }) 41 | 42 | describe('assert.fail 验证', () => { 43 | test('错误消息应该一致', () => { 44 | const errorMsg = '错误了' 45 | expect(() => assert.fail(errorMsg)).toThrow(`[AssertionError]: ${errorMsg}`) 46 | }) 47 | 48 | test('兜底错误消息应该一致', () => { 49 | expect(() => assert.fail()).toThrow('[AssertionError]: Failed') 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /test/core-react/utils/logger.test.ts: -------------------------------------------------------------------------------- 1 | import logger from '@core-react/utils/logger' 2 | 3 | describe('logger 验证', () => { 4 | test('warn 方法调用后输出结果应该一致', () => { 5 | const spy = jest.spyOn(console, 'warn').mockImplementation(jest.fn()) 6 | logger.warn('警告') 7 | expect(spy).toHaveBeenCalledWith('警告') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "outDir": "dist", 5 | "rootDir": ".", 6 | "declaration": true, 7 | 8 | /* Module Resolution */ 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "allowSyntheticDefaultImports": true, 12 | "esModuleInterop": true, 13 | 14 | /* Source Maps */ 15 | "sourceMap": true, 16 | 17 | /* Strict Checks */ 18 | "strict": true, 19 | 20 | /* Linter Checks */ 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noImplicitReturns": false, 24 | "noFallthroughCasesInSwitch": true, 25 | 26 | /* Experimental Options */ 27 | "experimentalDecorators": true, 28 | "emitDecoratorMetadata": true, 29 | 30 | /* Advanced Options */ 31 | "resolveJsonModule": true, 32 | 33 | /* JSX Options */ 34 | "jsx": "react-jsx", 35 | 36 | /* path */ 37 | "baseUrl": "./", 38 | "paths": { 39 | // for test 40 | "@core-react/*": ["packages/core-react/src/*"], 41 | "@test/*": ["./test/*"], 42 | 43 | // for Dumi 44 | "@@/*": [".dumi/tmp/*"], 45 | "@examples/*": ["./examples/*"], 46 | "@schema-render/core-react": ["packages/core-react/src/index"], 47 | "@schema-render/core-react/dist/esm/*": ["packages/core-react/src/*"], 48 | "@schema-render/form-render-react": ["packages/form-render-react/src/index"], 49 | "@schema-render/form-render-react/dist/esm/*": ["packages/form-render-react/src/*"], 50 | "@schema-render/search-react": ["packages/search-react/src/index"], 51 | "@schema-render/search-react/dist/esm/*": ["packages/search-react/src/*"], 52 | "@schema-render/search-table-react": ["packages/search-table-react/src/index"], 53 | "@schema-render/search-table-react/dist/esm/*": [ 54 | "packages/search-table-react/src/*" 55 | ] 56 | } 57 | }, 58 | "compileOnSave": true 59 | } 60 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true 5 | }, 6 | } 7 | --------------------------------------------------------------------------------