├── .browserslistrc
├── src
├── core
│ ├── schme-type-renderers
│ │ ├── NullRenderer.vue
│ │ ├── ConstantRenderer.vue
│ │ ├── BooleanRenderer.vue
│ │ ├── CustomRenderer.vue
│ │ ├── NumberRenderer.vue
│ │ ├── StringRenderer.vue
│ │ ├── ObjectRenderer.vue
│ │ └── ArrayRenderer.vue
│ ├── mixins
│ │ ├── index.ts
│ │ ├── select-mixin.ts
│ │ ├── current-value.js
│ │ ├── error-message.ts
│ │ └── base.ts
│ ├── types.ts
│ ├── format-component-map.ts
│ ├── deprecaped-SchemaRenderer.vue
│ ├── validator
│ │ ├── types.ts
│ │ ├── index.ts
│ │ ├── date-time-range-keyword.ts
│ │ └── instance-default-options.ts
│ ├── utils
│ │ ├── index.ts
│ │ ├── common-renderer-props.ts
│ │ ├── common.ts
│ │ └── schema.ts
│ ├── FormItem.vue
│ ├── SchemaItem.vue
│ ├── index.ts
│ └── SchemaForm.vue
├── assets
│ └── logo.png
├── shims-vue.d.ts
├── plugins
│ ├── index.ts
│ ├── image-uploader.ts
│ └── ImageUploader.vue
├── demos
│ ├── required.js
│ ├── boolean.js
│ ├── index.js
│ ├── custom.js
│ ├── number.js
│ ├── string.js
│ ├── deps.js
│ ├── nest-deps.js
│ ├── complex.js
│ ├── date.js
│ └── array.js
├── shims-tsx.d.ts
├── theme-element-ui
│ ├── Form.vue
│ ├── Alert.vue
│ ├── Constant.vue
│ ├── ColorPicker.vue
│ ├── Switch.vue
│ ├── DateTimePicker.vue
│ ├── FormItem.vue
│ ├── DatePicker.vue
│ ├── Selection.vue
│ ├── TextInput.vue
│ ├── ArrayItemAddAction.vue
│ ├── TimePicker.vue
│ ├── base
│ │ ├── DatePicker.vue
│ │ └── ArrayItemActions.vue
│ ├── NumberInput.vue
│ ├── DateTimeRangePicker.vue
│ ├── index.ts
│ ├── SingleTypeArrayWrapper.vue
│ └── form-component-props
│ │ └── index.ts
├── main.ts
├── CustomInput.vue
├── App-ts.vue
├── custom-keyword.ts
└── App.vue
├── public
├── favicon.ico
└── index.html
├── jest.config.js
├── docs
├── fetures.md
├── implements.md
├── renderer-props.md
└── dependencies.md
├── tests
└── unit
│ ├── utils
│ ├── Custom.vue
│ ├── index.ts
│ └── Helper.vue
│ ├── default-value
│ ├── boolean.spec.ts
│ ├── number.spec.ts
│ ├── string.spec.ts
│ ├── object.spec.ts
│ └── array.spec.ts
│ ├── theme-element
│ ├── constant.spec.ts
│ ├── switch.spec.ts
│ ├── numberInput.spec.ts
│ ├── textInput.spec.ts
│ ├── colorPicker.spec.ts
│ ├── datetime.spec.ts
│ ├── timePicker.spec.ts
│ ├── date.spec.ts
│ ├── selection.spec.ts
│ └── dateTimeRangePicker.spec.ts
│ ├── value-change
│ ├── array-value.spec.ts
│ └── reset-value.spec.ts
│ └── vjsf
│ ├── ui-schema.spec.ts
│ └── vjsf.spec.ts
├── babel.config.js
├── .prettierrc
├── publish.sh
├── .gitignore
├── .github
└── workflows
│ ├── build.yml
│ └── test-coverage.yml
├── .eslintrc.js
├── tsconfig.json
├── vue.config.js
└── package.json
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 |
--------------------------------------------------------------------------------
/src/core/schme-type-renderers/NullRenderer.vue:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wanwu-fe/vue-json-schema-form/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wanwu-fe/vue-json-schema-form/HEAD/src/assets/logo.png
--------------------------------------------------------------------------------
/src/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import Vue from 'vue'
3 | export default Vue
4 | }
5 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
3 | }
4 |
--------------------------------------------------------------------------------
/docs/fetures.md:
--------------------------------------------------------------------------------
1 | # 关于插件设计
2 |
3 | 插件应该满足以下要求
4 |
5 | - 能够定义`format`,并可以定义`format`需要使用的组件
6 |
7 | # 4-17 工作内容
8 |
9 | - 插件功能
10 | - 错误提醒
11 | - [x] form props
12 |
--------------------------------------------------------------------------------
/tests/unit/utils/Custom.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@vue/cli-plugin-babel/preset',
5 | {
6 | useBuiltIns: false,
7 | },
8 | ],
9 | ],
10 | }
11 |
--------------------------------------------------------------------------------
/src/core/mixins/index.ts:
--------------------------------------------------------------------------------
1 | // import currentValue from './current-value'
2 |
3 | import errorMessage from './error-message'
4 | import selectMixin from './select-mixin'
5 |
6 | export * from './base'
7 |
8 | export { errorMessage, selectMixin }
9 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false,
4 | "trailingComma": "es5",
5 | "arrowParens": "always",
6 | "endOfLine": "lf",
7 | "tabWidth": 2,
8 | "htmlWhitespaceSensitivity": "css",
9 | "vueIndentScriptAndStyle": true
10 | }
11 |
--------------------------------------------------------------------------------
/src/plugins/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import ImageUploader, { plugin } from './image-uploader'
4 |
5 | export default {
6 | install(vue: typeof Vue) {
7 | vue.use(ImageUploader)
8 | },
9 | }
10 |
11 | export { plugin as imagePlugin }
12 |
--------------------------------------------------------------------------------
/src/demos/required.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'Required',
3 | schema: {
4 | type: 'object',
5 | properties: {
6 | name: {
7 | type: 'string',
8 | },
9 | age: {
10 | type: 'number',
11 | },
12 | },
13 | required: ['name', 'age'],
14 | },
15 | }
16 |
--------------------------------------------------------------------------------
/publish.sh:
--------------------------------------------------------------------------------
1 | rm -rf dist
2 |
3 | npm run build:doc
4 |
5 | cd dist
6 |
7 | git init
8 |
9 | git checkout -B gh-pages
10 |
11 | git add .
12 |
13 | git commit -m "publish"
14 |
15 | git remote add origin https://github.com/wanwu-fe/vue-json-schema-form.git
16 |
17 | git push origin gh-pages -f
18 |
19 | cd ..
20 |
21 | rm -rf dist
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | coverage
5 | .vscode
6 |
7 | # local env files
8 | .env.local
9 | .env.*.local
10 |
11 | # Log files
12 | npm-debug.log*
13 | yarn-debug.log*
14 | yarn-error.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/src/demos/boolean.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'Boolean',
3 | schema: {
4 | type: 'object',
5 | properties: {
6 | selected: {
7 | type: 'boolean',
8 | title: '是否选中',
9 | },
10 | defaultTrue: {
11 | type: 'boolean',
12 | default: true,
13 | title: '默认选中',
14 | },
15 | },
16 | },
17 | }
18 |
--------------------------------------------------------------------------------
/src/shims-tsx.d.ts:
--------------------------------------------------------------------------------
1 | import Vue, { VNode } from 'vue'
2 |
3 | declare global {
4 | namespace JSX {
5 | // tslint:disable no-empty-interface
6 | interface Element extends VNode {}
7 | // tslint:disable no-empty-interface
8 | interface ElementClass extends Vue {}
9 | interface IntrinsicElements {
10 | [elem: string]: any
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/theme-element-ui/Form.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
20 |
--------------------------------------------------------------------------------
/tests/unit/utils/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import Helper from './Helper.vue' // @ts-ignore
4 | import Custom from './Custom.vue'
5 |
6 | import JsonSchemaForm from '../../../src/core'
7 | import ThemeElement from '../../../src/theme-element-ui'
8 |
9 | export function initialize() {
10 | Vue.use(JsonSchemaForm)
11 | Vue.use(ThemeElement)
12 | // Vue.use(Custom)
13 | Vue.component(Custom.name, Custom)
14 | }
15 |
16 | export { Helper, Custom }
17 |
--------------------------------------------------------------------------------
/src/core/types.ts:
--------------------------------------------------------------------------------
1 | import { AjvFormat, AjvKeyword } from './validator/types'
2 | import { Schema } from './utils/schema'
3 |
4 | interface CustomFormat extends AjvFormat {
5 | component: String // jsf-text-input || your-custom-component
6 | }
7 |
8 | interface CustomKeyword extends AjvKeyword {
9 | transformSchema?: (originSchema: Schema) => Schema
10 | }
11 |
12 | export interface JsonSchemFormPlugin {
13 | customFormats?: CustomFormat[]
14 | customKeywords?: CustomKeyword[]
15 | }
16 |
--------------------------------------------------------------------------------
/src/theme-element-ui/Alert.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
--------------------------------------------------------------------------------
/src/core/format-component-map.ts:
--------------------------------------------------------------------------------
1 | interface StringMap {
2 | [key: string]: string
3 | }
4 |
5 | export const stringFormatComponentMap: StringMap = {
6 | default: 'jsf-text-input',
7 | color: 'jsf-color-picker',
8 | date: 'jsf-date-picker',
9 | 'date-time': 'jsf-date-time-picker',
10 | time: 'jsf-time-picker',
11 | }
12 |
13 | export const numberFormatComponentMap: StringMap = {
14 | default: 'jsf-number-input',
15 | date: 'jsf-date-picker',
16 | 'date-time': 'jsf-date-time-picker',
17 | time: 'jsf-time-picker',
18 | }
19 |
--------------------------------------------------------------------------------
/tests/unit/utils/Helper.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
25 |
--------------------------------------------------------------------------------
/src/core/schme-type-renderers/ConstantRenderer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import 'element-ui/lib/theme-chalk/index.css'
4 |
5 | import CustomInput from './CustomInput.vue'
6 | import schemaForm from './core'
7 | import JsonSchemaFormThemeElementUI from './theme-element-ui'
8 |
9 | import App from './App.vue'
10 |
11 | Vue.config.productionTip = false
12 |
13 | Vue.component('custom-input', CustomInput)
14 | Vue.use(schemaForm)
15 | Vue.use(JsonSchemaFormThemeElementUI)
16 |
17 | // Vue.component('jsf-image-uploader', ImageUploader)
18 |
19 | new Vue({
20 | render: (h) => h(App),
21 | }).$mount('#app')
22 |
--------------------------------------------------------------------------------
/src/theme-element-ui/Constant.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ value }}
4 |
5 |
6 |
7 |
22 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ${{ matrix.os }}
8 |
9 | strategy:
10 | matrix:
11 | node-version: [8.x, 10.x, 12.x]
12 | os: [ubuntu-latest, macos-latest, windows-latest]
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Use Node.js ${{ matrix.node-version }}
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: ${{ matrix.node-version }}
20 | - run: npm install
21 | - run: npm run build
22 | env:
23 | CI: true
24 |
--------------------------------------------------------------------------------
/src/demos/index.js:
--------------------------------------------------------------------------------
1 | import stringDemo from './string'
2 | import numberDemo from './number'
3 | import arrayDemo from './array'
4 | import booleanDemo from './boolean'
5 | import requiredDemo from './required'
6 | import depDemo from './deps'
7 | import dateDemo from './date'
8 | import customDemo from './custom'
9 | import complexDemo from './complex'
10 | import nestDepsDemo from './nest-deps'
11 |
12 | export default [
13 | stringDemo,
14 | numberDemo,
15 | arrayDemo,
16 | booleanDemo,
17 | requiredDemo,
18 | depDemo,
19 | dateDemo,
20 | customDemo,
21 | complexDemo,
22 | nestDepsDemo,
23 | ]
24 |
--------------------------------------------------------------------------------
/src/plugins/image-uploader.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import { JsonSchemFormPlugin } from '../core/types'
3 | import ImageUploader from './ImageUploader.vue'
4 |
5 | const URL_REG = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/ // eslint-disable-line
6 |
7 | const plugin: JsonSchemFormPlugin = {
8 | customFormats: [
9 | {
10 | name: 'image',
11 | definition: URL_REG,
12 | component: 'jsf-image-uploader',
13 | },
14 | ],
15 | }
16 |
17 | export { plugin }
18 | export default {
19 | install(vue: typeof Vue) {
20 | vue.component('jsf-image-uploader', ImageUploader)
21 | },
22 | }
23 |
--------------------------------------------------------------------------------
/src/CustomInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
26 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= htmlWebpackPlugin.options.title %>
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/App-ts.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 |
6 |
7 |
8 |
19 |
20 |
30 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 |
4 | env: {
5 | node: true,
6 | },
7 |
8 | parserOptions: {
9 | parser: '@typescript-eslint/parser',
10 | },
11 |
12 | ignorePatterns: ['__test__'],
13 |
14 | rules: {
15 | 'no-console': 'off',
16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
17 | 'no-unused-vars': 'off',
18 | },
19 |
20 | overrides: [
21 | {
22 | files: [
23 | '**/__tests__/*.{j,t}s?(x)',
24 | '**/tests/unit/**/*.spec.{j,t}s?(x)',
25 | ],
26 | env: {
27 | jest: true,
28 | },
29 | },
30 | ],
31 |
32 | extends: [
33 | 'plugin:vue/essential',
34 | 'eslint:recommended',
35 | '@vue/prettier',
36 | '@vue/typescript',
37 | ],
38 | }
39 |
--------------------------------------------------------------------------------
/src/core/deprecaped-SchemaRenderer.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
36 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "strict": true,
6 | "jsx": "preserve",
7 | "importHelpers": true,
8 | "moduleResolution": "node",
9 | "experimentalDecorators": true,
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "sourceMap": true,
13 | "baseUrl": ".",
14 | "types": ["webpack-env", "jest"],
15 | "declaration": true,
16 | "declarationDir": "dist",
17 | "paths": {
18 | "@/*": ["src/*"]
19 | },
20 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"]
21 | },
22 | "include": [
23 | "src/**/*.ts",
24 | "src/**/*.tsx",
25 | "src/**/*.vue",
26 | "tests/**/*.ts",
27 | "tests/**/*.tsx"
28 | ],
29 | "exclude": ["node_modules"]
30 | }
31 |
--------------------------------------------------------------------------------
/src/core/validator/types.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Options,
3 | FormatDefinition,
4 | FormatValidator,
5 | KeywordDefinition,
6 | } from 'ajv'
7 |
8 | export interface AjvFormat {
9 | name: string
10 | definition: FormatValidator | FormatDefinition
11 | }
12 |
13 | export interface AjvKeyword {
14 | name: string
15 | definition: KeywordDefinition
16 | }
17 |
18 | export interface CreateInstanceOptions {
19 | // locale?: string
20 | options?: Options
21 | formats?: AjvFormat | AjvFormat[]
22 | keywords?: AjvKeyword | AjvKeyword[]
23 | }
24 |
25 | export interface ConstantCreateInstanceOptions extends CreateInstanceOptions {
26 | // locale: string
27 | options: Options
28 | formats: AjvFormat[]
29 | keywords: AjvKeyword[]
30 | }
31 |
32 | export interface EnumKeyValueItem {
33 | key: string
34 | value: V
35 | }
36 |
--------------------------------------------------------------------------------
/.github/workflows/test-coverage.yml:
--------------------------------------------------------------------------------
1 | name: test-coverage
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ${{ matrix.os }}
8 |
9 | strategy:
10 | matrix:
11 | node-version: [8.x, 10.x, 12.x]
12 | os: [ubuntu-latest, macos-latest, windows-latest]
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Use Node.js ${{ matrix.node-version }}
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: ${{ matrix.node-version }}
20 | - run: npm install
21 | - run: npm run test:coverage
22 | env:
23 | CI: true
24 |
25 | - name: Upload coverage to Codecov
26 | uses: codecov/codecov-action@v1
27 | with:
28 | flags: unittests
29 | file: ./coverage/clover.xml
30 | fail_ci_if_error: true
31 |
--------------------------------------------------------------------------------
/src/theme-element-ui/ColorPicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
32 |
--------------------------------------------------------------------------------
/src/theme-element-ui/Switch.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
33 |
--------------------------------------------------------------------------------
/src/core/schme-type-renderers/BooleanRenderer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
29 |
--------------------------------------------------------------------------------
/src/core/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './schema'
2 | export * from './common'
3 | export * from './common-renderer-props'
4 |
5 | var SINGLE_QUOTE = /'|\\/g
6 | function escapeQuotes(str: string) {
7 | return str
8 | .replace(SINGLE_QUOTE, '\\$&')
9 | .replace(/\n/g, '\\n')
10 | .replace(/\r/g, '\\r')
11 | .replace(/\f/g, '\\f')
12 | .replace(/\t/g, '\\t')
13 | }
14 |
15 | function toQuotedString(str: string) {
16 | return "'" + escapeQuotes(str) + "'"
17 | }
18 |
19 | export function escapeJsonPointer(str: string) {
20 | return str.replace(/~/g, '~0').replace(/\//g, '~1')
21 | }
22 |
23 | function joinPaths(a: any, b: any) {
24 | if (a == '""') return b
25 | return (a + ' + ' + b).replace(/' \+ '/g, '')
26 | }
27 |
28 | export function getJsonPointerPath(currentPath: string, prop: string) {
29 | var path = toQuotedString('/' + escapeJsonPointer(prop))
30 | return joinPaths(currentPath, path)
31 | }
32 |
--------------------------------------------------------------------------------
/src/demos/custom.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'Custom Component',
3 | schema: {
4 | type: 'object',
5 | properties: {
6 | custom: {
7 | type: 'string',
8 | minLength: 5,
9 | default: '123',
10 | /**
11 | * string: 组件
12 | * object: full config
13 | */
14 | jsfCustom: 'custom-input',
15 | },
16 | fullCustom: {
17 | type: 'string',
18 | minLength: 5,
19 | jsfCustom: {
20 | component: 'custom-input',
21 | withFormItem: true, // default true
22 | },
23 | },
24 | customVjsf: {
25 | type: 'string',
26 | vjsf: {
27 | component: 'custom-input',
28 | },
29 | },
30 | customKeyword: {
31 | type: 'object',
32 | test: true,
33 | vjsf: {
34 | title: '自定义关键字',
35 | },
36 | },
37 | },
38 | },
39 | }
40 |
--------------------------------------------------------------------------------
/src/core/FormItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
38 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | publicPath: process.env.TYPE === 'doc' ? '/vue-json-schema-form' : '/',
3 | configureWebpack: (config) => {
4 | if (process.env.NODE_ENV === 'production' && process.env.TYPE !== 'doc') {
5 | config.externals = ['element-ui', 'ajv', 'ajv-i18n', 'vue']
6 | }
7 | },
8 | // chainWebpack: (config) => {
9 | // if (process.env.NODE_ENV === 'production' && process.env.TYPE !== 'doc') {
10 | // config.module.rule('ts').uses.delete('cache-loader')
11 | // config.module
12 | // .rule('ts')
13 | // .use('ts-loader')
14 | // .loader('ts-loader')
15 | // .tap((options) => ({
16 | // ...options,
17 | // transpileOnly: false,
18 | // happyPackMode: false,
19 | // }))
20 | // }
21 | // },
22 | // parallel: false,
23 | pluginOptions: {
24 | webpackBundleAnalyzer: {
25 | openAnalyzer: false,
26 | },
27 | },
28 | }
29 |
--------------------------------------------------------------------------------
/src/core/mixins/select-mixin.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import { Component, Prop } from 'vue-property-decorator'
3 |
4 | @Component
5 | export default class SelectMixin extends Vue {
6 | @Prop() schema: any
7 |
8 | get isSelect() {
9 | return this.schema.enum || this.schema.enumKeyValue
10 | }
11 |
12 | get options() {
13 | if (this.schema.enumKeyValue) return this.schema.enumKeyValue
14 | else if (this.schema.enum) {
15 | const options = this.schema.enum.map((e: string | number) => ({
16 | key: e,
17 | value: e,
18 | }))
19 | return options
20 | } else return []
21 | }
22 |
23 | getOptions() {
24 | if (this.schema.enumKeyValue) return this.schema.enumKeyValue
25 | else if (this.schema.enmu) {
26 | const options = this.schema.enum.map((e: string | number) => ({
27 | key: e,
28 | value: e,
29 | }))
30 | return options
31 | } else return []
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/core/utils/common-renderer-props.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import { Component, Prop } from 'vue-property-decorator'
3 |
4 | @Component
5 | class CommonBasePorps extends Vue {
6 | @Prop() value: any
7 | @Prop({ type: Function }) onChange: any
8 | @Prop({ type: String }) format: any
9 | @Prop({ type: Object }) schema: any
10 | @Prop() errors: any
11 | @Prop({ type: String }) title: any
12 | @Prop({ type: Boolean }) required: any
13 | @Prop({ type: Boolean }) requiredError: any
14 | @Prop({ type: String }) description: any
15 | @Prop({ type: Object }) vjsf: any
16 | }
17 |
18 | const commonBasePropsMixin = {
19 | props: {
20 | value: {},
21 | onChange: {
22 | type: Function,
23 | required: true,
24 | },
25 | format: String,
26 | schema: Object,
27 | errors: {},
28 | title: String,
29 | required: Boolean,
30 | requiredError: Boolean,
31 | description: String,
32 | vjsf: Object,
33 | },
34 | }
35 |
36 | export { CommonBasePorps, commonBasePropsMixin }
37 |
--------------------------------------------------------------------------------
/src/theme-element-ui/DateTimePicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
38 |
--------------------------------------------------------------------------------
/src/core/mixins/current-value.js:
--------------------------------------------------------------------------------
1 | // import { getDefaultValueOfSchema } from '../utils'
2 |
3 | export default function(createWithSchema = false, defaultValue) {
4 | const mixin = {
5 | data() {
6 | return {
7 | currentValue: createWithSchema ? null : defaultValue,
8 | }
9 | },
10 | watch: {
11 | currentValue(newV) {
12 | if (newV !== this.value) this.$emit('input', newV)
13 | },
14 | },
15 | }
16 |
17 | // mixin.created = function() {
18 | // // get default value from schema
19 | // this.currentValue =
20 | // this.value || getDefaultValueOfSchema(this.schema) || defaultValue
21 | // }
22 |
23 | // if (createWithSchema) {
24 | // mixin.created = function() {
25 | // // get default value from schema
26 | // this.currentValue = getDefaultValueOfSchema(this.schema)
27 | // }
28 | // } else {
29 | // mixin.created = function() {
30 | // // get default value from schema
31 | // this.currentValue = this.value
32 | // }
33 | // }
34 | return mixin
35 | }
36 |
--------------------------------------------------------------------------------
/src/theme-element-ui/FormItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
41 |
--------------------------------------------------------------------------------
/src/theme-element-ui/DatePicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
44 |
--------------------------------------------------------------------------------
/docs/implements.md:
--------------------------------------------------------------------------------
1 | # Keywords Implemented
2 |
3 | - [x] type
4 | - [x] maximum / minimum and exclusiveMaximum / exclusiveMinimum (only use in validation)
5 | - [x] maxLength/minLength
6 | - [x] pattern
7 | - [x] format
8 | - [x] formatMaximum / formatMinimum and formatExclusiveMaximum / formatExclusiveMinimum (only use in validation)
9 | - [x] uniqueItems (only use in validation)
10 | - [x] items
11 | - [x] required
12 | - [x] properties
13 | - [x] enum
14 | - [x] dependencies
15 |
16 | # Keywords in progress
17 |
18 | - [ ] multipleOf
19 | - [ ] maxItems/minItems
20 | - [ ] additionalItems (交互很难确定,比如`additionalItems: ture`理论上可以添加任何类型)
21 | - [ ] additionalProperties
22 | - [ ] const(`"const": { "$data": "1/foo" }`)
23 | - [ ] if/then/else (if we find any usefull case)
24 | - [ ] oneOf/anyOf/allOf
25 |
26 | # Keywords just in validation
27 |
28 | - [ ] not
29 |
30 | # Keywords will not be supported
31 |
32 | - [ ] patternProperties (无法想象出应用场景)
33 | - [ ] patternRequired (同上)
34 | - [ ] propertyNames (既然我们不实现 patternProperties,这个自然没什么意义)
35 |
36 | # Keywords need test case
37 |
38 | - [ ] contains
39 | - [ ] maxProperties/minProperties
40 |
--------------------------------------------------------------------------------
/src/theme-element-ui/Selection.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
18 |
19 |
20 |
21 |
22 |
43 |
--------------------------------------------------------------------------------
/src/custom-keyword.ts:
--------------------------------------------------------------------------------
1 | import { JsonSchemFormPlugin } from './core/types'
2 |
3 | /**
4 | * Ajv custom keyword doc: https://ajv.js.org/custom.html
5 | */
6 |
7 | const plugin: JsonSchemFormPlugin = {
8 | customKeywords: [
9 | {
10 | name: 'test',
11 | definition: {
12 | // validate(schema: any, data: any) {
13 | // return typeof data === 'object' && data.x === 1
14 | // },
15 | macro(schema: any) {
16 | return {
17 | ...schema,
18 | type: 'object',
19 | properties: {
20 | x: {
21 | type: 'number',
22 | minimum: 5,
23 | },
24 | },
25 | }
26 | },
27 | errors: true,
28 | },
29 | transformSchema(schema: any) {
30 | return {
31 | ...schema,
32 | type: 'object',
33 | properties: {
34 | x: {
35 | type: 'number',
36 | vjsf: {
37 | title: '测试数字',
38 | },
39 | },
40 | },
41 | }
42 | },
43 | },
44 | ],
45 | }
46 |
47 | export default plugin
48 |
--------------------------------------------------------------------------------
/src/demos/number.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'Number',
3 | schema: {
4 | type: 'object',
5 | properties: {
6 | const: {
7 | type: 'number',
8 | const: 12,
9 | vjsf: {
10 | title: '固定值',
11 | },
12 | },
13 | simple: {
14 | type: 'number',
15 | default: 5,
16 | vjsf: {
17 | placeholder: '普通',
18 | title: '普通数字',
19 | additionProps: {
20 | min: 10,
21 | },
22 | },
23 | },
24 | minMax: {
25 | type: 'number',
26 | title: '最大最小值',
27 | minimum: 10,
28 | maximum: 20,
29 | },
30 | integer: {
31 | type: 'integer',
32 | title: '整数',
33 | },
34 | select: {
35 | type: 'number',
36 | title: '选择',
37 | default: 5,
38 | enumKeyValue: [
39 | {
40 | key: 'option1',
41 | value: 0,
42 | },
43 | {
44 | key: 'option2',
45 | value: 2,
46 | },
47 | {
48 | key: 'option3',
49 | value: 3,
50 | },
51 | ],
52 | },
53 | },
54 | },
55 | }
56 |
--------------------------------------------------------------------------------
/src/theme-element-ui/TextInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
48 |
--------------------------------------------------------------------------------
/src/theme-element-ui/ArrayItemAddAction.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
32 |
33 |
47 |
--------------------------------------------------------------------------------
/src/core/mixins/error-message.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import { Component, Prop } from 'vue-property-decorator'
3 |
4 | function isDependenciesError(schemaPath: string) {
5 | return schemaPath.indexOf('/dependencies/') > 0
6 | }
7 |
8 | @Component({
9 | inject: {
10 | form: 'formContext',
11 | },
12 | })
13 | export default class ErrorMessageMixin extends Vue {
14 | form: any
15 | @Prop({ type: Boolean }) isDependenciesKey: any
16 | @Prop() requiredError: any
17 | get errorMessage() {
18 | if (this.requiredError) return '必须填写'
19 | // const errors = this.form.errors.filter(
20 | // (e: any) => e.dataPath === (this as any).path
21 | // )
22 | // return error ? error.message : ''
23 | // return this.errors[0] ? this.errors[0].message : ''
24 | return this.firstMatchedError ? this.firstMatchedError.message : ''
25 | }
26 |
27 | get firstMatchedError() {
28 | return this.errors.find((e: any) => {
29 | const schemaPath = e.schemaPath
30 | if (this.isDependenciesKey && isDependenciesError(schemaPath)) {
31 | return false
32 | }
33 | return true
34 | })
35 | }
36 |
37 | get errors() {
38 | const errors = this.form.errors.filter(
39 | (e: any) => e.dataPath === (this as any).path
40 | )
41 | return errors
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/theme-element-ui/TimePicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
14 |
47 |
--------------------------------------------------------------------------------
/tests/unit/default-value/boolean.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 |
3 | import { initialize, Helper as Wrapper } from '../utils'
4 |
5 | initialize()
6 |
7 | describe('boolean renderer default values', () => {
8 | it('should use `false` when no default', async () => {
9 | const wrapper: any = mount(Wrapper, {
10 | data: () => ({
11 | schema: {
12 | type: 'boolean',
13 | },
14 | value: undefined,
15 | }),
16 | })
17 | await wrapper.vm.$nextTick()
18 | expect(wrapper.vm.value).toBe(false)
19 | })
20 |
21 | it('should use `!!default` when default provided', async () => {
22 | const wrapper: any = mount(Wrapper, {
23 | data: () => ({
24 | schema: {
25 | type: 'boolean',
26 | default: 1,
27 | },
28 | value: undefined,
29 | }),
30 | })
31 | await wrapper.vm.$nextTick()
32 | expect(wrapper.vm.value).toBe(true)
33 | })
34 |
35 | it('should use `value` when value provided', async () => {
36 | const wrapper: any = mount(Wrapper, {
37 | data: () => ({
38 | schema: {
39 | type: 'boolean',
40 | default: 1,
41 | },
42 | value: false,
43 | }),
44 | })
45 | await wrapper.vm.$nextTick()
46 | expect(wrapper.vm.value).toBe(false)
47 | })
48 | })
49 |
--------------------------------------------------------------------------------
/docs/renderer-props.md:
--------------------------------------------------------------------------------
1 | 对于每个最终的渲染组件,我们提供一套标准的 props,来帮助我们更好的开发渲染主题
2 |
3 | ### FormItem Props
4 |
5 | ```js
6 | {
7 | // 是否是必填
8 | required: boolean,
9 | // schema 对象
10 | schema: schema,
11 | // 第一个匹配的错误,注意这个会跳过`dependencies`相关的错误
12 | firstMatchedError: this.firstMatchedError,
13 | // 是否必须填写错误
14 | requiredError: boolean,
15 | // 所有错误
16 | errors: this.errors,
17 | // label文本
18 | label: vjsf.title || path,
19 | // 描述
20 | description: vjsf.description,
21 | }
22 | ```
23 |
24 | ### Renderer Props
25 |
26 | ```js
27 | {
28 | // 值
29 | value: value,
30 | // 数据变化回调
31 | onChange: onChange,
32 | // 格式化
33 | format: this.schema.format,
34 | // schema 对象
35 | schema: this.schema,
36 | // 所有错误
37 | errors: this.errors,
38 | // 标题
39 | title: vjsf.title || path,
40 | // 是否必填
41 | required: this.required,
42 | // 是否必填错误
43 | requiredError: this.requiredError,
44 | // 描述
45 | description: vjsf.description,
46 | ...this.vjsf.additionProps,
47 | }
48 | ```
49 |
50 | ### vjsf
51 |
52 | 在定义 schema 的时候,我们可以增加一个帮助我们更好得渲染表单的配置对象,比如:
53 |
54 | ```js
55 | const schema = {
56 | type: 'string',
57 | vjsf: {
58 | title: '名字',
59 | description: '描述',
60 | component: 'your-input-component', // 通过该字段可以实现自定义组件
61 | additionProps: { ...props }, // 如果你有对于渲染组件自定义的props,可以通过该对象传递
62 | ...others,
63 | },
64 | }
65 | ```
66 |
--------------------------------------------------------------------------------
/src/demos/string.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'String',
3 | schema: {
4 | type: 'object',
5 | vjsf: {
6 | title: 'string demo',
7 | },
8 | default: {
9 | simple: '123',
10 | },
11 | required: ['simple'],
12 | properties: {
13 | const: {
14 | type: 'string',
15 | const: 'jokcy',
16 | vjsf: {
17 | title: '固定值',
18 | description: '123',
19 | },
20 | },
21 | simple: {
22 | type: 'string',
23 | minLength: 2,
24 | default: 'haha',
25 | pattern: '/^abc&/',
26 | errorMessage: {
27 | pattern: '请填写正确的内容',
28 | },
29 | vjsf: {
30 | placeholder: '请输入普通字符串',
31 | title: '普通字符串',
32 | },
33 | },
34 | color: {
35 | type: 'string',
36 | format: 'color',
37 | title: '颜色选择',
38 | },
39 | select: {
40 | type: 'string',
41 | vjsf: {
42 | title: '选择',
43 | placeholder: '选择框啦',
44 | },
45 | enumKeyValue: [
46 | {
47 | key: 'option1',
48 | value: '1',
49 | },
50 | {
51 | key: 'option2',
52 | value: '2',
53 | },
54 | {
55 | key: 'option3',
56 | value: '3',
57 | },
58 | ],
59 | },
60 | },
61 | },
62 | }
63 |
--------------------------------------------------------------------------------
/src/core/schme-type-renderers/CustomRenderer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
55 |
--------------------------------------------------------------------------------
/src/core/schme-type-renderers/NumberRenderer.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
14 |
15 |
16 |
49 |
--------------------------------------------------------------------------------
/tests/unit/theme-element/constant.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 |
3 | import { initialize, Helper as Wrapper } from '../utils'
4 |
5 | initialize()
6 |
7 | describe('constant component', () => {
8 | it('format `const` should render constant component', async () => {
9 | const wrapper: any = mount(Wrapper, {
10 | data: () => ({
11 | schema: {
12 | type: 'string',
13 | const: 'const-value',
14 | },
15 | }),
16 | })
17 | const picker = wrapper.find({ name: 'JsfConstant' })
18 | expect(picker.vm).not.toBeUndefined()
19 | })
20 | it('when type of value is string, el value should be the same as const', async () => {
21 | const wrapper: any = mount(Wrapper, {
22 | data: () => ({
23 | schema: {
24 | type: 'string',
25 | const: 'const-value',
26 | },
27 | }),
28 | })
29 | const picker = wrapper.find({ name: 'JsfConstant' })
30 | await wrapper.vm.$nextTick()
31 | expect(picker.vm.value).toBe('const-value')
32 | })
33 | it('when type of value is number, el value should be the same as const', async () => {
34 | const wrapper: any = mount(Wrapper, {
35 | data: () => ({
36 | schema: {
37 | type: 'string',
38 | const: 12,
39 | },
40 | }),
41 | })
42 | const picker = wrapper.find({ name: 'JsfConstant' })
43 | await wrapper.vm.$nextTick()
44 | expect(picker.vm.value).toBe(12)
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/src/demos/deps.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'Dependencies',
3 | schema: {
4 | type: 'object',
5 | propertiesOrder: ['selected', 'name1', 'name2', 'name3', 'others'],
6 | properties: {
7 | selected: {
8 | type: 'number',
9 | title: '是否选中',
10 | enum: [1, 2, 3],
11 | default: 2,
12 | },
13 | others: {
14 | type: 'string',
15 | },
16 | },
17 | dependencies: {
18 | selected: {
19 | oneOf: [
20 | {
21 | properties: {
22 | selected: {
23 | // const: 1,
24 |
25 | const: 1,
26 | },
27 | name1: {
28 | type: 'string',
29 | title: '名字1',
30 | },
31 | },
32 | required: ['name1'],
33 | },
34 | {
35 | properties: {
36 | selected: {
37 | const: 2,
38 | },
39 | name2: {
40 | type: 'string',
41 | title: '名字2',
42 | },
43 | },
44 | required: ['name2'],
45 | },
46 | {
47 | properties: {
48 | selected: {
49 | const: 3,
50 | },
51 | name3: {
52 | type: 'string',
53 | title: '名字3',
54 | },
55 | },
56 | required: ['name3'],
57 | },
58 | ],
59 | },
60 | },
61 | },
62 | }
63 |
--------------------------------------------------------------------------------
/src/demos/nest-deps.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'Nested Object',
3 | schema: {
4 | type: 'object',
5 | properties: {
6 | object: {
7 | type: 'object',
8 | properties: {
9 | selected: {
10 | type: 'number',
11 | title: '是否选中',
12 | enum: [1, 2, 3],
13 | },
14 | },
15 | dependencies: {
16 | selected: {
17 | oneOf: [
18 | {
19 | properties: {
20 | selected: {
21 | const: 1,
22 | },
23 | name1: {
24 | type: 'string',
25 | title: '名字1',
26 | },
27 | },
28 | required: ['name1'],
29 | },
30 | {
31 | properties: {
32 | selected: {
33 | const: 2,
34 | },
35 | name2: {
36 | type: 'string',
37 | title: '名字2',
38 | },
39 | },
40 | required: ['name2'],
41 | },
42 | {
43 | properties: {
44 | selected: {
45 | const: 3,
46 | },
47 | name3: {
48 | type: 'string',
49 | title: '名字3',
50 | },
51 | },
52 | required: ['name3'],
53 | },
54 | ],
55 | },
56 | },
57 | },
58 | },
59 | },
60 | }
61 |
--------------------------------------------------------------------------------
/src/theme-element-ui/base/DatePicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
61 |
--------------------------------------------------------------------------------
/tests/unit/theme-element/switch.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 |
3 | import { initialize, Helper as Wrapper } from '../utils'
4 |
5 | initialize()
6 |
7 | describe('switch component', () => {
8 | it('format `switch` should render switch component', async () => {
9 | const wrapper: any = mount(Wrapper, {
10 | data: () => ({
11 | schema: {
12 | type: 'boolean',
13 | },
14 | }),
15 | })
16 | const picker = wrapper.find({ name: 'JsfSwitch' })
17 | expect(picker.vm).not.toBeUndefined()
18 | })
19 | it('el value should be the same as value', () => {
20 | const wrapper: any = mount(Wrapper, {
21 | data: () => ({
22 | schema: {
23 | type: 'boolean',
24 | },
25 | value: true,
26 | }),
27 | })
28 | const picker = wrapper.find({ name: 'JsfSwitch' })
29 | expect(picker.vm.value).toBe(true)
30 | })
31 | it('el value should be the same as default', async () => {
32 | const wrapper: any = mount(Wrapper, {
33 | data: () => ({
34 | schema: {
35 | type: 'boolean',
36 | default: true,
37 | },
38 | value: undefined,
39 | }),
40 | })
41 | const picker = wrapper.find({ name: 'JsfSwitch' })
42 | await wrapper.vm.$nextTick()
43 | expect(picker.vm.value).toBe(true)
44 | })
45 | it('value should change when el emit input event', () => {
46 | const wrapper: any = mount(Wrapper, {
47 | data: () => ({
48 | schema: {
49 | type: 'boolean',
50 | },
51 | value: undefined,
52 | }),
53 | })
54 | const elPicker = wrapper.find({ name: 'JsfSwitch' })
55 | elPicker.vm.$emit('input', true)
56 | expect(wrapper.vm.value).toBe(true)
57 | })
58 | })
59 |
--------------------------------------------------------------------------------
/tests/unit/theme-element/numberInput.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 |
3 | import { initialize, Helper as Wrapper } from '../utils'
4 |
5 | initialize()
6 |
7 | describe('numberInput', () => {
8 | it('format `number` should render numberInput component', async () => {
9 | const wrapper: any = mount(Wrapper, {
10 | data: () => ({
11 | schema: {
12 | type: 'number',
13 | },
14 | }),
15 | })
16 | const picker = wrapper.find({ name: 'JsfNumberInput' })
17 | expect(picker.vm).not.toBeUndefined()
18 | })
19 | it('el value should be the same as value', () => {
20 | const wrapper: any = mount(Wrapper, {
21 | data: () => ({
22 | schema: {
23 | type: 'number',
24 | },
25 | value: 67,
26 | }),
27 | })
28 | const picker = wrapper.find({ name: 'JsfNumberInput' })
29 | expect(picker.vm.value).toBe(67)
30 | })
31 | it('el value should be the same as default', async () => {
32 | const wrapper: any = mount(Wrapper, {
33 | data: () => ({
34 | schema: {
35 | type: 'number',
36 | default: 83,
37 | },
38 | value: undefined,
39 | }),
40 | })
41 | const picker = wrapper.find({ name: 'JsfNumberInput' })
42 | await wrapper.vm.$nextTick()
43 | expect(picker.vm.value).toBe(83)
44 | })
45 | it('value should change when el emit input event', () => {
46 | const wrapper: any = mount(Wrapper, {
47 | data: () => ({
48 | schema: {
49 | type: 'number',
50 | },
51 | value: undefined,
52 | }),
53 | })
54 | const elPicker = wrapper.find({ name: 'JsfNumberInput' })
55 | elPicker.vm.$emit('input', 35)
56 | expect(wrapper.vm.value).toBe(35)
57 | })
58 | })
59 |
--------------------------------------------------------------------------------
/tests/unit/theme-element/textInput.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 |
3 | import { initialize, Helper as Wrapper } from '../utils'
4 |
5 | initialize()
6 |
7 | describe('textInput', () => {
8 | it('format `string` should render textInput component', async () => {
9 | const wrapper: any = mount(Wrapper, {
10 | data: () => ({
11 | schema: {
12 | type: 'string',
13 | },
14 | }),
15 | })
16 | const picker = wrapper.find({ name: 'JsfTextInput' })
17 | expect(picker.vm).not.toBeUndefined()
18 | })
19 | it('el value should be the same as value', async () => {
20 | const wrapper: any = mount(Wrapper, {
21 | data: () => ({
22 | schema: {
23 | type: 'string',
24 | },
25 | value: 'yes',
26 | }),
27 | })
28 | const picker = wrapper.find({ name: 'JsfTextInput' })
29 | expect(picker.vm.value).toBe('yes')
30 | })
31 | it('el value should be the same as default', async () => {
32 | const wrapper: any = mount(Wrapper, {
33 | data: () => ({
34 | schema: {
35 | type: 'string',
36 | default: 'yes',
37 | },
38 | value: undefined,
39 | }),
40 | })
41 | const picker = wrapper.find({ name: 'JsfTextInput' })
42 | await wrapper.vm.$nextTick()
43 | expect(picker.vm.value).toBe('yes')
44 | })
45 | it('value should change when el emit input event', async () => {
46 | const wrapper: any = mount(Wrapper, {
47 | data: () => ({
48 | schema: {
49 | type: 'string',
50 | },
51 | value: undefined,
52 | }),
53 | })
54 | const picker = wrapper.find({ name: 'JsfTextInput' })
55 | picker.vm.$emit('input', 'yes')
56 | expect(wrapper.vm.value).toBe('yes')
57 | })
58 | })
59 |
--------------------------------------------------------------------------------
/src/core/validator/index.ts:
--------------------------------------------------------------------------------
1 | import Ajv, { Options } from 'ajv'
2 | import ajvErrors from 'ajv-errors'
3 |
4 | import { CreateInstanceOptions, ConstantCreateInstanceOptions } from './types'
5 | import defaultOptions from './instance-default-options'
6 |
7 | export * from './types'
8 |
9 | function mergeMaybeArray(a1: any, a2: any) {
10 | const f1 = a1 ? (Array.isArray(a1) ? a1 : [a1]) : []
11 | const f2 = a2 ? (Array.isArray(a2) ? a2 : [a2]) : []
12 | return [...f1, ...f2]
13 | }
14 |
15 | function mergerOptions(
16 | op1: CreateInstanceOptions,
17 | op2: CreateInstanceOptions
18 | ): ConstantCreateInstanceOptions {
19 | const ajvOption: Options = {
20 | ...op1.options,
21 | ...op2.options,
22 | }
23 |
24 | const formats = mergeMaybeArray(op1.formats, op2.formats) // TODO: 去除 name 相同的?考虑名称相同但是 `number` 和 `string` 不同
25 | const keywords = mergeMaybeArray(op1.keywords, op2.keywords)
26 |
27 | return {
28 | // locale: op2.locale || op1.locale || 'zh',
29 | options: ajvOption,
30 | formats,
31 | keywords,
32 | }
33 | }
34 |
35 | export function createInstance(opts: CreateInstanceOptions = {}) {
36 | const options = mergerOptions(defaultOptions, opts)
37 |
38 | const { options: instanceOptions, formats, keywords } = options
39 |
40 | const ajv = new Ajv(instanceOptions)
41 | ajvErrors(ajv as any)
42 |
43 | formats.forEach(({ name, definition }) => ajv.addFormat(name, definition))
44 | keywords.forEach(({ name, definition }) => ajv.addKeyword(name, definition))
45 |
46 | // ajv.validate
47 |
48 | return ajv
49 | }
50 |
51 | const defaultInstance = createInstance({})
52 |
53 | export function validateData(schema: any, data: any) {
54 | const valid = defaultInstance.validate(schema, data)
55 | return {
56 | valid,
57 | errors: defaultInstance.errors,
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/demos/complex.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'Complex Demo',
3 | schema: {
4 | type: 'object',
5 | propertiesOrder: ['name', 'age', 'fimaly', 'pets', 'petName'],
6 | properties: {
7 | name: {
8 | type: 'string',
9 | minLength: 5,
10 | },
11 | age: {
12 | type: 'number',
13 | maximum: 10,
14 | minimum: 5,
15 | },
16 | fimaly: {
17 | type: 'array',
18 | items: {
19 | type: 'object',
20 | properties: {
21 | name: {
22 | type: 'string',
23 | title: '名字',
24 | },
25 | releation: {
26 | type: 'string',
27 | title: '关系',
28 | },
29 | },
30 | required: ['name', 'releation'],
31 | },
32 | },
33 | pets: {
34 | type: 'string',
35 | enum: ['NO', 'ONE'],
36 | },
37 | nest: {
38 | type: 'object',
39 | properties: {
40 | nest1: {
41 | type: 'string',
42 | },
43 | nest2: {
44 | type: 'string',
45 | },
46 | },
47 | required: ['nest1'],
48 | },
49 | },
50 | dependencies: {
51 | pets: {
52 | oneOf: [
53 | {
54 | properties: {
55 | pets: {
56 | const: 'NO',
57 | },
58 | },
59 | },
60 | {
61 | properties: {
62 | pets: {
63 | const: 'ONE',
64 | },
65 | petName: {
66 | type: 'string',
67 | },
68 | },
69 | required: ['petName'],
70 | },
71 | ],
72 | },
73 | },
74 | required: ['name', 'age'],
75 | },
76 | }
77 |
--------------------------------------------------------------------------------
/docs/dependencies.md:
--------------------------------------------------------------------------------
1 | # demo
2 |
3 | ```js
4 | export default {
5 | name: '依赖关系',
6 | schema: {
7 | type: 'object',
8 | properties: {
9 | selected: {
10 | type: 'number',
11 | title: '是否选中',
12 | enum: [1, 2, 3],
13 | },
14 | },
15 | dependencies: {
16 | selected: {
17 | oneOf: [
18 | {
19 | properties: {
20 | selected: {
21 | // const: 1,
22 |
23 | const: 1,
24 | },
25 | name1: {
26 | type: 'string',
27 | title: '名字1',
28 | },
29 | },
30 | required: ['name1'],
31 | },
32 | {
33 | properties: {
34 | selected: {
35 | const: 2,
36 | },
37 | name2: {
38 | type: 'string',
39 | title: '名字2',
40 | },
41 | },
42 | required: ['name2'],
43 | },
44 | {
45 | properties: {
46 | selected: {
47 | const: 3,
48 | },
49 | name3: {
50 | type: 'string',
51 | title: '名字3',
52 | },
53 | },
54 | required: ['name3'],
55 | },
56 | ],
57 | },
58 | },
59 | },
60 | }
61 | ```
62 |
63 | # 解释
64 |
65 | 通过`dependencies`声明依赖关系,`dependencies`的`key`是依赖选项,比如在这里`selected`是依赖项,在`selected`有值的情况下才会展示和执行他包含的内容。
66 |
67 | `selected`的值是一个`oneOf`则对应我们对于`selected`不同的结果会现实其中某个结果,比如在这里如果:
68 |
69 | - `selected`是`1`,则我们必须填写`name1`
70 | - `selected`是`2`,则我们必须填写`name2`
71 | - `selected`是`3`,则我们必须填写`name3`
72 |
73 | 注意这里我们在每一项中都声明了一个`selected`,他的类型是`const`也就是固定值,以此我们来强制区分不同的结果,符合`selected`为`1`的结果必定不会符合其他的选项,就完全符合`oneOf`的逻辑。
74 |
--------------------------------------------------------------------------------
/src/core/utils/common.ts:
--------------------------------------------------------------------------------
1 | export function isObject(thing: any) {
2 | return typeof thing === 'object' && thing !== null && !Array.isArray(thing)
3 | }
4 |
5 | export function isEmptyObject(thing: any) {
6 | return isObject(thing) && Object.keys(thing).length === 0
7 | }
8 |
9 | export function hasOwnProperty(obj: any, key: string) {
10 | /**
11 | * 直接调用`obj.hasOwnProperty`有可能会因为
12 | * obj 覆盖了 prototype 上的 hasOwnProperty 而产生错误
13 | */
14 | return Object.prototype.hasOwnProperty.call(obj, key)
15 | }
16 |
17 | /* Gets the type of a given schema. */
18 | export function getSchemaType(schema: any) {
19 | let { type } = schema
20 |
21 | if (type) return type
22 |
23 | if (!type && schema.const) {
24 | return guessType(schema.const)
25 | }
26 |
27 | if (!type && schema.enum) {
28 | return 'string'
29 | }
30 |
31 | if (!type && (schema.properties || schema.additionalProperties)) {
32 | return 'object'
33 | }
34 |
35 | console.warn('can not guess schema type, just use object', schema)
36 | return 'object'
37 |
38 | // if (type instanceof Array && type.length === 2 && type.includes('null')) {
39 | // return type.find((type) => type !== 'null')
40 | // }
41 | }
42 |
43 | // In the case where we have to implicitly create a schema, it is useful to know what type to use
44 | // based on the data we are defining
45 | export const guessType = function guessType(value: any) {
46 | if (Array.isArray(value)) {
47 | return 'array'
48 | } else if (typeof value === 'string') {
49 | return 'string'
50 | } else if (value == null) {
51 | return 'null'
52 | } else if (typeof value === 'boolean') {
53 | return 'boolean'
54 | } else if (!isNaN(value)) {
55 | return 'number'
56 | } else if (typeof value === 'object') {
57 | return 'object'
58 | }
59 | // Default to string if we can't figure it out
60 | return 'string'
61 | }
62 |
--------------------------------------------------------------------------------
/src/theme-element-ui/NumberInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 |
16 |
61 |
62 |
73 |
--------------------------------------------------------------------------------
/tests/unit/theme-element/colorPicker.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 |
3 | import { initialize, Helper as Wrapper } from '../utils'
4 |
5 | initialize()
6 |
7 | describe('colorPicker', () => {
8 | it('format `color` should render colorpicker', async () => {
9 | const wrapper: any = mount(Wrapper, {
10 | data: () => ({
11 | schema: {
12 | type: 'string',
13 | format: 'color',
14 | },
15 | }),
16 | })
17 | const picker = wrapper.find({ name: 'JsfColorPicker' })
18 | expect(picker.vm).not.toBeUndefined()
19 | })
20 | it('el value should be the same as value', () => {
21 | const wrapper: any = mount(Wrapper, {
22 | data: () => ({
23 | schema: {
24 | type: 'string',
25 | format: 'color',
26 | },
27 | value: 'red',
28 | }),
29 | })
30 | const picker = wrapper.find({ name: 'JsfColorPicker' })
31 | expect(picker.vm.value).toBe('red')
32 | })
33 | it('el value should be the same as default', async () => {
34 | const wrapper: any = mount(Wrapper, {
35 | data: () => ({
36 | schema: {
37 | type: 'string',
38 | format: 'color',
39 | default: 'red',
40 | },
41 | value: undefined,
42 | }),
43 | })
44 | const picker = wrapper.find({ name: 'JsfColorPicker' })
45 | await wrapper.vm.$nextTick()
46 | expect(picker.vm.value).toBe('red')
47 | })
48 | it('value should change when el emit input event', () => {
49 | const wrapper: any = mount(Wrapper, {
50 | data: () => ({
51 | schema: {
52 | type: 'string',
53 | format: 'color',
54 | },
55 | value: undefined,
56 | }),
57 | })
58 | const elPicker = wrapper.find({ name: 'JsfColorPicker' })
59 | elPicker.vm.$emit('input', 'red')
60 | expect(wrapper.vm.value).toBe('red')
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/src/demos/date.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'Date Time',
3 | schema: {
4 | type: 'object',
5 | properties: {
6 | simpleDate: {
7 | type: 'string',
8 | format: 'date',
9 | title: '日期',
10 | },
11 | numberDate: {
12 | type: 'number',
13 | format: 'date',
14 | title: '时间戳',
15 | // vjsf: {
16 | // additionProps: {
17 | // readonly: true,
18 | // },
19 | // },
20 | },
21 | simpleDateTime: {
22 | type: 'string',
23 | format: 'date-time',
24 | title: '日期时间',
25 | },
26 | simpleTime: {
27 | type: 'string',
28 | format: 'time',
29 | vjsf: {
30 | title: '时间字符串',
31 | placeholder: '选择时间',
32 | },
33 | },
34 | simpleTimeStamp: {
35 | type: 'number',
36 | format: 'time',
37 | title: '只有时间时间戳',
38 | },
39 | numberDateTime: {
40 | type: 'number',
41 | format: 'date-time',
42 | title: '日期时间时间戳',
43 | },
44 | simpleDateRange: {
45 | type: 'array',
46 | items: {
47 | type: 'string',
48 | format: 'date',
49 | },
50 | dateRange: true,
51 | title: '日期区间',
52 | },
53 | simpleDateTimeRange: {
54 | type: 'array',
55 | items: {
56 | type: 'string',
57 | format: 'date-time',
58 | },
59 | dateRange: true,
60 | title: '日期时间区间',
61 | },
62 | numberDateTimeRange: {
63 | type: 'array',
64 | items: {
65 | type: 'number',
66 | format: 'date-time',
67 | },
68 | dateRange: true,
69 | title: '日期时间区间时间戳',
70 | },
71 | timeRange: {
72 | type: 'array',
73 | items: {
74 | type: 'number',
75 | format: 'time',
76 | },
77 | dateTimeRange: true,
78 | vjsf: {
79 | title: '时间区间',
80 | },
81 | },
82 | },
83 | },
84 | }
85 |
--------------------------------------------------------------------------------
/src/theme-element-ui/base/ArrayItemActions.vue:
--------------------------------------------------------------------------------
1 |
2 |
25 |
26 |
27 |
57 |
58 |
69 |
--------------------------------------------------------------------------------
/src/core/validator/date-time-range-keyword.ts:
--------------------------------------------------------------------------------
1 | import { KeywordDefinition } from 'ajv'
2 | import { isObject } from '../utils'
3 |
4 | const DateRangeKeyword: KeywordDefinition = {
5 | type: 'boolean',
6 | compile(schema) {
7 | const items = schema.items
8 | const type = schema.type
9 |
10 | const validFun = () => true
11 |
12 | if (type !== 'array' || !isObject(items)) {
13 | // throw new Error('date time range keyword should use single time array')
14 | return validFun
15 | }
16 | const itemType: string = items.type
17 | const itemFormat: string = items.format
18 |
19 | if (
20 | (itemType !== 'number' && itemType !== 'string') ||
21 | (itemFormat !== 'date' && itemFormat !== 'date-time')
22 | ) {
23 | // throw new Error('date time must be `number` or `string` type')
24 | return validFun
25 | }
26 |
27 | return (data) => {
28 | const [before, after] = data
29 | return Date.parse(before) <= Date.parse(after)
30 | }
31 | },
32 | macro() {
33 | return {
34 | minItems: 2,
35 | maxItems: 2,
36 | }
37 | },
38 | }
39 |
40 | const DateTimeRangeKeyword: KeywordDefinition = {
41 | type: 'boolean',
42 | compile(schema) {
43 | const items = schema.items
44 | const type = schema.type
45 |
46 | const validFun = () => true
47 |
48 | if (type !== 'array' || !isObject(items)) {
49 | // throw new Error('date time range keyword should use single time array')
50 | return validFun
51 | }
52 | const itemType: string = items.type
53 | const itemFormat: string = items.format
54 |
55 | if (
56 | (itemType !== 'number' && itemType !== 'string') ||
57 | (itemFormat !== 'date' &&
58 | itemFormat !== 'date-time' &&
59 | itemFormat !== 'time')
60 | ) {
61 | // throw new Error('date time must be `number` or `string` type')
62 | return validFun
63 | }
64 |
65 | return (data) => {
66 | const [before, after] = data
67 | return Date.parse(before) <= Date.parse(after)
68 | }
69 | },
70 | macro() {
71 | return {
72 | minItems: 2,
73 | maxItems: 2,
74 | }
75 | },
76 | }
77 |
78 | export { DateTimeRangeKeyword, DateRangeKeyword }
79 |
--------------------------------------------------------------------------------
/src/theme-element-ui/DateTimeRangePicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
20 |
21 |
22 |
23 |
63 |
64 |
75 |
--------------------------------------------------------------------------------
/tests/unit/value-change/array-value.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 |
3 | import { initialize, Helper as Wrapper } from '../utils'
4 |
5 | initialize()
6 |
7 | describe('array value change', () => {
8 | it('sort down should work', async () => {
9 | const wrapper: any = mount(Wrapper, {
10 | data: () => ({
11 | schema: {
12 | type: 'array',
13 | items: {
14 | type: 'string',
15 | },
16 | },
17 | value: ['a', 'b'],
18 | }),
19 | })
20 | const aw = wrapper.find({ name: 'JsfSingleTypeArrayWrapper' })
21 | expect(aw).not.toBeUndefined()
22 | aw.vm.onDown('a', 0)
23 | expect(wrapper.vm.value).toEqual(['b', 'a'])
24 | })
25 |
26 | it('sort up should work', async () => {
27 | const wrapper: any = mount(Wrapper, {
28 | data: () => ({
29 | schema: {
30 | type: 'array',
31 | items: {
32 | type: 'string',
33 | },
34 | },
35 | value: ['a', 'b'],
36 | }),
37 | })
38 | const aw = wrapper.find({ name: 'JsfSingleTypeArrayWrapper' })
39 | expect(aw).not.toBeUndefined()
40 | aw.vm.onUp('b', 1)
41 | expect(wrapper.vm.value).toEqual(['b', 'a'])
42 | })
43 |
44 | it('delete should work', async () => {
45 | const wrapper: any = mount(Wrapper, {
46 | data: () => ({
47 | schema: {
48 | type: 'array',
49 | items: {
50 | type: 'string',
51 | },
52 | },
53 | value: ['a', 'b'],
54 | }),
55 | })
56 | const aw = wrapper.find({ name: 'JsfSingleTypeArrayWrapper' })
57 | expect(aw).not.toBeUndefined()
58 | aw.vm.onDelete('a', 0)
59 | expect(wrapper.vm.value).toEqual(['b'])
60 | })
61 |
62 | it('add should work', async () => {
63 | const wrapper: any = mount(Wrapper, {
64 | data: () => ({
65 | schema: {
66 | type: 'array',
67 | items: {
68 | type: 'string',
69 | },
70 | },
71 | value: ['a', 'b'],
72 | }),
73 | })
74 | const aw = wrapper.find({ name: 'JsfSingleTypeArrayWrapper' })
75 | expect(aw).not.toBeUndefined()
76 | aw.vm.onAdd('a', 0)
77 | expect(wrapper.vm.value).toEqual(['a', undefined, 'b'])
78 | })
79 | })
80 |
--------------------------------------------------------------------------------
/src/core/schme-type-renderers/StringRenderer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
16 |
17 |
18 |
19 |
80 |
--------------------------------------------------------------------------------
/tests/unit/default-value/number.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 |
3 | import { initialize, Helper as Wrapper } from '../utils'
4 |
5 | initialize()
6 |
7 | describe('number renderer default values', () => {
8 | it('should use `default` as default value when value is undefined', async () => {
9 | const wrapper: any = mount(Wrapper, {
10 | data: () => ({
11 | schema: {
12 | type: 'number',
13 | default: 10,
14 | },
15 | value: undefined,
16 | }),
17 | })
18 | await wrapper.vm.$nextTick()
19 | expect(wrapper.vm.value).toEqual(10)
20 | })
21 |
22 | it('should use value when value is not undefined', async () => {
23 | const wrapper: any = mount(Wrapper, {
24 | data: () => ({
25 | schema: {
26 | type: 'number',
27 | default: 10,
28 | },
29 | value: 0,
30 | }),
31 | })
32 | await wrapper.vm.$nextTick()
33 | expect(wrapper.vm.value).toEqual(0)
34 | })
35 |
36 | it('should use `default` as default value when in object', async () => {
37 | const wrapper: any = mount(Wrapper, {
38 | data: () => ({
39 | schema: {
40 | type: 'object',
41 | properties: {
42 | age: {
43 | type: 'number',
44 | default: 10,
45 | },
46 | },
47 | },
48 | value: {},
49 | }),
50 | })
51 | await wrapper.vm.$nextTick()
52 | expect(wrapper.vm.value.age).toEqual(10)
53 | })
54 |
55 | it('should not create default value when `default` not provided', async () => {
56 | const wrapper: any = mount(Wrapper, {
57 | data: () => ({
58 | schema: {
59 | type: 'object',
60 | properties: {
61 | age: {
62 | type: 'number',
63 | },
64 | },
65 | },
66 | value: {},
67 | }),
68 | })
69 | await wrapper.vm.$nextTick()
70 | expect(wrapper.vm.value.age).toBeUndefined()
71 | })
72 |
73 | it('should use object default value as default', async () => {
74 | const wrapper: any = mount(Wrapper, {
75 | data: () => ({
76 | schema: {
77 | type: 'object',
78 | properties: {
79 | age: {
80 | type: 'number',
81 | },
82 | },
83 | default: {
84 | age: 20,
85 | },
86 | },
87 | // value: {}, // 如果object赋予对象,则不会使用default
88 | value: '123',
89 | }),
90 | })
91 | await wrapper.vm.$nextTick()
92 | expect(wrapper.vm.value.age).toEqual(20)
93 | })
94 | })
95 |
--------------------------------------------------------------------------------
/src/demos/array.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'Array',
3 | schema: {
4 | type: 'object',
5 | properties: {
6 | name: {
7 | type: 'string',
8 | },
9 | fixed: {
10 | type: 'array',
11 | title: '固定数组',
12 | items: [
13 | { type: 'string', title: '名字' },
14 | { type: 'number', title: '年龄' },
15 | {
16 | type: 'object',
17 | title: '一项',
18 | properties: {
19 | 'name/1': {
20 | type: 'string',
21 | title: '名字',
22 | minLength: 5,
23 | },
24 | },
25 | },
26 | ],
27 | // default: ['Jokcy', 12],
28 | },
29 | singleType: {
30 | type: 'array',
31 | title: '单一数组',
32 | items: {
33 | type: 'string',
34 | },
35 | default: ['aaa', 'bbb'],
36 | },
37 | nestSingleType: {
38 | type: 'array',
39 | title: '嵌套单一数组',
40 | items: {
41 | type: 'object',
42 | properties: {
43 | singleType: {
44 | type: 'array',
45 | title: '单一数组',
46 | items: {
47 | type: 'string',
48 | },
49 | default: ['aaa', 'bbb'],
50 | },
51 | },
52 | },
53 | default: ['aaa', 'bbb'],
54 | },
55 | multiChoice: {
56 | type: 'array',
57 | title: '多选',
58 | default: null,
59 | items: {
60 | type: 'string',
61 | enumKeyValue: [
62 | {
63 | key: 'option1',
64 | value: '1',
65 | },
66 | {
67 | key: 'option2',
68 | value: '2',
69 | },
70 | {
71 | key: 'option3',
72 | value: '3',
73 | },
74 | ],
75 | },
76 | },
77 | objectArray: {
78 | type: 'array',
79 | title: '对象数组',
80 | default: [
81 | {
82 | name: 'aaa',
83 | age: 12,
84 | },
85 | ],
86 | items: {
87 | type: 'object',
88 | // title: '对象',
89 | properties: {
90 | name: {
91 | type: 'string',
92 | title: '名字',
93 | },
94 | age: {
95 | type: 'number',
96 | title: '年龄',
97 | },
98 | },
99 | },
100 | },
101 | },
102 | },
103 | }
104 |
--------------------------------------------------------------------------------
/src/core/SchemaItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
79 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@zdwh/vue-json-schema-form",
3 | "version": "1.0.18",
4 | "private": false,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build:core": "vue-cli-service build --target lib --name index src/core/index.ts --no-clean",
8 | "build:theme": "vue-cli-service build --target lib --name theme-element/index src/theme-element-ui/index.ts --no-clean",
9 | "build:doc": "TYPE=doc vue-cli-service build",
10 | "build": "npm run clean && npm run build:core && npm run build:theme",
11 | "clean": "rimraf dist",
12 | "test:unit": "vue-cli-service test:unit",
13 | "test:coverage": "vue-cli-service test:unit --coverage",
14 | "test:unit:watch": "vue-cli-service test:unit --watch",
15 | "lint": "vue-cli-service lint",
16 | "pub": "npm run build && npm version patch && npm publish --access=public"
17 | },
18 | "main": "dist/index.common.js",
19 | "publishConfig": {
20 | "registry": "https://registry.npmjs.org/"
21 | },
22 | "license": "MIT",
23 | "files": [
24 | "dist"
25 | ],
26 | "gitHooks": {
27 | "pre-commit": "npm run lint"
28 | },
29 | "dependencies": {
30 | "jsonpointer": "^4.0.1",
31 | "lodash.clonedeep": "^4.5.0",
32 | "lodash.union": "^4.6.0",
33 | "vue-class-component": "^7.2.3",
34 | "vue-property-decorator": "^8.4.1"
35 | },
36 | "devDependencies": {
37 | "@types/ajv-errors": "^1.0.2",
38 | "@types/jest": "^24.0.19",
39 | "@types/jsonpointer": "^4.0.0",
40 | "@types/lodash": "^4.14.152",
41 | "@types/lodash.clonedeep": "^4.5.6",
42 | "@types/lodash.union": "^4.6.6",
43 | "@types/node": "^13.13.0",
44 | "@typescript-eslint/eslint-plugin": "^2.18.0",
45 | "@typescript-eslint/parser": "^2.18.0",
46 | "@vue/cli-plugin-babel": "~4.2.0",
47 | "@vue/cli-plugin-eslint": "~4.2.0",
48 | "@vue/cli-plugin-typescript": "~4.3.0",
49 | "@vue/cli-plugin-unit-jest": "~4.3.0",
50 | "@vue/cli-service": "~4.2.0",
51 | "@vue/eslint-config-prettier": "^6.0.0",
52 | "@vue/eslint-config-typescript": "^5.0.1",
53 | "@vue/test-utils": "1.0.0-beta.31",
54 | "ajv": "^6.12.0",
55 | "ajv-errors": "^1.0.1",
56 | "ajv-i18n": "^3.5.0",
57 | "babel-eslint": "^10.0.3",
58 | "core-js": "^3.6.4",
59 | "element-ui": "^2.13.0",
60 | "eslint": "^6.7.2",
61 | "eslint-plugin-prettier": "^3.1.1",
62 | "eslint-plugin-vue": "^6.1.2",
63 | "prettier": "^1.19.1",
64 | "rimraf": "^3.0.2",
65 | "stylus": "^0.54.7",
66 | "stylus-loader": "^3.0.2",
67 | "typescript": "~3.8.3",
68 | "vue": "^2.6.11",
69 | "vue-cli-plugin-webpack-bundle-analyzer": "~2.0.0",
70 | "vue-json-pretty": "^1.6.3",
71 | "vue-template-compiler": "^2.6.11"
72 | },
73 | "peerDependencies": {
74 | "ajv": "^6.12.0",
75 | "ajv-i18n": "^3.5.0",
76 | "element-ui": "^2.13.0",
77 | "jsonpointer": "^4.0.1",
78 | "lodash.union": "^4.6.0",
79 | "vue": "^2.6.11"
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/tests/unit/default-value/string.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 |
3 | import { initialize, Helper as Wrapper } from '../utils'
4 |
5 | initialize()
6 |
7 | // import Wrapper from './Helper.vue'
8 | // import VueSchemaForm from '../..'
9 |
10 | // Vue.config.productionTip = false
11 | // Vue.use(VueSchemaForm)
12 |
13 | describe('string renderer default values', () => {
14 | it('should use `default` as default value when value is undefined', async () => {
15 | const wrapper: any = mount(Wrapper, {
16 | data: () => ({
17 | schema: {
18 | type: 'string',
19 | default: 'text',
20 | },
21 | value: undefined,
22 | }),
23 | })
24 | await wrapper.vm.$nextTick()
25 | expect(wrapper.vm.value).toMatch('text')
26 | })
27 |
28 | it('should use `default` as default value when value is empty string', async () => {
29 | const wrapper: any = mount(Wrapper, {
30 | data: () => ({
31 | schema: {
32 | type: 'string',
33 | default: 'text',
34 | },
35 | value: '',
36 | }),
37 | })
38 | await wrapper.vm.$nextTick()
39 |
40 | // empty string is string, so we treat it as value
41 | expect(wrapper.vm.value).toMatch('')
42 | })
43 |
44 | it('should use `default` as default value when in object', async () => {
45 | const wrapper: any = mount(Wrapper, {
46 | data: () => ({
47 | schema: {
48 | type: 'object',
49 | properties: {
50 | name: {
51 | type: 'string',
52 | default: 'text',
53 | },
54 | },
55 | },
56 | value: {},
57 | }),
58 | })
59 | await wrapper.vm.$nextTick()
60 | expect(wrapper.vm.value.name).toMatch('text')
61 | })
62 |
63 | it('should not create default value when `default` not provided', async () => {
64 | const wrapper: any = mount(Wrapper, {
65 | data: () => ({
66 | schema: {
67 | type: 'object',
68 | properties: {
69 | name: {
70 | type: 'string',
71 | },
72 | },
73 | },
74 | value: {},
75 | }),
76 | })
77 | await wrapper.vm.$nextTick()
78 | expect(wrapper.vm.value.name).toBeUndefined()
79 | })
80 |
81 | it('should use object default value as default', async () => {
82 | const wrapper: any = mount(Wrapper, {
83 | data: () => ({
84 | schema: {
85 | type: 'object',
86 | properties: {
87 | name: {
88 | type: 'string',
89 | },
90 | },
91 | default: {
92 | name: 'text',
93 | },
94 | },
95 | value: null,
96 | }),
97 | })
98 | await wrapper.vm.$nextTick()
99 | expect(wrapper.vm.value.name).toMatch('text')
100 | })
101 | })
102 |
--------------------------------------------------------------------------------
/tests/unit/vjsf/ui-schema.spec.ts:
--------------------------------------------------------------------------------
1 | import { shallowMount, mount } from '@vue/test-utils'
2 |
3 | import SchemaItem from '../../../src/core/SchemaItem.vue'
4 |
5 | import { initialize, Helper as Wrapper, Custom } from '../utils'
6 |
7 | initialize()
8 |
9 | describe('ui schema', () => {
10 | it('should render as expect when use ui-schema', async () => {
11 | const wrapper: any = mount(Wrapper, {
12 | data: () => ({
13 | schema: {
14 | type: 'string',
15 | default: 1,
16 | },
17 | uiSchema: {
18 | component: 'my-custom',
19 | additionProps: {
20 | a: 'b',
21 | },
22 | title: 'test title',
23 | },
24 | value: undefined,
25 | }),
26 | })
27 | await wrapper.vm.$nextTick()
28 | const test = wrapper.find({ name: 'MyCustom' })
29 | const attrs = test.attributes()
30 | expect(attrs.a).toBe('b')
31 | expect(attrs.title).toBe('test title')
32 | })
33 |
34 | it('should pass down ui-schema correctly', async () => {
35 | const wrapper: any = mount(Wrapper, {
36 | data: () => ({
37 | schema: {
38 | type: 'object',
39 | properties: {
40 | name: {
41 | type: 'string',
42 | },
43 | age: {
44 | type: 'number',
45 | },
46 | pets: {
47 | type: 'array',
48 | items: {
49 | type: 'string',
50 | },
51 | default: ['123'],
52 | },
53 | },
54 | },
55 | uiSchema: {
56 | title: 'object',
57 | properties: {
58 | name: {
59 | additionProps: {
60 | a: 'b',
61 | },
62 | title: 'name',
63 | },
64 | age: {
65 | title: 'age',
66 | },
67 | pets: {
68 | title: 'pets',
69 | items: {
70 | title: 'ptname',
71 | },
72 | },
73 | },
74 | },
75 | value: undefined,
76 | }),
77 | })
78 | await wrapper.vm.$nextTick()
79 | const obj = wrapper.find({ name: 'JsfObjectRenderer' })
80 | const str = wrapper.find({ name: 'JsfStringRenderer' })
81 | const num = wrapper.find({ name: 'JsfNumberRenderer' })
82 | const arr = wrapper.find({ name: 'JsfArrayRenderer' })
83 | // const attrs = test.attributes()
84 | expect(obj.props().uiSchema.title).toBe('object')
85 | // expect(obj.vm.vjsf.title).toBe('object')
86 | expect(str.props().uiSchema.title).toBe('name')
87 | expect(str.vm.vjsf.title).toBe('name')
88 | expect(num.props().uiSchema.title).toBe('age')
89 | expect(num.vm.vjsf.title).toBe('age')
90 | expect(arr.props().uiSchema.title).toBe('pets')
91 | expect(arr.vm.vjsf.title).toBe('pets')
92 |
93 | const arrSubStr = arr.find({ name: 'JsfStringRenderer' })
94 | expect(arrSubStr.props().uiSchema.title).toBe('ptname')
95 | // expect(attrs.title).toBe('test title')
96 | })
97 | })
98 |
--------------------------------------------------------------------------------
/tests/unit/vjsf/vjsf.spec.ts:
--------------------------------------------------------------------------------
1 | import { shallowMount, mount } from '@vue/test-utils'
2 |
3 | import SchemaItem from '../../../src/core/SchemaItem.vue'
4 |
5 | import { initialize, Helper as Wrapper, Custom } from '../utils'
6 |
7 | initialize()
8 |
9 | describe('custom component', () => {
10 | it('should render expect props to form components', async () => {
11 | const wrapper: any = mount(Wrapper, {
12 | data: () => ({
13 | schema: {
14 | type: 'string',
15 | default: 1,
16 | vjsf: {
17 | additionProps: {
18 | a: 'b',
19 | },
20 | },
21 | },
22 | value: undefined,
23 | }),
24 | })
25 | await wrapper.vm.$nextTick()
26 | const test = wrapper.find({ name: 'JsfTextInput' })
27 | const props = test.props()
28 | // expect(props.value).toBe(1)
29 | expect(props.value).toBeUndefined()
30 | expect(typeof props.onChange).toBe('function')
31 | expect(props.schema.default).toBe(1)
32 | expect(props.format).toBe('text')
33 | expect(props.title).toBe('')
34 | })
35 |
36 | it('schema item `.isCustomComponent` should be true when custom component provided', async () => {
37 | const wrapper: any = shallowMount(SchemaItem, {
38 | propsData: {
39 | schema: {
40 | type: 'string',
41 | vjsf: {
42 | component: 'input',
43 | },
44 | },
45 | path: '',
46 | rootSchema: {},
47 | onChange: () => {},
48 | value: undefined,
49 | },
50 | provide: {
51 | formContext: {
52 | errors: [],
53 | },
54 | transformSchema: (s: any) => s,
55 | },
56 | })
57 | await wrapper.vm.$nextTick()
58 | expect(wrapper.vm.isCustomComponent).toBe(true)
59 | })
60 |
61 | it('should pass additionProps to custom component', async () => {
62 | const wrapper: any = mount(Wrapper, {
63 | data: () => ({
64 | schema: {
65 | type: 'string',
66 | default: 1,
67 | vjsf: {
68 | component: 'my-custom',
69 | additionProps: {
70 | a: 'b',
71 | },
72 | },
73 | },
74 | value: undefined,
75 | }),
76 | })
77 | await wrapper.vm.$nextTick()
78 | const test = wrapper.find({ name: 'MyCustom' })
79 | const attrs = test.attributes()
80 | expect(attrs.a).toBe('b')
81 | })
82 |
83 | it('should pass additionProps to normal component', async () => {
84 | const wrapper: any = mount(Wrapper, {
85 | data: () => ({
86 | schema: {
87 | type: 'string',
88 | default: 1,
89 | vjsf: {
90 | additionProps: {
91 | a: 'b',
92 | },
93 | },
94 | },
95 | value: undefined,
96 | }),
97 | })
98 | await wrapper.vm.$nextTick()
99 | const test = wrapper.find({ name: 'JsfTextInput' })
100 | const attrs = test.attributes()
101 | expect(attrs.a).toBe('b')
102 | })
103 | })
104 |
--------------------------------------------------------------------------------
/src/core/validator/instance-default-options.ts:
--------------------------------------------------------------------------------
1 | import { CreateInstanceOptions, EnumKeyValueItem } from './types'
2 | import {
3 | DateRangeKeyword,
4 | DateTimeRangeKeyword,
5 | } from './date-time-range-keyword'
6 |
7 | const COLOR_REG = /^(rgb\s*?\(\s*?(000|0?\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\s*?,\s*?(000|0?\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\s*?,\s*?(000|0?\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\s*?\))$|^(rgba\s*?\(\s*?(000|0?\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\s*?,\s*?(000|0?\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\s*?,\s*?(000|0?\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\s*?,\s*?(0|0\.\d*|1|1.0*)\s*?\))$|^(transparent)$|^(#([a-fA-F0-9]){3})$|^(#([a-fA-F0-9]){6}$)|(^hsl\s*?\(\s*?(000|0?\d{1,2}|[1-2]\d\d|3[0-5]\d|360)\s*?,\s*?(000|100|0?\d{2}|0?0?\d)%\s*?,\s*?(000|100|0?\d{2}|0?0?\d)%\s*?\)$)|(^hsla\s*?\(\s*?(000|0?\d{1,2}|[1-2]\d\d|3[0-5]\d|360)\s*?,\s*?(000|100|0?\d{2}|0?0?\d)%\s*?,\s*?(000|100|0?\d{2}|0?0?\d)%\s*?,\s*?(0|0\.\d*|1|1.0*)\s*?\)$)$/
8 |
9 | // const URL_REG = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/ // eslint-disable-line
10 |
11 | const defaultOptions: CreateInstanceOptions = {
12 | options: {
13 | allErrors: true,
14 | jsonPointers: true,
15 | },
16 | formats: [
17 | {
18 | name: 'color',
19 | definition: (v) => v === '' || COLOR_REG.test(v),
20 | },
21 | // {
22 | // name: 'image',
23 | // definition: (v) => v === '' || URL_REG.test(v),
24 | // },
25 | ],
26 | keywords: [
27 | {
28 | name: 'dateRange',
29 | definition: DateRangeKeyword,
30 | },
31 | {
32 | name: 'dateTimeRange',
33 | definition: DateTimeRangeKeyword,
34 | },
35 | {
36 | name: 'enumKeyValue',
37 | definition: {
38 | type: ['number', 'string'],
39 | errors: 'full',
40 | compile: (sch) => {
41 | const values = sch.map(
42 | (s: EnumKeyValueItem) => s.value
43 | )
44 | // const multi = parentSchema.multiple
45 |
46 | return function doValidate(data) {
47 | const flag = values.indexOf(data) > -1
48 |
49 | if (!flag) {
50 | const fun: any = doValidate
51 | const errors = fun.errors || []
52 | errors.push({
53 | keyword: 'enumKeyValue',
54 | message: '请选择一个正确的选项',
55 | params: {
56 | keyword: 'enumKeyValue',
57 | },
58 | })
59 | fun.errors = errors
60 | }
61 |
62 | return flag
63 | }
64 | },
65 | metaSchema: {
66 | type: 'array',
67 | items: {
68 | oneOf: [
69 | {
70 | type: 'object',
71 | properties: {
72 | key: { type: 'string' },
73 | value: { type: 'string' },
74 | },
75 | },
76 | {
77 | type: 'object',
78 | properties: {
79 | key: { type: 'string' },
80 | value: { type: 'number' },
81 | },
82 | },
83 | ],
84 | },
85 | },
86 | },
87 | },
88 | ],
89 | }
90 |
91 | export default defaultOptions
92 |
--------------------------------------------------------------------------------
/src/theme-element-ui/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import { Button } from 'element-ui'
4 |
5 | import Form from './Form.vue'
6 | import FormItem from './FormItem.vue'
7 | import Selection from './Selection.vue'
8 | import TextInput from './TextInput.vue'
9 | import ColorPicker from './ColorPicker.vue'
10 | import NumberInput from './NumberInput.vue'
11 | import DatePicker from './DatePicker.vue'
12 | import TimePicker from './TimePicker.vue'
13 | import DateTimePicker from './DateTimePicker.vue'
14 | import DateTimeRangePicker from './DateTimeRangePicker.vue'
15 | import Switch from './Switch.vue'
16 | import Alert from './Alert.vue'
17 | // import ArrayItemActions from './ArrayItemActions.vue'
18 | // import ArrayItemAddAction from './ArrayItemAddAction.vue'
19 | import SingleTypeArrayWrapper from './SingleTypeArrayWrapper.vue'
20 | import Constant from './Constant.vue'
21 |
22 | import { CommonBase } from './form-component-props'
23 |
24 | function getComponentName(component: any) {
25 | const options = component.options
26 | const name = options.name || component.name
27 | if (!options.name) {
28 | throw new Error(`${name} has no name in option`)
29 | }
30 | return name
31 | }
32 |
33 | export default {
34 | install(vue: typeof Vue) {
35 | const gn = getComponentName
36 | vue.component(gn(Form), Form)
37 | // vue.component(gn(FormItem), FormItem)
38 | vue.component(gn(Selection), Selection)
39 | vue.component(gn(TextInput), TextInput)
40 | vue.component(gn(ColorPicker), ColorPicker)
41 | vue.component(gn(NumberInput), NumberInput)
42 | vue.component(gn(Alert), Alert)
43 | vue.component(gn(DatePicker), DatePicker)
44 | vue.component(gn(TimePicker), TimePicker)
45 | vue.component(gn(DateTimePicker), DateTimePicker)
46 | vue.component(gn(DateTimeRangePicker), DateTimeRangePicker)
47 | vue.component(gn(Switch), Switch)
48 | // vue.component(gn(ArrayItemActions), ArrayItemActions)
49 | // vue.component(gn(ArrayItemAddAction), ArrayItemAddAction)
50 | vue.component(gn(SingleTypeArrayWrapper), SingleTypeArrayWrapper)
51 | vue.component(gn(Constant), Constant)
52 | vue.component('JsfButton', Button)
53 | },
54 | }
55 |
56 | export const components = {
57 | Form,
58 | Selection,
59 | TextInput,
60 | ColorPicker,
61 | NumberInput,
62 | Alert,
63 | DatePicker,
64 | TimePicker,
65 | DateTimePicker,
66 | Switch,
67 | SingleTypeArrayWrapper,
68 | Constant,
69 | FormItem,
70 | }
71 |
72 | export {
73 | Form,
74 | Selection,
75 | TextInput,
76 | ColorPicker,
77 | NumberInput,
78 | Alert,
79 | DatePicker,
80 | TimePicker,
81 | DateTimePicker,
82 | Switch,
83 | SingleTypeArrayWrapper,
84 | Constant,
85 | FormItem,
86 | CommonBase,
87 | }
88 |
89 | export const FormItemPropsMixin = {
90 | computed: {
91 | formItemProps(): any {
92 | return {
93 | required: (this as any).required,
94 | schema: (this as any).schema,
95 | firstMatchedError: (this as any).firstMatchedError,
96 | requiredError: (this as any).requiredError,
97 | errors: (this as any).errors,
98 | label: (this as any).title,
99 | description: (this as any).description,
100 | }
101 | },
102 | },
103 | }
104 |
--------------------------------------------------------------------------------
/tests/unit/theme-element/datetime.spec.ts:
--------------------------------------------------------------------------------
1 | import { shallowMount, mount } from '@vue/test-utils'
2 |
3 | import { initialize, Helper as Wrapper, Custom } from '../utils'
4 |
5 | initialize()
6 |
7 | describe('datetime', () => {
8 | it('format `datetime` should render datetimepicker', async () => {
9 | const wrapper: any = mount(Wrapper, {
10 | data: () => ({
11 | schema: {
12 | type: 'string',
13 | format: 'date-time',
14 | },
15 | }),
16 | })
17 | const picker = wrapper.find({ name: 'JsfDateTimePicker' })
18 | await wrapper.vm.$nextTick()
19 | expect(picker.vm).not.toBeUndefined()
20 | })
21 |
22 | it('format of date-time is string, el value should be the same as default', async () => {
23 | const datetime = '2020-07-17 10:20:30'
24 | const wrapper: any = mount(Wrapper, {
25 | data: () => ({
26 | schema: {
27 | type: 'string',
28 | format: 'date-time',
29 | default: datetime,
30 | },
31 | value: undefined,
32 | }),
33 | })
34 | const elPicker = wrapper.find({ name: 'ElDatePicker' })
35 | await wrapper.vm.$nextTick()
36 | expect(elPicker.vm.value).toBe(datetime)
37 | })
38 |
39 | it('format of date-time is string, el value should be the same as value', async () => {
40 | const datetime = '2020-07-17 10:20:30'
41 | const wrapper: any = mount(Wrapper, {
42 | data: () => ({
43 | schema: {
44 | type: 'string',
45 | format: 'date-time',
46 | },
47 | value: datetime,
48 | }),
49 | })
50 | const elPicker = wrapper.find({ name: 'ElDatePicker' })
51 | expect(elPicker.vm.value).toBe(datetime)
52 | })
53 |
54 | it('format of date-time is string, value should update when el-date-picker emit input event', async () => {
55 | const datetime = '2020-07-17 10:20:30'
56 | const wrapper: any = mount(Wrapper, {
57 | data: () => ({
58 | schema: {
59 | type: 'string',
60 | format: 'date-time',
61 | },
62 | value: undefined,
63 | }),
64 | })
65 | const elPicker = wrapper.find({ name: 'ElDatePicker' })
66 | elPicker.vm.$emit('input', datetime)
67 | expect(wrapper.vm.value).toBe(new Date(datetime).toJSON()) // github服务器的识趣跟我们不一样
68 | })
69 |
70 | it('format of date-time is number, el value should be the same as value', async () => {
71 | const time = new Date('2020-07-15 10:10:10').getTime()
72 | const wrapper: any = mount(Wrapper, {
73 | data: () => ({
74 | schema: {
75 | type: 'number',
76 | format: 'date-time',
77 | },
78 | value: time,
79 | }),
80 | })
81 | const picker = wrapper.find({ name: 'ElDatePicker' })
82 | expect(picker.vm.value).toBe(time)
83 | })
84 | it('format of date-time is number, value should update when el emit input event', async () => {
85 | const time = new Date('2020-07-15 10:20:30').getTime()
86 | const wrapper: any = mount(Wrapper, {
87 | data: () => ({
88 | schema: {
89 | type: 'number',
90 | format: 'date-time',
91 | },
92 | value: undefined,
93 | }),
94 | })
95 | const elPicker = wrapper.find({ name: 'ElDatePicker' })
96 | elPicker.vm.$emit('input', time)
97 | expect(wrapper.vm.value).toBe(time)
98 | })
99 | })
100 |
--------------------------------------------------------------------------------
/tests/unit/default-value/object.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 |
3 | import { initialize, Helper as Wrapper } from '../utils'
4 |
5 | initialize()
6 |
7 | describe('object renderer default values', () => {
8 | it('should update value to `{}` if other type provided', async () => {
9 | const wrapper: any = mount(Wrapper, {
10 | data: () => ({
11 | schema: {
12 | type: 'object',
13 | properties: {
14 | aaa: {
15 | type: 'string',
16 | default: 'text',
17 | },
18 | },
19 | },
20 | value: '',
21 | }),
22 | })
23 | await wrapper.vm.$nextTick()
24 | expect(wrapper.vm.value.aaa).toMatch('text')
25 | })
26 |
27 | it('should update value to `{}` if other type provided with value given', async () => {
28 | const wrapper: any = mount(Wrapper, {
29 | data: () => ({
30 | schema: {
31 | type: 'object',
32 | properties: {
33 | name: {
34 | type: 'string',
35 | default: 'text',
36 | },
37 | },
38 | },
39 | value: 'text',
40 | }),
41 | })
42 | await wrapper.vm.$nextTick()
43 | expect(wrapper.vm.value.name).toMatch('text')
44 | })
45 |
46 | it('should use default as default value if `default` provided', async () => {
47 | const wrapper: any = mount(Wrapper, {
48 | data: () => ({
49 | schema: {
50 | type: 'object',
51 | properties: {
52 | name: {
53 | type: 'string',
54 | default: 'text',
55 | },
56 | },
57 | },
58 | value: 'text',
59 | }),
60 | })
61 | await wrapper.vm.$nextTick()
62 | expect(wrapper.vm.value.name).toMatch('text')
63 | })
64 |
65 | it('should use value when create with value provided', async () => {
66 | const wrapper: any = mount(Wrapper, {
67 | data: () => ({
68 | schema: {
69 | type: 'object',
70 | properties: {
71 | name: {
72 | type: 'string',
73 | default: 'text',
74 | },
75 | },
76 | },
77 | value: {
78 | name: 'jokcy',
79 | },
80 | }),
81 | })
82 | await wrapper.vm.$nextTick()
83 | expect(wrapper.vm.value.name).toMatch('jokcy')
84 | })
85 |
86 | it('should match expected result', async () => {
87 | const wrapper: any = mount(Wrapper, {
88 | data: () => ({
89 | schema: {
90 | type: 'object',
91 | properties: {
92 | name: {
93 | type: 'string',
94 | default: 'text',
95 | },
96 | age: {
97 | type: 'number',
98 | default: 10,
99 | },
100 | pets: {
101 | type: 'array',
102 | items: {
103 | type: 'string',
104 | },
105 | },
106 | },
107 | default: {
108 | pets: ['kiki'],
109 | },
110 | },
111 | value: '',
112 | }),
113 | })
114 | await wrapper.vm.$nextTick()
115 | expect(wrapper.vm.value.name).toMatch('text')
116 | expect(wrapper.vm.value.age).toEqual(10)
117 | expect(wrapper.vm.value.pets).toEqual(['kiki'])
118 | })
119 | })
120 |
--------------------------------------------------------------------------------
/src/core/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import { Prop, Component } from 'vue-property-decorator'
3 |
4 | import * as utils from './utils'
5 |
6 | import SchemaForm from './SchemaForm.vue'
7 | import SchemaItem from './SchemaItem.vue'
8 | // import SchemaRenderer from './SchemaRenderer.vue'
9 | // import createValidator from './validator'
10 |
11 | export default {
12 | install(vue: typeof Vue) {
13 | vue.component('JsonSchemaForm', SchemaForm)
14 | vue.component('JsfItem', SchemaItem)
15 | // vue.component(SchemaRenderer.name, SchemaRenderer)
16 |
17 | Promise.resolve().then(() => {
18 | if (!vue.component('JsfForm')) {
19 | throw new Error('要使用JsonSchemaForm请至少使用一个主题')
20 | }
21 | })
22 | },
23 | }
24 |
25 | @Component
26 | class ThemeBaseClass extends Vue {
27 | @Prop() value: any
28 | @Prop({ type: Function }) onChange: any
29 | @Prop({ type: String }) format: any
30 | @Prop({ type: Object }) schema: any
31 | @Prop() errors: any
32 | @Prop({ type: String }) title: any
33 | @Prop({ type: Boolean }) required: any
34 | @Prop({ type: Boolean }) requiredError: any
35 | @Prop({ type: String }) description: any
36 | @Prop({ type: Object }) vjsf: any
37 | @Prop({ type: Boolean }) isDependenciesKey: any
38 |
39 | defaultPlaceholder: string = '请输入'
40 |
41 | get placeholder() {
42 | return this.vjsf.placeholder || this.defaultPlaceholder
43 | }
44 |
45 | get additionProps() {
46 | return this.vjsf.additionProps || {}
47 | }
48 |
49 | get firstMatchedError() {
50 | return this.errors.find((e: any) => {
51 | const schemaPath = e.schemaPath
52 | if (this.isDependenciesKey && isDependenciesError(schemaPath)) {
53 | return false
54 | }
55 | return true
56 | })
57 | }
58 | get formItemProps() {
59 | return {
60 | required: this.required,
61 | schema: this.schema,
62 | firstMatchedError: (this as any).firstMatchedError,
63 | requiredError: this.requiredError,
64 | errors: this.errors,
65 | label: this.title,
66 | description: this.description,
67 | }
68 | }
69 | }
70 |
71 | function isDependenciesError(schemaPath: string) {
72 | return schemaPath.indexOf('/dependencies/') > 0
73 | }
74 |
75 | const ThemeBaseMixin = Vue.extend({
76 | props: {
77 | value: {},
78 | onChange: Function,
79 | format: String,
80 | schema: Object,
81 | errors: Array,
82 | title: String,
83 | required: Boolean,
84 | requiredError: Boolean,
85 | description: String,
86 | vjsf: Object,
87 | isDependenciesKey: Boolean,
88 | },
89 |
90 | data() {
91 | return {
92 | defaultPlaceholder: '请输入',
93 | }
94 | },
95 |
96 | computed: {
97 | placeholder() {
98 | return (this.vjsf as any).placeholder || (this as any).defaultPlaceholder
99 | },
100 | additionProps() {
101 | return (this.vjsf as any).additionProps || {}
102 | },
103 | firstMatchedError() {
104 | return this.errors.find((e: any) => {
105 | const schemaPath = e.schemaPath
106 | if (this.isDependenciesKey && isDependenciesError(schemaPath)) {
107 | return false
108 | }
109 | return true
110 | })
111 | },
112 | formItemProps() {
113 | return {
114 | required: this.required,
115 | schema: this.schema,
116 | firstMatchedError: (this as any).firstMatchedError,
117 | requiredError: this.requiredError,
118 | errors: this.errors,
119 | label: this.title,
120 | description: this.description,
121 | }
122 | },
123 | },
124 | })
125 |
126 | export { utils, SchemaForm as JsonSchemaForm, ThemeBaseClass, ThemeBaseMixin }
127 |
--------------------------------------------------------------------------------
/tests/unit/theme-element/timePicker.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 |
3 | import { initialize, Helper as Wrapper } from '../utils'
4 |
5 | initialize()
6 |
7 | describe('time component', () => {
8 | it('format `time` should render time component', async () => {
9 | const wrapper: any = mount(Wrapper, {
10 | data: () => ({
11 | schema: {
12 | type: 'number',
13 | format: 'time',
14 | },
15 | }),
16 | })
17 | const picker = wrapper.find({ name: 'JsfTimePicker' })
18 | expect(picker.vm).not.toBeUndefined()
19 | })
20 |
21 | it('format of time is number, el value should be the same as default', async () => {
22 | const time = new Date('2020-07-29 15:23:07').getTime()
23 | const wrapper: any = mount(Wrapper, {
24 | data: () => ({
25 | schema: {
26 | type: 'number',
27 | format: 'time',
28 | default: time,
29 | },
30 | value: undefined,
31 | }),
32 | })
33 | const elPicker = wrapper.find({ name: 'JsfTimePicker' })
34 | await wrapper.vm.$nextTick()
35 | expect(elPicker.vm.value).toBe(time)
36 | })
37 | it('format of time is number, el value should be the same as value', async () => {
38 | const time = new Date('2020-07-29 15:23:07').getTime()
39 | const wrapper: any = mount(Wrapper, {
40 | data: () => ({
41 | schema: {
42 | type: 'number',
43 | format: 'time',
44 | },
45 | value: time,
46 | }),
47 | })
48 | const elPicker = wrapper.find({ name: 'JsfTimePicker' })
49 | expect(elPicker.vm.value).toBe(time)
50 | })
51 | it('format of time is number, value should update when el-date-picker emit input event', async () => {
52 | const time = new Date('2020-07-29 15:23:07').getTime()
53 | const wrapper: any = mount(Wrapper, {
54 | data: () => ({
55 | schema: {
56 | type: 'number',
57 | format: 'time',
58 | },
59 | value: undefined,
60 | }),
61 | })
62 | const elPicker = wrapper.find({ name: 'JsfTimePicker' })
63 | elPicker.vm.$emit('input', time)
64 | expect(wrapper.vm.value).toBe(time)
65 | })
66 |
67 | it('format of time is string, el value should be the same as default', async () => {
68 | const time = '15:23:07'
69 | const wrapper: any = mount(Wrapper, {
70 | data: () => ({
71 | schema: {
72 | type: 'string',
73 | format: 'time',
74 | default: time,
75 | },
76 | value: undefined,
77 | }),
78 | })
79 | const elPicker = wrapper.find({ name: 'JsfTimePicker' })
80 | await wrapper.vm.$nextTick()
81 | expect(elPicker.vm.value).toBe(time)
82 | })
83 | it('format of time is string, el value should be the same as value', async () => {
84 | const time = '15:23:07'
85 | const wrapper: any = mount(Wrapper, {
86 | data: () => ({
87 | schema: {
88 | type: 'string',
89 | format: 'time',
90 | },
91 | value: time,
92 | }),
93 | })
94 | const elPicker = wrapper.find({ name: 'JsfTimePicker' })
95 | expect(elPicker.vm.value).toBe(time)
96 | })
97 | it('format of time is string, value should update when el-date-picker emit input event', async () => {
98 | const time = '15:23:07'
99 | const wrapper: any = mount(Wrapper, {
100 | data: () => ({
101 | schema: {
102 | type: 'string',
103 | format: 'time',
104 | },
105 | value: undefined,
106 | }),
107 | })
108 | const elPicker = wrapper.find({ name: 'JsfTimePicker' })
109 | elPicker.vm.$emit('input', time)
110 | expect(wrapper.vm.value).toBe(time)
111 | })
112 | })
113 |
--------------------------------------------------------------------------------
/src/theme-element-ui/SingleTypeArrayWrapper.vue:
--------------------------------------------------------------------------------
1 |
2 |
34 |
35 |
36 |
64 |
65 |
138 |
--------------------------------------------------------------------------------
/src/plugins/ImageUploader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
20 | 点击上传
21 |
22 |
23 |
29 |
30 |
31 |
32 |
127 |
128 |
133 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | chose(index)"
9 | >{{ name }}
11 |
12 |
13 |
14 |
24 |
25 |
26 | 校 验/validate
27 | 清除错误/clear errors
28 | 重置数据/reset
29 |
30 |
31 |
41 |
42 |
43 |
44 |
113 |
114 |
126 |
127 |
170 |
--------------------------------------------------------------------------------
/src/core/mixins/base.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import { Component, Inject, Prop } from 'vue-property-decorator'
3 | import { getVJSFConfig, escapeJsonPointer } from '../utils'
4 |
5 | // @Component()
6 | // const CommonBase = Vue.extend({
7 | // props: {
8 | // schema: {
9 | // type: Object,
10 | // required: true,
11 | // },
12 | // value: {
13 | // required: false,
14 | // },
15 | // rootSchema: {
16 | // type: Object,
17 | // required: true,
18 | // },
19 | // path: {
20 | // type: String,
21 | // required: true,
22 | // },
23 | // onChange: {
24 | // type: Function,
25 | // required: true,
26 | // },
27 | // required: Boolean,
28 | // requiredError: Boolean,
29 | // },
30 |
31 | // inject: {
32 | // form: 'formContext',
33 | // },
34 | // })
35 |
36 | // export default CommonBase
37 |
38 | @Component
39 | class CommonBaseClass extends Vue {
40 | @Prop({ type: Object, required: true }) schema: any
41 | @Prop({ required: false }) value: any
42 | @Prop({ type: Object, required: true }) rootSchema: any
43 | @Prop({ type: String, required: true }) path: any
44 | @Prop({ type: Function, required: true }) onChange: any
45 | @Prop({ type: Boolean }) required: any
46 | @Prop({ type: Boolean }) requiredError: any
47 | @Prop({ type: Boolean }) isDependenciesKey: any
48 | @Prop({ type: Object }) uiSchema: any
49 |
50 | @Inject('formContext') form: any
51 |
52 | handleChange(v: any) {}
53 |
54 | getPath(name: string) {
55 | return this.path + '/' + escapeJsonPointer(name)
56 | }
57 | }
58 |
59 | function isDependenciesError(schemaPath: string) {
60 | return schemaPath.indexOf('/dependencies/') > 0
61 | }
62 |
63 | @Component
64 | class RendererBaseClass extends CommonBaseClass {
65 | get format() {
66 | return this.schema.format || 'text'
67 | }
68 |
69 | // get errorMessage() {
70 | // if (this.requiredError) return '必须填写'
71 | // // const errors = this.form.errors.filter(
72 | // // (e: any) => e.dataPath === (this as any).path
73 | // // )
74 | // // return error ? error.message : ''
75 | // // return this.errors[0] ? this.errors[0].message : ''
76 | // return this.firstMatchedError ? this.firstMatchedError.message : ''
77 | // }
78 |
79 | // get firstMatchedError() {
80 | // return this.errors.find((e: any) => {
81 | // const schemaPath = e.schemaPath
82 | // if (this.isDependenciesKey && isDependenciesError(schemaPath)) {
83 | // return false
84 | // }
85 | // return true
86 | // })
87 | // }
88 |
89 | get errors() {
90 | const errors = this.form.errors.filter(
91 | (e: any) => e.dataPath === (this as any).path
92 | )
93 | return errors
94 | }
95 |
96 | get rendererProps() {
97 | return {
98 | value: this.value,
99 | onChange: this.handleChange,
100 | format: this.format,
101 | schema: this.schema,
102 | errors: this.errors,
103 | title: this.title,
104 | required: this.required,
105 | requiredError: this.requiredError,
106 | description: this.description,
107 | disabled: !!this.vjsf.disabled,
108 | vjsf: this.vjsf,
109 | path: this.path,
110 | isDependenciesKey: this.isDependenciesKey,
111 | ...this.vjsf.additionProps,
112 | }
113 | }
114 |
115 | // get formItemProps() {
116 | // return {
117 | // required: this.required,
118 | // schema: this.schema,
119 | // firstMatchedError: this.firstMatchedError,
120 | // requiredError: this.requiredError,
121 | // errors: this.errors,
122 | // label: this.title,
123 | // description: this.description,
124 | // }
125 | // }
126 |
127 | get vjsf() {
128 | return getVJSFConfig(this.schema, this.uiSchema)
129 | }
130 |
131 | get title() {
132 | return this.schemaTitle || this.path
133 | }
134 |
135 | get schemaTitle() {
136 | if (this.schema.title) {
137 | console.warn(
138 | 'the usage of `title` in schema will be deprecated soon use `vjsf.title` instead'
139 | )
140 | return this.schema.title
141 | }
142 | return this.vjsf.title
143 | }
144 |
145 | get description() {
146 | return this.vjsf.description
147 | }
148 | }
149 |
150 | export { CommonBaseClass, RendererBaseClass }
151 |
--------------------------------------------------------------------------------
/tests/unit/theme-element/date.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 |
3 | import { initialize, Helper as Wrapper } from '../utils'
4 |
5 | initialize()
6 |
7 | describe('date', () => {
8 | // string
9 | it('format `date` should render datepicker', async () => {
10 | const wrapper: any = mount(Wrapper, {
11 | data: () => ({
12 | schema: {
13 | type: 'string',
14 | format: 'date',
15 | },
16 | }),
17 | })
18 | const picker = wrapper.find({ name: 'JsfDatePicker' })
19 | expect(picker.vm).not.toBeUndefined()
20 | })
21 |
22 | it('value should update when el-date-picker emit input event', async () => {
23 | const wrapper: any = mount(Wrapper, {
24 | data: () => ({
25 | schema: {
26 | type: 'string',
27 | format: 'date',
28 | },
29 | value: undefined,
30 | }),
31 | })
32 | const elPicker = wrapper.find({ name: 'ElDatePicker' })
33 | elPicker.vm.$emit('input', '123')
34 | expect(wrapper.vm.value).toBe('123')
35 | })
36 | //TODO: at packages/date-picker/src/picker.vue
37 | // TypeError: dateStr.match is not a function
38 | // 值类型传参有误
39 | // it('get number when type is string', async () => {
40 | // const wrapper: any = mount(Wrapper, {
41 | // data: () => ({
42 | // schema: {
43 | // type: 'string',
44 | // format: 'date',
45 | // },
46 | // value: new Date('2020-07-15 10:10:10').getTime(),
47 | // }),
48 | // })
49 | // const picker = wrapper.find({ name: 'JsfDatePicker' })
50 | // expect(picker.vm.value).toBeUndefined()
51 | // })
52 | it('value should be the same as value', async () => {
53 | const date = '2020-07-15'
54 | const wrapper: any = mount(Wrapper, {
55 | data: () => ({
56 | schema: {
57 | type: 'string',
58 | format: 'date',
59 | },
60 | value: date,
61 | }),
62 | })
63 | const picker = wrapper.find({ name: 'JsfDatePicker' })
64 | expect(picker.vm.value).toBe(date)
65 | })
66 | it('value should be the same as default', async () => {
67 | const date = '2020-07-15'
68 | const wrapper: any = mount(Wrapper, {
69 | data: () => ({
70 | schema: {
71 | type: 'string',
72 | format: 'date',
73 | default: date,
74 | },
75 | value: undefined,
76 | }),
77 | })
78 | const picker = wrapper.find({ name: 'JsfDatePicker' })
79 | await wrapper.vm.$nextTick()
80 | expect(picker.vm.value).toBe(date)
81 | })
82 | // TODO:type传参有误
83 | // it('set datetime when type is date', async () => {
84 | // const wrapper: any = mount(Wrapper, {
85 | // data: () => ({
86 | // schema: {
87 | // type: 'string',
88 | // format: 'date',
89 | // },
90 | // value: '2020-07-15 10:20:30',
91 | // }),
92 | // })
93 | // await wrapper.vm.$nextTick()
94 | // expect(wrapper.vm.value).toBe('2020-07-15')
95 | // })
96 |
97 | // number
98 | it('format of date is number, value should update when el-date-picker emit input event', async () => {
99 | const time = new Date('2020-07-15 10:10:10').getTime()
100 | const wrapper: any = mount(Wrapper, {
101 | data: () => ({
102 | schema: {
103 | type: 'number',
104 | format: 'date',
105 | },
106 | value: undefined,
107 | }),
108 | })
109 | const elPicker = wrapper.find({ name: 'ElDatePicker' })
110 | elPicker.vm.$emit('input', time)
111 | expect(wrapper.vm.value).toBe(time)
112 | })
113 | it('format of date is number, value should be the same as value', async () => {
114 | const time = new Date('2020-07-15 10:10:10').getTime()
115 | const wrapper: any = mount(Wrapper, {
116 | data: () => ({
117 | schema: {
118 | type: 'number',
119 | format: 'date',
120 | },
121 | value: time,
122 | }),
123 | })
124 | const picker = wrapper.find({ name: 'ElDatePicker' })
125 | expect(picker.vm.value).toBe(time)
126 | })
127 | // TODO:值类型传参有误
128 | // it('get string when type is number', async () => {
129 | // const wrapper: any = mount(Wrapper, {
130 | // data: () => ({
131 | // schema: {
132 | // type: 'number',
133 | // format: 'date',
134 | // },
135 | // value: '2020-07-15',
136 | // }),
137 | // })
138 | // await wrapper.vm.$nextTick()
139 | // expect(wrapper.vm.value).toBe(new Date('2020-07-15').getTime())
140 | // })
141 | })
142 |
--------------------------------------------------------------------------------
/tests/unit/value-change/reset-value.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 |
3 | import { initialize, Helper as Wrapper } from '../utils'
4 |
5 | initialize()
6 |
7 | describe('when reset value to {}', () => {
8 | it('reset to expect value when value reset', async () => {
9 | const wrapper: any = mount(Wrapper, {
10 | data: () => ({
11 | schema: {
12 | type: 'object',
13 | properties: {
14 | n: {
15 | type: 'number',
16 | default: 10,
17 | },
18 | s: {
19 | type: 'string',
20 | default: '123',
21 | },
22 | b: {
23 | type: 'boolean',
24 | default: true,
25 | },
26 | a: {
27 | type: 'array',
28 | items: {
29 | type: 'string',
30 | },
31 | default: ['123'],
32 | },
33 | },
34 | },
35 | value: undefined,
36 | }),
37 | })
38 | await wrapper.vm.$nextTick()
39 | expect(wrapper.vm.value.n).toEqual(10)
40 | expect(wrapper.vm.value.s).toBe('123')
41 | expect(wrapper.vm.value.b).toBe(true)
42 | expect(wrapper.vm.value.a).toEqual(['123'])
43 | wrapper.vm.value = {}
44 | await wrapper.vm.$nextTick()
45 | expect(wrapper.vm.value.n).toBeUndefined()
46 | expect(wrapper.vm.value.s).toBeUndefined()
47 | expect(wrapper.vm.value.b).toBe(false)
48 | expect(wrapper.vm.value.a).toBeUndefined()
49 | })
50 |
51 | it('number should become undefined when value reset', async () => {
52 | const wrapper: any = mount(Wrapper, {
53 | data: () => ({
54 | schema: {
55 | type: 'object',
56 | properties: {
57 | n: {
58 | type: 'number',
59 | default: 10,
60 | },
61 | },
62 | },
63 | value: undefined,
64 | }),
65 | })
66 | await wrapper.vm.$nextTick()
67 | expect(wrapper.vm.value.n).toEqual(10)
68 | wrapper.vm.value = {}
69 | await wrapper.vm.$nextTick()
70 | expect(wrapper.vm.value.n).toBeUndefined()
71 | })
72 |
73 | it('string should become undefined when value reset', async () => {
74 | const wrapper: any = mount(Wrapper, {
75 | data: () => ({
76 | schema: {
77 | type: 'object',
78 | properties: {
79 | n: {
80 | type: 'string',
81 | default: 'text',
82 | },
83 | },
84 | },
85 | value: undefined,
86 | }),
87 | })
88 | await wrapper.vm.$nextTick()
89 | expect(wrapper.vm.value.n).toMatch('text')
90 | wrapper.vm.value = {}
91 | await wrapper.vm.$nextTick()
92 | expect(wrapper.vm.value.n).toBeUndefined()
93 | })
94 |
95 | it('reset nest object should work fine', async () => {
96 | const wrapper: any = mount(Wrapper, {
97 | data: () => ({
98 | schema: {
99 | type: 'object',
100 | properties: {
101 | obj: {
102 | type: 'object',
103 | default: {
104 | a: 'text',
105 | },
106 | properties: {
107 | a: {
108 | type: 'string',
109 | },
110 | },
111 | },
112 | },
113 | },
114 | value: undefined,
115 | }),
116 | })
117 | await wrapper.vm.$nextTick()
118 | expect(wrapper.vm.value.obj.a).toMatch('text')
119 | wrapper.vm.value = {}
120 | await wrapper.vm.$nextTick()
121 | const text = wrapper.find({ name: 'JsfTextInput' })
122 | expect(wrapper.vm.value.obj).toBeUndefined()
123 | expect(text.props().value).toBeUndefined()
124 | text.vm.onChange('111')
125 | await wrapper.vm.$nextTick()
126 | expect(text.props().value).toBe('111')
127 | expect(wrapper.vm.value.obj.a).toBe('111')
128 | })
129 |
130 | it('should not change object default value', async () => {
131 | const schema = {
132 | type: 'object',
133 | default: {
134 | text: '1',
135 | },
136 | properties: {
137 | text: {
138 | type: 'string',
139 | },
140 | },
141 | }
142 | const wrapper: any = mount(Wrapper, {
143 | data: () => ({
144 | schema,
145 | value: undefined,
146 | }),
147 | })
148 | await wrapper.vm.$nextTick()
149 | expect(wrapper.vm.value.text).toMatch('1')
150 | const text = wrapper.find({ name: 'JsfTextInput' })
151 | text.vm.onChange('2')
152 | await wrapper.vm.$nextTick()
153 | expect(wrapper.vm.value.text).toMatch('2')
154 | expect(schema.default.text).toBe('1')
155 | })
156 | })
157 |
--------------------------------------------------------------------------------
/src/theme-element-ui/form-component-props/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import { Component, Prop } from 'vue-property-decorator'
3 |
4 | function isDependenciesError(schemaPath: string) {
5 | return schemaPath.indexOf('/dependencies/') > 0
6 | }
7 |
8 | @Component
9 | export class CommonBase extends Vue {
10 | @Prop() value: any
11 | @Prop({ type: Function }) onChange: any
12 | @Prop({ type: String }) format: any
13 | @Prop({ type: Object }) schema: any
14 | @Prop() errors: any
15 | @Prop({ type: String }) title: any
16 | @Prop({ type: Boolean }) required: any
17 | @Prop({ type: Boolean }) requiredError: any
18 | @Prop({ type: String }) description: any
19 | @Prop({ type: Object }) vjsf: any
20 | @Prop({ type: Boolean }) isDependenciesKey: any
21 | @Prop({ type: String }) path: any
22 |
23 | defaultPlaceholder: string = '请输入'
24 |
25 | get placeholder() {
26 | return this.vjsf.placeholder || this.defaultPlaceholder
27 | }
28 |
29 | get additionProps() {
30 | return this.vjsf.additionProps || {}
31 | }
32 |
33 | get firstMatchedError() {
34 | return this.errors.find((e: any) => {
35 | // TODO: dataPath是否所有的key都是正确的
36 | // TODO: 增加所有错误信息种类的test case
37 | const dataPath = e.dataPath
38 | if (this.isDependenciesKey && isDependenciesError(dataPath)) {
39 | return false
40 | }
41 | return dataPath === this.path
42 | })
43 | }
44 |
45 | get formItemProps() {
46 | return {
47 | required: this.required,
48 | schema: this.schema,
49 | firstMatchedError: this.firstMatchedError,
50 | requiredError: this.requiredError,
51 | errors: this.errors,
52 | label: this.title,
53 | description: this.description,
54 | }
55 | }
56 | }
57 |
58 | @Component
59 | export class FormItemBase extends Vue {
60 | @Prop({ type: String }) format: any
61 | @Prop({ type: Object }) schema: any
62 | @Prop() firstMatchedError: any
63 | @Prop() errors: any
64 | @Prop({ type: String }) label: any
65 | @Prop({ type: Boolean }) required: any
66 | @Prop({ type: Boolean }) requiredError: any
67 | @Prop({ type: String }) description: any
68 | }
69 |
70 | @Component
71 | export class SelectionBase extends CommonBase {
72 | defaultPlaceholder = '请选择'
73 |
74 | @Prop({ type: Number }) multipleLimit: any
75 | @Prop({ type: Boolean }) multiple: any
76 | @Prop({ type: Array }) options: any
77 | }
78 | export const TextInputBase = CommonBase
79 | export const ColorPickBase = CommonBase
80 | export class SwitchBase extends CommonBase {
81 | @Prop({ type: String, default: '#13ce66' }) activeColor: any
82 | @Prop({ type: String, default: '#ff4949' }) inactiveColor: any
83 | }
84 | export const NumberInputBase = CommonBase
85 |
86 | @Component
87 | export class DatePickerBase extends CommonBase {
88 | @Prop({ type: String }) type: any
89 | }
90 | export const DateTimePickerBase = DatePickerBase
91 | export const TimePickerBase = CommonBase
92 | // export const NumberInputBase = CommonBase
93 |
94 | // export const SelectionBase = Vue.extend({
95 | // props: {
96 | // options: Array,
97 | // value: [String, Number, Array],
98 | // placeholder: {
99 | // type: String,
100 | // default: '请选择',
101 | // },
102 | // multipleLimit: {
103 | // type: Number,
104 | // default: 0,
105 | // },
106 | // multiple: {
107 | // type: Boolean,
108 | // default: false,
109 | // },
110 | // },
111 | // })
112 |
113 | // export const TextInputBase = Vue.extend({
114 | // props: {
115 | // value: {
116 | // required: false,
117 | // },
118 | // format: String,
119 | // placeholder: String,
120 | // },
121 | // })
122 |
123 | // export const ColorPickBase = Vue.extend({
124 | // props: {
125 | // value: {
126 | // required: false,
127 | // },
128 | // },
129 | // })
130 |
131 | // export const SwitchBase = Vue.extend({
132 | // props: {
133 | // value: {
134 | // type: Boolean,
135 | // },
136 | // activeColor: {
137 | // type: String,
138 | // default: '#13ce66',
139 | // },
140 | // inactiveColor: {
141 | // type: String,
142 | // default: '#ff4949',
143 | // },
144 | // },
145 | // })
146 |
147 | // export const NumberInputBase = Vue.extend({
148 | // props: {
149 | // value: {
150 | // required: false,
151 | // },
152 | // placeholder: String,
153 | // schema: Object,
154 | // // min: {
155 | // // type: Number,
156 | // // default: -Infinity,
157 | // // },
158 | // // max: {
159 | // // type: Number,
160 | // // default: Infinity,
161 | // // },
162 | // // precision: Number,
163 | // },
164 | // })
165 |
166 | // export const DatePickerBase = Vue.extend({
167 | // props: {
168 | // value: [String, Number, Array],
169 | // schema: {
170 | // type: Object,
171 | // required: true,
172 | // },
173 | // type: {
174 | // type: String,
175 | // default: '',
176 | // },
177 | // },
178 | // })
179 |
180 | // export const DateTimePickerBase = Vue.extend({
181 | // props: {
182 | // value: [String, Number],
183 | // schema: {
184 | // type: Object,
185 | // required: true,
186 | // },
187 | // },
188 | // })
189 |
--------------------------------------------------------------------------------
/tests/unit/default-value/array.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 |
3 | import { initialize, Helper as Wrapper } from '../utils'
4 |
5 | initialize()
6 |
7 | describe('array renderer default values', () => {
8 | it('should update value to `[]` if other type provided', async () => {
9 | const wrapper: any = mount(Wrapper, {
10 | data: () => ({
11 | schema: {
12 | type: 'array',
13 | items: {
14 | type: 'string',
15 | },
16 | },
17 | value: '',
18 | }),
19 | })
20 | await wrapper.vm.$nextTick()
21 | expect(wrapper.vm.value).toBeUndefined()
22 | })
23 |
24 | it('should update value to `[]` if other type provided with value given', async () => {
25 | const wrapper: any = mount(Wrapper, {
26 | data: () => ({
27 | schema: {
28 | type: 'array',
29 | items: {
30 | type: 'string',
31 | },
32 | },
33 | value: 'text',
34 | }),
35 | })
36 | await wrapper.vm.$nextTick()
37 | expect(wrapper.vm.value).toBeUndefined()
38 | })
39 |
40 | it('should use default as default value if `default` provided', async () => {
41 | const wrapper: any = mount(Wrapper, {
42 | data: () => ({
43 | schema: {
44 | type: 'array',
45 | items: {
46 | type: 'string',
47 | },
48 | },
49 | value: ['text'],
50 | }),
51 | })
52 | await wrapper.vm.$nextTick()
53 | expect(wrapper.vm.value.length).toBe(1)
54 | expect(wrapper.vm.value[0]).toBe('text')
55 | })
56 |
57 | it('in object should use default as default value if `default` provided', async () => {
58 | const wrapper: any = mount(Wrapper, {
59 | data: () => ({
60 | schema: {
61 | type: 'object',
62 | properties: {
63 | array: {
64 | type: 'array',
65 | items: {
66 | type: 'string',
67 | },
68 | default: ['text'],
69 | },
70 | },
71 | },
72 | value: '',
73 | }),
74 | })
75 | await wrapper.vm.$nextTick()
76 | expect(wrapper.vm.value.array.length).toBe(1)
77 | expect(wrapper.vm.value.array[0]).toBe('text')
78 | })
79 |
80 | it('in object should use default as default value if object has default', async () => {
81 | const wrapper: any = mount(Wrapper, {
82 | data: () => ({
83 | schema: {
84 | type: 'object',
85 | properties: {
86 | array: {
87 | type: 'array',
88 | items: {
89 | type: 'string',
90 | },
91 | },
92 | },
93 | default: {
94 | array: ['text'],
95 | },
96 | },
97 | value: '',
98 | }),
99 | })
100 | await wrapper.vm.$nextTick()
101 | expect(wrapper.vm.value.array.length).toBe(1)
102 | expect(wrapper.vm.value.array[0]).toBe('text')
103 | })
104 |
105 | /****************** fixed array *********************/
106 | it('should use default as default value if has default', async () => {
107 | const wrapper: any = mount(Wrapper, {
108 | data: () => ({
109 | schema: {
110 | type: 'array',
111 | items: [
112 | {
113 | type: 'string',
114 | },
115 | {
116 | type: 'number',
117 | },
118 | ],
119 | default: ['text'],
120 | },
121 | value: '',
122 | }),
123 | })
124 | await wrapper.vm.$nextTick()
125 | expect(wrapper.vm.value.length).toBe(1)
126 | expect(wrapper.vm.value[0]).toBe('text')
127 | expect(wrapper.vm.value[1]).toBeUndefined()
128 | })
129 |
130 | it('with fixed array should use `[]` as default value if no default', async () => {
131 | const wrapper: any = mount(Wrapper, {
132 | data: () => ({
133 | schema: {
134 | type: 'array',
135 | items: [
136 | {
137 | type: 'string',
138 | },
139 | {
140 | type: 'number',
141 | },
142 | ],
143 | },
144 | value: '',
145 | }),
146 | })
147 | await wrapper.vm.$nextTick()
148 | expect(wrapper.vm.value).toEqual([])
149 | // expect(wrapper.vm.value[0]).toBeUndefined()
150 | // expect(wrapper.vm.value[1]).toBeUndefined()
151 | })
152 |
153 | it('should use value as `currentValue` when created and follow value change', async () => {
154 | const wrapper: any = mount(Wrapper, {
155 | data: () => ({
156 | schema: {
157 | type: 'array',
158 | items: {
159 | type: 'object',
160 | properties: {
161 | name: {
162 | type: 'string',
163 | },
164 | },
165 | },
166 | },
167 | value: [
168 | {
169 | name: '123',
170 | },
171 | ],
172 | }),
173 | })
174 | await wrapper.vm.$nextTick()
175 | const ar = wrapper.find({ name: 'JsfArrayRenderer' })
176 | expect(ar.vm.currentValue).toBe(ar.vm.value)
177 | expect(ar.vm.currentValue).toEqual([{ name: '123' }])
178 | wrapper.vm.value = [{ name: '456' }]
179 | await wrapper.vm.$nextTick()
180 | expect(ar.vm.currentValue).toBe(ar.vm.value)
181 | expect(ar.vm.currentValue).toEqual([{ name: '456' }])
182 | // expect(wrapper.vm.value[0]).toBeUndefined()
183 | // expect(wrapper.vm.value[1]).toBeUndefined()
184 | })
185 | })
186 |
--------------------------------------------------------------------------------
/tests/unit/theme-element/selection.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 |
3 | import { initialize, Helper as Wrapper } from '../utils'
4 |
5 | initialize()
6 |
7 | describe('selection', () => {
8 | it('format `number` with `enumKeyValue` should render selection component', async () => {
9 | const wrapper: any = mount(Wrapper, {
10 | data: () => ({
11 | schema: {
12 | type: 'number',
13 | enumKeyValue: [
14 | {
15 | key: 'option1',
16 | value: 1,
17 | },
18 | {
19 | key: 'option2',
20 | value: 2,
21 | },
22 | {
23 | key: 'option3',
24 | value: 3,
25 | },
26 | ],
27 | },
28 | }),
29 | })
30 | const picker = wrapper.find({ name: 'JsfSelection' })
31 | expect(picker.vm).not.toBeUndefined()
32 | })
33 | it('el value should be the same as value', () => {
34 | const wrapper: any = mount(Wrapper, {
35 | data: () => ({
36 | schema: {
37 | type: 'number',
38 | enumKeyValue: [
39 | {
40 | key: 'option1',
41 | value: 1,
42 | },
43 | {
44 | key: 'option2',
45 | value: 2,
46 | },
47 | {
48 | key: 'option3',
49 | value: 3,
50 | },
51 | ],
52 | },
53 | value: 1,
54 | }),
55 | })
56 | const picker = wrapper.find({ name: 'JsfSelection' })
57 | expect(picker.vm.value).toBe(1)
58 | })
59 | it('el value should be the same as default', async () => {
60 | const wrapper: any = mount(Wrapper, {
61 | data: () => ({
62 | schema: {
63 | type: 'number',
64 | enumKeyValue: [
65 | {
66 | key: 'option1',
67 | value: 1,
68 | },
69 | {
70 | key: 'option2',
71 | value: 2,
72 | },
73 | {
74 | key: 'option3',
75 | value: 3,
76 | },
77 | ],
78 | default: 1,
79 | },
80 | value: undefined,
81 | }),
82 | })
83 | const picker = wrapper.find({ name: 'JsfSelection' })
84 | await wrapper.vm.$nextTick()
85 | expect(picker.vm.value).toBe(1)
86 | })
87 | it('value should change when el emit input event', () => {
88 | const wrapper: any = mount(Wrapper, {
89 | data: () => ({
90 | schema: {
91 | type: 'number',
92 | enumKeyValue: [
93 | {
94 | key: 'option1',
95 | value: 1,
96 | },
97 | {
98 | key: 'option2',
99 | value: 2,
100 | },
101 | {
102 | key: 'option3',
103 | value: 3,
104 | },
105 | ],
106 | },
107 | value: undefined,
108 | }),
109 | })
110 | const picker = wrapper.find({ name: 'JsfSelection' })
111 | picker.vm.$emit('input', 1)
112 | expect(wrapper.vm.value).toBe(1)
113 | })
114 | it('when type of selection is multiple, el value should be the same as value', () => {
115 | const wrapper: any = mount(Wrapper, {
116 | data: () => ({
117 | schema: {
118 | type: 'array',
119 | default: null,
120 | items: {
121 | type: 'string',
122 | enumKeyValue: [
123 | {
124 | key: 'option1',
125 | value: '1',
126 | },
127 | {
128 | key: 'option2',
129 | value: '2',
130 | },
131 | {
132 | key: 'option3',
133 | value: '3',
134 | },
135 | ],
136 | },
137 | },
138 | value: ['1', '2'],
139 | }),
140 | })
141 | const picker = wrapper.find({ name: 'JsfSelection' })
142 | expect(picker.vm.value).toStrictEqual(['1', '2'])
143 | })
144 | it('when type of selection is multiple, el value should be the same as default', async () => {
145 | const wrapper: any = mount(Wrapper, {
146 | data: () => ({
147 | schema: {
148 | type: 'array',
149 | default: ['1', '2'],
150 | items: {
151 | type: 'string',
152 | enumKeyValue: [
153 | {
154 | key: 'option1',
155 | value: '1',
156 | },
157 | {
158 | key: 'option2',
159 | value: '2',
160 | },
161 | {
162 | key: 'option3',
163 | value: '3',
164 | },
165 | ],
166 | },
167 | },
168 | value: undefined,
169 | }),
170 | })
171 | const picker = wrapper.find({ name: 'JsfSelection' })
172 | await wrapper.vm.$nextTick()
173 | expect(picker.vm.value).toStrictEqual(['1', '2'])
174 | })
175 | it('when type of selection is multiple, value should change when el emit input event', async () => {
176 | const wrapper: any = mount(Wrapper, {
177 | data: () => ({
178 | schema: {
179 | type: 'array',
180 | default: null,
181 | items: {
182 | type: 'string',
183 | enumKeyValue: [
184 | {
185 | key: 'option1',
186 | value: '1',
187 | },
188 | {
189 | key: 'option2',
190 | value: '2',
191 | },
192 | {
193 | key: 'option3',
194 | value: '3',
195 | },
196 | ],
197 | },
198 | },
199 | value: undefined,
200 | }),
201 | })
202 | const elPicker = wrapper.find({ name: 'JsfSelection' })
203 | elPicker.vm.$emit('input', ['1', '2'])
204 | expect(wrapper.vm.value).toStrictEqual(['1', '2'])
205 | })
206 | })
207 |
--------------------------------------------------------------------------------
/src/core/schme-type-renderers/ObjectRenderer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
18 |
19 |
20 |
32 |
33 |
34 |
35 |
36 |
37 |
221 |
--------------------------------------------------------------------------------
/src/core/SchemaForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
14 |
15 |
237 |
--------------------------------------------------------------------------------
/src/core/schme-type-renderers/ArrayRenderer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
19 |
20 |
30 |
31 |
32 |
33 |
41 |
42 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
250 |
--------------------------------------------------------------------------------
/tests/unit/theme-element/dateTimeRangePicker.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 |
3 | import { initialize, Helper as Wrapper } from '../utils'
4 |
5 | initialize()
6 |
7 | describe('dateTimeRange', () => {
8 | it('format `date-time-range` should render datetimepicker', async () => {
9 | const wrapper: any = mount(Wrapper, {
10 | data: () => ({
11 | schema: {
12 | type: 'array',
13 | items: {
14 | type: 'string',
15 | format: 'date-time',
16 | },
17 | dateTimeRange: true,
18 | },
19 | }),
20 | })
21 | const picker = wrapper.find({ name: 'JsfDateTimeRangePicker' })
22 | await wrapper.vm.$nextTick()
23 | expect(picker.vm).not.toBeUndefined()
24 | })
25 |
26 | it('items format `date` is string, el value should be the same as value', () => {
27 | const date = ['2020-07-17', '2020-07-18']
28 | const wrapper: any = mount(Wrapper, {
29 | data: () => ({
30 | schema: {
31 | type: 'array',
32 | items: {
33 | type: 'string',
34 | format: 'date',
35 | },
36 | dateTimeRange: true,
37 | },
38 | value: date,
39 | }),
40 | })
41 | const picker = wrapper.find({ name: 'JsfDateTimeRangePicker' })
42 | expect(picker.vm.value).toBe(date)
43 | })
44 | it('items format `date` is string, el value should be the same as default', async () => {
45 | const date = ['2020-07-17', '2020-07-18']
46 | const wrapper: any = mount(Wrapper, {
47 | data: () => ({
48 | schema: {
49 | type: 'array',
50 | items: {
51 | type: 'string',
52 | format: 'date',
53 | },
54 | dateTimeRange: true,
55 | default: date,
56 | },
57 | value: undefined,
58 | }),
59 | })
60 | const picker = wrapper.find({ name: 'JsfDateTimeRangePicker' })
61 | await wrapper.vm.$nextTick()
62 | expect(picker.vm.value).toEqual(date)
63 | })
64 | it('items format `date` is string, value should update when el emit input event', () => {
65 | const date = ['2020-07-17', '2020-07-18']
66 | const wrapper: any = mount(Wrapper, {
67 | data: () => ({
68 | schema: {
69 | type: 'array',
70 | items: {
71 | type: 'string',
72 | format: 'date',
73 | },
74 | dateTimeRange: true,
75 | },
76 | value: undefined,
77 | }),
78 | })
79 | const picker = wrapper.find({ name: 'JsfDateTimeRangePicker' })
80 | picker.vm.$emit('input', date)
81 | expect(wrapper.vm.value).toBe(date)
82 | })
83 |
84 | it('items format `date` is number, el value should be the same as value', () => {
85 | const date = [
86 | new Date('2020-07-17').getTime(),
87 | new Date('2020-07-18').getTime(),
88 | ]
89 | const wrapper: any = mount(Wrapper, {
90 | data: () => ({
91 | schema: {
92 | type: 'array',
93 | items: {
94 | type: 'number',
95 | format: 'date',
96 | },
97 | dateTimeRange: true,
98 | },
99 | value: date,
100 | }),
101 | })
102 | const picker = wrapper.find({ name: 'JsfDateTimeRangePicker' })
103 | expect(picker.vm.value).toBe(date)
104 | })
105 | it('items format `date` is number, el value should be the same as default', async () => {
106 | const date = [
107 | new Date('2020-07-17').getTime(),
108 | new Date('2020-07-18').getTime(),
109 | ]
110 | const wrapper: any = mount(Wrapper, {
111 | data: () => ({
112 | schema: {
113 | type: 'array',
114 | items: {
115 | type: 'number',
116 | format: 'date',
117 | },
118 | dateTimeRange: true,
119 | default: date,
120 | },
121 | value: undefined,
122 | }),
123 | })
124 | const picker = wrapper.find({ name: 'JsfDateTimeRangePicker' })
125 | await wrapper.vm.$nextTick()
126 | expect(picker.vm.value).toEqual(date)
127 | })
128 | it('items format `date` is number, value should update when el emit input event', () => {
129 | const date = [
130 | new Date('2020-07-17').getTime(),
131 | new Date('2020-07-18').getTime(),
132 | ]
133 | const wrapper: any = mount(Wrapper, {
134 | data: () => ({
135 | schema: {
136 | type: 'array',
137 | items: {
138 | type: 'number',
139 | format: 'date',
140 | },
141 | dateTimeRange: true,
142 | },
143 | value: undefined,
144 | }),
145 | })
146 | const picker = wrapper.find({ name: 'JsfDateTimeRangePicker' })
147 | picker.vm.$emit('input', date)
148 | expect(wrapper.vm.value).toBe(date)
149 | })
150 |
151 | it('items format `date-time` is string, el value should be the same as value', () => {
152 | const date = ['2020-07-17 10:20:30', '2020-07-18 10:20:30']
153 | const wrapper: any = mount(Wrapper, {
154 | data: () => ({
155 | schema: {
156 | type: 'array',
157 | items: {
158 | type: 'string',
159 | format: 'date-time',
160 | },
161 | dateTimeRange: true,
162 | },
163 | value: date,
164 | }),
165 | })
166 | const picker = wrapper.find({ name: 'JsfDateTimeRangePicker' })
167 | expect(picker.vm.value).toBe(date)
168 | })
169 | it('items format `date-time` is string, el value should be the same as default', async () => {
170 | const date = ['2020-07-17 10:20:30', '2020-07-18 10:20:30']
171 | const wrapper: any = mount(Wrapper, {
172 | data: () => ({
173 | schema: {
174 | type: 'array',
175 | items: {
176 | type: 'string',
177 | format: 'date-time',
178 | },
179 | dateTimeRange: true,
180 | default: date,
181 | },
182 | value: undefined,
183 | }),
184 | })
185 | const picker = wrapper.find({ name: 'JsfDateTimeRangePicker' })
186 | await wrapper.vm.$nextTick()
187 | expect(picker.vm.value).toEqual(date)
188 | })
189 | it('items format `date-time` is string, value should update when el emit input event', () => {
190 | const date = ['2020-07-17 10:20:30', '2020-07-18 10:20:30']
191 | const wrapper: any = mount(Wrapper, {
192 | data: () => ({
193 | schema: {
194 | type: 'array',
195 | items: {
196 | type: 'string',
197 | format: 'date-time',
198 | },
199 | dateTimeRange: true,
200 | },
201 | value: undefined,
202 | }),
203 | })
204 | const picker = wrapper.find({ name: 'JsfDateTimeRangePicker' })
205 | picker.vm.$emit('input', date)
206 | expect(wrapper.vm.value).toBe(date)
207 | })
208 |
209 | it('items format `date-time` is number, el value should be the same as value', () => {
210 | const date = [
211 | new Date('2020-07-17 10:20:30').getTime(),
212 | new Date('2020-07-18 10:20:30').getTime(),
213 | ]
214 | const wrapper: any = mount(Wrapper, {
215 | data: () => ({
216 | schema: {
217 | type: 'array',
218 | items: {
219 | type: 'number',
220 | format: 'date-time',
221 | },
222 | dateTimeRange: true,
223 | },
224 | value: date,
225 | }),
226 | })
227 | const picker = wrapper.find({ name: 'JsfDateTimeRangePicker' })
228 | expect(picker.vm.value).toBe(date)
229 | })
230 | it('items format `date-time` is number, el value should be the same as default', async () => {
231 | const date = [
232 | new Date('2020-07-17 10:20:30').getTime(),
233 | new Date('2020-07-18 10:20:30').getTime(),
234 | ]
235 | const wrapper: any = mount(Wrapper, {
236 | data: () => ({
237 | schema: {
238 | type: 'array',
239 | items: {
240 | type: 'number',
241 | format: 'date-time',
242 | },
243 | dateTimeRange: true,
244 | default: date,
245 | },
246 | value: undefined,
247 | }),
248 | })
249 | const picker = wrapper.find({ name: 'JsfDateTimeRangePicker' })
250 | await wrapper.vm.$nextTick()
251 | expect(picker.vm.value).toEqual(date)
252 | })
253 | it('items format `date-time` is number, value should update when el emit input event', () => {
254 | const date = [
255 | new Date('2020-07-17 10:20:30').getTime(),
256 | new Date('2020-07-18 10:20:30').getTime(),
257 | ]
258 | const wrapper: any = mount(Wrapper, {
259 | data: () => ({
260 | schema: {
261 | type: 'array',
262 | items: {
263 | type: 'number',
264 | format: 'date-time',
265 | },
266 | dateTimeRange: true,
267 | },
268 | value: undefined,
269 | }),
270 | })
271 | const picker = wrapper.find({ name: 'JsfDateTimeRangePicker' })
272 | picker.vm.$emit('input', date)
273 | expect(wrapper.vm.value).toBe(date)
274 | })
275 | })
276 |
--------------------------------------------------------------------------------
/src/core/utils/schema.ts:
--------------------------------------------------------------------------------
1 | import jsonpointer from 'jsonpointer'
2 | import union from 'lodash.union'
3 |
4 | import { isObject, hasOwnProperty, getSchemaType } from './common'
5 | import { validateData } from '../validator'
6 |
7 | enum SchemaTypes {
8 | 'NUMBER' = 'number',
9 | 'INTEGER' = 'integer',
10 | 'STRING' = 'string',
11 | 'OBJECT' = 'object',
12 | 'ARRAY' = 'array',
13 | 'BOOLEAN' = 'boolean',
14 | }
15 |
16 | export interface VueJsonSchemaConfig {
17 | title?: string
18 | description?: string
19 | component?: string
20 | additionProps?: {
21 | [key: string]: any
22 | }
23 | withFormItem?: boolean
24 | disabled?: boolean
25 | }
26 |
27 | // type Schema = any
28 | export interface Schema {
29 | type: SchemaTypes
30 | const: any
31 | format?: string
32 | properties?: {
33 | [key: string]: Schema
34 | }
35 | items?: Schema | Schema[]
36 | dependencies?: {
37 | [key: string]: string[] | Schema
38 | }
39 | oneOf?: Schema[]
40 | vjsf?: VueJsonSchemaConfig
41 | required?: string[]
42 | enum: any[]
43 | enumKeyValue: any[]
44 | additionalProperties: any
45 | }
46 |
47 | // function resolveSchema(schema: any, data: any = {}) {}
48 |
49 | export function resolveSchema(schema: any, rootSchema = {}, formData = {}) {
50 | if (hasOwnProperty(schema, '$ref')) {
51 | return resolveReference(schema, rootSchema, formData)
52 | } else if (hasOwnProperty(schema, 'dependencies')) {
53 | const resolvedSchema = resolveDependencies(schema, rootSchema, formData)
54 | return retrieveSchema(resolvedSchema, rootSchema, formData)
55 | }
56 | // else if (hasOwnProperty(schema, "allOf")) {
57 | // return {
58 | // ...schema,
59 | // allOf: schema.allOf.map((allOfSubschema: any) =>
60 | // retrieveSchema(allOfSubschema, rootSchema, formData)
61 | // ),
62 | // };
63 | // }
64 | else {
65 | // No $ref or dependencies attribute found, returning the original schema.
66 | return schema
67 | }
68 | }
69 |
70 | export function retrieveSchema(
71 | schema: any,
72 | rootSchema = {},
73 | formData = {}
74 | ): Schema {
75 | if (!isObject(schema)) {
76 | return {} as Schema
77 | }
78 | let resolvedSchema = resolveSchema(schema, rootSchema, formData)
79 |
80 | // TODO: allOf and additionalProperties not implemented
81 | // if ("allOf" in schema) {
82 | // try {
83 | // resolvedSchema = mergeAllOf({
84 | // ...resolvedSchema,
85 | // allOf: resolvedSchema.allOf,
86 | // });
87 | // } catch (e) {
88 | // console.warn("could not merge subschemas in allOf:\n" + e);
89 | // const { allOf, ...resolvedSchemaWithoutAllOf } = resolvedSchema;
90 | // return resolvedSchemaWithoutAllOf;
91 | // }
92 | // }
93 | // const hasAdditionalProperties =
94 | // resolvedSchema.hasOwnProperty("additionalProperties") &&
95 | // resolvedSchema.additionalProperties !== false;
96 | // if (hasAdditionalProperties) {
97 | // return stubExistingAdditionalProperties(
98 | // resolvedSchema,
99 | // rootSchema,
100 | // formData
101 | // );
102 | // }
103 | return resolvedSchema
104 | }
105 |
106 | // export function processSchema(
107 | // schema: any,
108 | // rootSchema: any = {},
109 | // data: any = {}
110 | // ): Schema {
111 | // if (hasOwnProperty(schema, '$ref')) {
112 | // return resolveReference(schema, rootSchema, data)
113 | // }
114 | // if (hasOwnProperty(schema, 'dependencies')) {
115 | // const resolvedSchema = resolveSchema(schema)
116 | // }
117 | // }
118 |
119 | function resolveReference(schema: any, rootSchema: any, formData: any): Schema {
120 | // Retrieve the referenced schema definition.
121 | const $refSchema = findSchemaDefinition(schema.$ref, rootSchema)
122 | // Drop the $ref property of the source schema.
123 | const { $ref, ...localSchema } = schema
124 | // Update referenced schema definition with local schema properties.
125 | return retrieveSchema({ ...$refSchema, ...localSchema }, rootSchema, formData)
126 | }
127 |
128 | export function findSchemaDefinition($ref: string, rootSchema = {}): Schema {
129 | const origRef = $ref
130 | if ($ref.startsWith('#')) {
131 | // Decode URI fragment representation.
132 | $ref = decodeURIComponent($ref.substring(1))
133 | } else {
134 | throw new Error(`Could not find a definition for ${origRef}.`)
135 | }
136 | const current = jsonpointer.get(rootSchema, $ref)
137 | if (current === undefined) {
138 | throw new Error(`Could not find a definition for ${origRef}.`)
139 | }
140 | if (hasOwnProperty(current, '$ref')) {
141 | // return { ...current, findSchemaDefinition(current.$ref, rootSchema) } ?
142 | return findSchemaDefinition(current.$ref, rootSchema)
143 | }
144 | return current
145 | }
146 |
147 | function resolveDependencies(
148 | schema: any,
149 | rootSchema: any,
150 | formData: any
151 | ): Schema {
152 | // Drop the dependencies from the source schema.
153 | let { dependencies = {}, ...resolvedSchema } = schema
154 | // if ("oneOf" in resolvedSchema) {
155 | // resolvedSchema =
156 | // resolvedSchema.oneOf[
157 | // getMatchingOption(formData, resolvedSchema.oneOf, rootSchema)
158 | // ];
159 | // } else if ("anyOf" in resolvedSchema) {
160 | // resolvedSchema =
161 | // resolvedSchema.anyOf[
162 | // getMatchingOption(formData, resolvedSchema.anyOf, rootSchema)
163 | // ];
164 | // }
165 | return processDependencies(dependencies, resolvedSchema, rootSchema, formData)
166 | }
167 | function processDependencies(
168 | dependencies: any,
169 | resolvedSchema: any,
170 | rootSchema: any,
171 | formData: any
172 | ): Schema {
173 | // Process dependencies updating the local schema properties as appropriate.
174 | for (const dependencyKey in dependencies) {
175 | // Skip this dependency if its trigger property is not present.
176 | if (formData[dependencyKey] === undefined) {
177 | continue
178 | }
179 | // Skip this dependency if it is not included in the schema (such as when dependencyKey is itself a hidden dependency.)
180 | if (
181 | resolvedSchema.properties &&
182 | !(dependencyKey in resolvedSchema.properties)
183 | ) {
184 | continue
185 | }
186 | const {
187 | [dependencyKey]: dependencyValue,
188 | ...remainingDependencies
189 | } = dependencies
190 | if (Array.isArray(dependencyValue)) {
191 | resolvedSchema = withDependentProperties(resolvedSchema, dependencyValue)
192 | } else if (isObject(dependencyValue)) {
193 | resolvedSchema = withDependentSchema(
194 | resolvedSchema,
195 | rootSchema,
196 | formData,
197 | dependencyKey,
198 | dependencyValue
199 | )
200 | }
201 | return processDependencies(
202 | remainingDependencies,
203 | resolvedSchema,
204 | rootSchema,
205 | formData
206 | )
207 | }
208 | return resolvedSchema
209 | }
210 |
211 | function withDependentProperties(schema: any, additionallyRequired: any) {
212 | if (!additionallyRequired) {
213 | return schema
214 | }
215 | const required = Array.isArray(schema.required)
216 | ? Array.from(new Set([...schema.required, ...additionallyRequired]))
217 | : additionallyRequired
218 | return { ...schema, required: required }
219 | }
220 |
221 | function withDependentSchema(
222 | schema: any,
223 | rootSchema: any,
224 | formData: any,
225 | dependencyKey: any,
226 | dependencyValue: any
227 | ) {
228 | // retrieveSchema
229 | let { oneOf, ...dependentSchema } = retrieveSchema(
230 | dependencyValue,
231 | rootSchema,
232 | formData
233 | )
234 | schema = mergeSchemas(schema, dependentSchema)
235 | // Since it does not contain oneOf, we return the original schema.
236 | if (oneOf === undefined) {
237 | return schema
238 | } else if (!Array.isArray(oneOf)) {
239 | throw new Error(`invalid: it is some ${typeof oneOf} instead of an array`)
240 | }
241 | // Resolve $refs inside oneOf.
242 | const resolvedOneOf = oneOf.map((subschema) =>
243 | hasOwnProperty(subschema, '$ref')
244 | ? resolveReference(subschema, rootSchema, formData)
245 | : subschema
246 | )
247 | return withExactlyOneSubschema(
248 | schema,
249 | rootSchema,
250 | formData,
251 | dependencyKey,
252 | resolvedOneOf
253 | )
254 | }
255 |
256 | function withExactlyOneSubschema(
257 | schema: any,
258 | rootSchema: any,
259 | formData: any,
260 | dependencyKey: any,
261 | oneOf: any
262 | ) {
263 | const validSubschemas = oneOf.filter((subschema: any) => {
264 | if (!subschema.properties) {
265 | return false
266 | }
267 | const { [dependencyKey]: conditionPropertySchema } = subschema.properties
268 | if (conditionPropertySchema) {
269 | const conditionSchema = {
270 | type: 'object',
271 | properties: {
272 | [dependencyKey]: conditionPropertySchema,
273 | },
274 | }
275 | // TODO: validate formdata
276 | const { errors } = validateData(conditionSchema, formData)
277 | return !errors || errors.length === 0
278 | }
279 | })
280 | if (validSubschemas.length !== 1) {
281 | console.warn(
282 | "ignoring oneOf in dependencies because there isn't exactly one subschema that is valid"
283 | )
284 | return schema
285 | }
286 | // debugger
287 | const subschema = validSubschemas[0]
288 | const {
289 | [dependencyKey]: conditionPropertySchema,
290 | ...dependentSubschema
291 | } = subschema.properties
292 | const dependentSchema = { ...subschema, properties: dependentSubschema }
293 | return mergeSchemas(
294 | schema,
295 | // retrieveSchema
296 | retrieveSchema(dependentSchema, rootSchema, formData)
297 | )
298 | }
299 |
300 | // Recursively merge deeply nested schemas.
301 | // The difference between mergeSchemas and mergeObjects
302 | // is that mergeSchemas only concats arrays for
303 | // values under the "required" keyword, and when it does,
304 | // it doesn't include duplicate values.
305 | export function mergeSchemas(obj1: any, obj2: any) {
306 | var acc = Object.assign({}, obj1) // Prevent mutation of source object.
307 | return Object.keys(obj2).reduce((acc, key) => {
308 | const left = obj1 ? obj1[key] : {},
309 | right = obj2[key]
310 | if (obj1 && hasOwnProperty(obj1, key) && isObject(right)) {
311 | acc[key] = mergeSchemas(left, right)
312 | } else if (
313 | obj1 &&
314 | obj2 &&
315 | (getSchemaType(obj1) === 'object' || getSchemaType(obj2) === 'object') &&
316 | key === 'required' &&
317 | Array.isArray(left) &&
318 | Array.isArray(right)
319 | ) {
320 | // Don't include duplicate values when merging
321 | // "required" fields.
322 | acc[key] = union(left, right)
323 | } else {
324 | acc[key] = right
325 | }
326 | return acc
327 | }, acc)
328 | }
329 |
330 | export function getVJSFConfig(
331 | schema: Schema,
332 | uiSchema: VueJsonSchemaConfig | undefined
333 | ): VueJsonSchemaConfig {
334 | if (uiSchema) return uiSchema
335 | return schema.vjsf || {}
336 | }
337 |
--------------------------------------------------------------------------------