├── .browserslistrc
├── .coveralls.yml
├── .editorconfig
├── .env
├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── pull_request_template.md
├── .gitignore
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── babel.config.js
├── docs
├── .vuepress
│ ├── components
│ │ ├── CustomWrapper.vue
│ │ ├── Demo.vue
│ │ └── SourceCode.vue
│ ├── config.js
│ ├── customComponentsConfig.js
│ ├── customWrapperConfig.js
│ ├── enhanceApp.js
│ ├── public
│ │ └── logo.png
│ ├── schemas
│ │ ├── arrayOfObjects.js
│ │ ├── basic.js
│ │ ├── conditions.js
│ │ ├── conditionsAllOf.js
│ │ ├── conditionsAllOfSeveral.js
│ │ ├── conditionsOneOf.js
│ │ ├── customComponents.js
│ │ ├── defaults.js
│ │ ├── home.js
│ │ ├── index.js
│ │ ├── nested.js
│ │ ├── order.js
│ │ ├── radio.js
│ │ └── selectTitles.js
│ ├── shims-vue.d.ts
│ ├── styles.css
│ └── ui-schemas.js
├── README.md
├── examples
│ └── README.md
└── guide
│ └── README.md
├── jest.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── favicon.ico
└── index.html
├── rollup.config.js
├── src
├── App.vue
├── JsonSchema
│ ├── JsonSchema.vue
│ ├── JsonSchemaArray.vue
│ ├── JsonSchemaArrayFormWrap.vue
│ ├── JsonSchemaForm.vue
│ ├── __tests__
│ │ └── JsonSchema.spec.ts
│ └── index.ts
├── __tests__
│ ├── App.spec.ts
│ ├── entry.spec.ts
│ └── main.spec.ts
├── components
│ ├── Checkbox.vue
│ ├── InputWrapper.vue
│ ├── Radio.vue
│ ├── Select.vue
│ ├── TextInput.vue
│ ├── __tests__
│ │ ├── Checkbox.spec.ts
│ │ ├── InputWrapper.spec.ts
│ │ ├── Radio.spec.ts
│ │ ├── Select.spec.ts
│ │ ├── TextInput.spec.ts
│ │ └── restrict-directive.spec.ts
│ └── restrict-directive.ts
├── entry.ts
├── main.ts
├── nanoclone.d.ts
├── shims-tsx.d.ts
├── shims-vue.d.ts
├── types.ts
├── utils
│ ├── __testData__.ts
│ ├── __tests__
│ │ ├── defaultComponents.spec.ts
│ │ ├── errorMessages.spec.ts
│ │ ├── generateDefaultValue.spec.ts
│ │ ├── getComponent.spec.ts
│ │ ├── getErrorText.spec.ts
│ │ ├── mergeDeep.spec.ts
│ │ ├── processConditions.spec.ts
│ │ ├── processSchema.spec.ts
│ │ ├── setValidators.spec.ts
│ │ └── unrefSchema.spec.ts
│ ├── defaultComponents.ts
│ ├── errorMessages.ts
│ ├── generateDefaultValue.ts
│ ├── getComponent.ts
│ ├── getErrorText.ts
│ ├── mergeDeep.ts
│ ├── processConditions.ts
│ ├── processSchema.ts
│ ├── setValidators.ts
│ └── unrefSchema.ts
└── vuelidate.d.ts
├── tests
└── unit
│ └── .eslintrc.js
├── tsconfig.json
└── vue.config.js
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 |
--------------------------------------------------------------------------------
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | service_name: travis-pro
2 | repo_token: DOfEgBH8FE8dVHr6vfj6F1csFhQCjDafR
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | trim_trailing_whitespace = true
5 | insert_final_newline = true
6 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | BUILD=hz
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true
5 | },
6 | 'extends': [
7 | 'plugin:vue/essential',
8 | '@vue/standard',
9 | '@vue/typescript'
10 | ],
11 | rules: {
12 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
14 | 'vue/valid-v-on': ['error', {
15 | 'modifiers': ['eventname]']
16 | }]
17 | },
18 | parserOptions: {
19 | parser: '@typescript-eslint/parser'
20 | },
21 | overrides: [
22 | {
23 | files: [
24 | '**/__tests__/*.{j,t}s?(x)'
25 | ],
26 | env: {
27 | jest: true
28 | }
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | #### What's this PR do?
2 | #### Where should the reviewer start?
3 | #### How should this be manually tested?
4 | #### Any background context you want to provide?
5 | #### What are the relevant tickets?
6 | #### Screenshots (if appropriate)
7 | #### Questions:
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | /docs/.vuepress/dist
5 | /coverage
6 | stats.html
7 |
8 | # local env files
9 | .env.local
10 | .env.*.local
11 |
12 | # Log files
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 |
17 | # Editor directories and files
18 | .idea
19 | .vscode
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - lts/*
4 | install:
5 | - npm ci
6 | script:
7 | - npm run lint
8 | - npm run test:unit
9 | - npm run build
10 | - npm run docs:build
11 | after_script:
12 | - cat ./coverage/lcov.info | coveralls
13 | deploy:
14 | provider: pages
15 | skip_cleanup: true
16 | local_dir: docs/.vuepress/dist
17 | github_token: $GITHUB_TOKEN # A token generated on GitHub allowing Travis to push code on you repository. Set in the Travis settings page of your repository, as a secure variable
18 | keep_history: true
19 | on:
20 | branch: master
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at . All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | TBA
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Roman Sabirov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.com/roma219/vue-jsonschema-form) [](https://coveralls.io/github/roma219/vue-jsonschema-form?branch=master&service=github&kill_cache=1) [](https://david-dm.org/roma219/vue-jsonschema-form) [](https://snyk.io/test/github/roma219/vue-jsonschema-form) [](https://lgtm.com/projects/g/roma219/vue-jsonschema-form/context:javascript) 
2 |
3 |
4 | 
5 | 
6 |
7 | # vue-jsonschema-form
8 | JSON Schema based form generator built with Vue.js. Currently Work in Progress.
9 |
10 | ### Full Guide and Examples
11 | https://roma219.github.io/vue-jsonschema-form/guide/
12 |
13 | [](https://codesandbox.io/s/vue-jsonschema-form-basic-example-ulwwy?fontsize=14&hidenavigation=1&module=%2Fsrc%2FApp.vue&theme=dark)
14 |
15 | ### Installation
16 | ```
17 | npm install @roma219/vue-jsonschema-form
18 | ```
19 |
20 | ### Usage
21 | ```
22 |
23 |
24 | schema = {
25 | type: 'object',
26 | properties: {
27 | aaa: { type: 'string', minLength: 1 },
28 | bbb: { type: 'boolean' },
29 | ccc: { type: 'string', enum: ['1', '2', '3'] },
30 | ddd: {
31 | type: 'object',
32 | title: '',
33 | properties: {
34 | a1: { type: 'string', minLength: 1, maxLength: 5 },
35 | b2: { type: 'boolean', default: true },
36 | ddd: {
37 | type: 'object',
38 | properties: {
39 | a1: { type: 'string', default: 'aaa' },
40 | b2: { type: 'boolean' }
41 | }
42 | }
43 | }
44 | }
45 | }
46 | }
47 | ```
48 |
49 |
50 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [ [ '@vue/app', { useBuiltIns: 'entry' } ] ]
3 | }
4 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/CustomWrapper.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
19 |
20 |
34 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/Demo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 | {{ tab }}
11 |
12 |
13 |
26 |
27 |
28 |
29 |
30 |
134 |
135 |
184 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/SourceCode.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
24 |
25 |
53 |
--------------------------------------------------------------------------------
/docs/.vuepress/config.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 | const webpack = require('webpack')
3 |
4 | module.exports = {
5 | title: 'Vue JSON Schema Form',
6 | description: 'Form Generator based on JSON Schema',
7 | base: '/vue-jsonschema-form/',
8 | head: [
9 | ['link', { rel: 'icon', href: '/logo.png' }]
10 | ],
11 | themeConfig: {
12 | repo: 'roma219/vue-jsonschema-form',
13 | docsDir: 'docs',
14 | search: true,
15 | displayAllHeaders: true,
16 | nav: [
17 | { text: 'Home', link: '/' },
18 | { text: 'Guide', link: '/guide/' },
19 | { text: 'Examples', link: '/examples/' }
20 | ],
21 | sidebar: [
22 | {
23 | title: 'Guide',
24 | path: '/guide/',
25 | collapsable: false,
26 | sidebarDepth: 1
27 | },
28 | {
29 | title: 'Examples',
30 | path: '/examples/'
31 | }
32 | ]
33 | },
34 | plugins: [
35 | [
36 | 'vuepress-plugin-typescript',
37 | {
38 | tsLoaderOptions: {
39 | // All options of ts-loader
40 | },
41 | }
42 | ]
43 | ],
44 | configureWebpack: (config) => {
45 | return { plugins: [
46 | new webpack.EnvironmentPlugin({ ...process.env })
47 | ]}
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/docs/.vuepress/customComponentsConfig.js:
--------------------------------------------------------------------------------
1 | [{
2 | componentName: 'VSelect',
3 | contains: 'enum',
4 | props: (propName, schema, uiSchema) => ({
5 | label: schema.title || propName,
6 | items: schema.enum,
7 | outlined: true
8 | }),
9 | eventName: 'change'
10 | }, {
11 | componentName: 'VSwitch',
12 | matcher: { type: 'boolean' },
13 | eventName: 'change',
14 | props: (propName, schema, uiSchema) => ({
15 | label: schema.title || propName
16 | })
17 | },{
18 | componentName: 'VTextField',
19 | matcher: { type: 'string' },
20 | props: (propName, schema, uiSchema) => ({
21 | label: schema.title || propName,
22 | outlined: true,
23 | clearable: true,
24 | hint: schema.description,
25 | 'persistent-hint': true
26 | })
27 | }, {
28 | componentName: 'VDatePicker',
29 | uiSchemaMatcher: { uiType: 'datepicker' },
30 | eventName: 'change',
31 | props: (propName, schema, uiSchema) => ({
32 | 'full-width': true
33 | })
34 | }]
35 |
--------------------------------------------------------------------------------
/docs/.vuepress/customWrapperConfig.js:
--------------------------------------------------------------------------------
1 | {
2 | componentName: 'CustomWrapper',
3 | props: (propName, schema, uiSchema) => ({
4 | title: schema.title || propName
5 | })
6 | }
7 |
--------------------------------------------------------------------------------
/docs/.vuepress/enhanceApp.js:
--------------------------------------------------------------------------------
1 | import JsonSchema from './../../src/JsonSchema/JsonSchema.vue'
2 | import JsonSchemaForm from './../../src/JsonSchema/JsonSchemaForm.vue'
3 | import JsonSchemaArray from './../../src/JsonSchema/JsonSchemaArray.vue'
4 | import TextInput from './../../src/components/TextInput.vue'
5 | import Checkbox from './../../src/components/Checkbox.vue'
6 | import Radio from './../../src/components/Radio.vue'
7 | import Select from './../../src/components/Select.vue'
8 | import InputWrapper from './../../src/components/InputWrapper.vue'
9 | import VueHighlightJS from 'vue-highlightjs'
10 | import javascript from 'highlight.js/lib/languages/javascript';
11 | import json from 'highlight.js/lib/languages/json';
12 | import Vuetify, { VTextField, VSwitch, VSelect, VApp, VDatePicker } from 'vuetify/lib'
13 | import '@mdi/font/css/materialdesignicons.css'
14 | import 'highlight.js/styles/ocean.css';
15 | import './styles.css'
16 |
17 | export default ({ Vue, options, router, siteData }) => {
18 | Vue.use(Vuetify, {
19 | components: { VTextField, VSwitch, VSelect, VDatePicker, VApp }
20 | })
21 | Vue.use(VueHighlightJS, {
22 | languages: {
23 | javascript,
24 | json
25 | }
26 | })
27 | options.vuetify = new Vuetify({})
28 | Vue.component('JsonSchema', JsonSchema)
29 | Vue.component('JsonSchemaArray', JsonSchemaArray)
30 | Vue.component('JsonSchemaForm', JsonSchemaForm)
31 | Vue.component('TextInput', TextInput)
32 | Vue.component('Checkbox', Checkbox)
33 | Vue.component('Radio', Radio)
34 | Vue.component('Select', Select)
35 | Vue.component('InputWrapper', InputWrapper)
36 | }
37 |
--------------------------------------------------------------------------------
/docs/.vuepress/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/roma219/vue-jsonschema-form/dcc87157b7773fdc9d0ffe975dfc356d462f1463/docs/.vuepress/public/logo.png
--------------------------------------------------------------------------------
/docs/.vuepress/schemas/arrayOfObjects.js:
--------------------------------------------------------------------------------
1 | export default {
2 | type: 'object',
3 | properties: {
4 | array: {
5 | type: 'array',
6 | title: 'Users',
7 | items: {
8 | type: 'object',
9 | properties: { name: { type: 'string', title: 'Username' } }
10 | }
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/docs/.vuepress/schemas/basic.js:
--------------------------------------------------------------------------------
1 | export default {
2 | type: 'object',
3 | properties: {
4 | a: { type: 'string', title: 'Username' },
5 | b: { type: 'boolean', title: 'Use Avatar' },
6 | mySelect: {
7 | type: 'string',
8 | title: 'Account Type',
9 | enum: ['User', 'Editor', 'Admin']
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/docs/.vuepress/schemas/conditions.js:
--------------------------------------------------------------------------------
1 | export default {
2 | type: 'object',
3 | properties: {
4 | a: { type: 'string', title: 'Your favourite front-end framework?' },
5 | b: { type: 'number', title: 'Amount of likes' }
6 | },
7 | if: {
8 | properties: {
9 | a: {
10 | const: 'Vue'
11 | }
12 | }
13 | },
14 | then: {
15 | properties: {
16 | b: {
17 | minimum: 1
18 | },
19 | c: { type: 'boolean', title: 'Are you sure?' }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/docs/.vuepress/schemas/conditionsAllOf.js:
--------------------------------------------------------------------------------
1 | export default {
2 | type: 'object',
3 | properties: {
4 | a: { type: 'string', title: 'Your favourite front-end framework?' },
5 | b: { type: 'number', title: 'Amount of likes' },
6 | c: { type: 'boolean', title: 'Are you sure?' }
7 | },
8 | if: {
9 | allOf: [{
10 | properties: {
11 | a: {
12 | const: 'Vue'
13 | }
14 | }
15 | }, {
16 | properties: {
17 | c: {
18 | const: true
19 | }
20 | }
21 | }]
22 | },
23 | then: {
24 | properties: {
25 | x: {
26 | type: 'string', title: 'Field'
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/docs/.vuepress/schemas/conditionsAllOfSeveral.js:
--------------------------------------------------------------------------------
1 | export default {
2 | type: 'object',
3 | properties: {
4 | a: { type: 'string', title: 'Your favourite front-end framework?' },
5 | b: { type: 'number', title: 'Amount of likes' },
6 | c: { type: 'boolean', title: 'Are you sure?' }
7 | },
8 | allOf: [{
9 | if: {
10 | properties: {
11 | a: {
12 | const: 'Vue'
13 | }
14 | }
15 | },
16 | then: {
17 | properties: {
18 | x: {
19 | type: 'string', title: 'Field'
20 | }
21 | }
22 | }
23 | }, {
24 | if: {
25 | properties: {
26 | a: {
27 | const: 'React'
28 | }
29 | }
30 | },
31 | then: {
32 | properties: {
33 | y: {
34 | type: 'boolean', title: 'Another'
35 | }
36 | }
37 | }
38 | }]
39 | }
40 |
--------------------------------------------------------------------------------
/docs/.vuepress/schemas/conditionsOneOf.js:
--------------------------------------------------------------------------------
1 | export default {
2 | type: 'object',
3 | properties: {
4 | a: { type: 'string', title: 'Your favourite front-end framework?' },
5 | b: { type: 'number', title: 'Amount of likes' }
6 | },
7 | if: {
8 | oneOf: [{
9 | properties: {
10 | a: {
11 | const: 'Vue'
12 | }
13 | }
14 | }, {
15 | properties: {
16 | a: {
17 | const: 'React'
18 | }
19 | }
20 | }
21 | ]
22 | },
23 | then: {
24 | properties: {
25 | b: {
26 | minimum: 1
27 | },
28 | c: { type: 'boolean', title: 'Are you sure?' }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/docs/.vuepress/schemas/customComponents.js:
--------------------------------------------------------------------------------
1 | export default {
2 | type: 'object',
3 | properties: {
4 | a: { title: 'Name', type: 'string', description: 'Very important field' },
5 | confirm: { type: 'boolean' },
6 | c: { title: 'Planet', type: 'string', enum: ['Earth', 'Mars', 'Jupiter'], default: 'Mars' },
7 | date: { type: 'string' }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/docs/.vuepress/schemas/defaults.js:
--------------------------------------------------------------------------------
1 | export default {
2 | type: 'object',
3 | properties: {
4 | a: { title: 'User Provider', type: 'string', default: 'Fabric №1' },
5 | b: { type: 'boolean', title: 'Yes?' }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/docs/.vuepress/schemas/home.js:
--------------------------------------------------------------------------------
1 | export default {
2 | type: 'object',
3 | properties: {
4 | a: { type: 'string', title: 'Name', minLength: 1 },
5 | b: { type: 'number', title: 'Age', maximum: 99 },
6 | c: { type: 'boolean', title: 'Agree' }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/docs/.vuepress/schemas/index.js:
--------------------------------------------------------------------------------
1 | import arrayOfObjects from './arrayOfObjects.js'
2 | import basic from './basic.js'
3 | import conditions from './conditions.js'
4 | import conditionsAllOf from './conditionsAllOf.js'
5 | import conditionsAllOfSeveral from './conditionsAllOfSeveral.js'
6 | import conditionsOneOf from './conditionsOneOf.js'
7 | import customComponents from './customComponents.js'
8 | import defaults from './defaults.js'
9 | import home from './home.js'
10 | import nested from './nested.js'
11 | import order from './order.js'
12 | import radio from './radio.js'
13 | import selectTitles from './selectTitles.js'
14 |
15 | export default {
16 | arrayOfObjects,
17 | basic,
18 | conditions,
19 | conditionsAllOf,
20 | conditionsAllOfSeveral,
21 | conditionsOneOf,
22 | customComponents,
23 | defaults,
24 | home,
25 | nested,
26 | order,
27 | radio,
28 | selectTitles
29 | }
30 |
--------------------------------------------------------------------------------
/docs/.vuepress/schemas/nested.js:
--------------------------------------------------------------------------------
1 | export default {
2 | type: 'object',
3 | properties: {
4 | a: { type: 'string', title: 'Username' },
5 | b: {
6 | type: 'object',
7 | title: '',
8 | properties: {
9 | c: { type: 'boolean', title: 'Is Admin' }
10 | }
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/docs/.vuepress/schemas/order.js:
--------------------------------------------------------------------------------
1 | export default {
2 | type: 'object',
3 | properties: {
4 | a: { type: 'string', title: 'Name' },
5 | b: { type: 'boolean', title: 'Superuser' },
6 | c: { type: 'string', title: 'Surname' }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/docs/.vuepress/schemas/radio.js:
--------------------------------------------------------------------------------
1 | export default {
2 | type: 'object',
3 | properties: {
4 | a: {
5 | type: 'string',
6 | title: 'User Type',
7 | enum: ['User', 'Editor', 'Admin']
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/docs/.vuepress/schemas/selectTitles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | home: {
3 | type: 'object',
4 | properties: {
5 | a: { type: 'string', title: 'Name', minLength: 1 },
6 | b: { type: 'number', title: 'Age', maximum: 99 },
7 | c: { type: 'boolean', title: 'Agree' }
8 | }
9 | },
10 | basic: {
11 | type: 'object',
12 | properties: {
13 | a: { type: 'string', title: 'Username' },
14 | b: { type: 'boolean', title: 'Use Avatar' },
15 | mySelect: {
16 | type: 'string',
17 | title: 'Account Type',
18 | enum: ['User', 'Editor', 'Admin'],
19 | default: 'User'
20 | }
21 | }
22 | },
23 | nested: {
24 | type: 'object',
25 | properties: {
26 | a: { type: 'string', title: 'Username' },
27 | b: {
28 | type: 'object',
29 | title: '',
30 | properties: {
31 | c: { type: 'boolean', title: 'Is Admin' }
32 | }
33 | }
34 | }
35 | },
36 | arrayOfObjects: {
37 | type: 'object',
38 | properties: {
39 | array: {
40 | type: 'array',
41 | title: 'Users',
42 | items: {
43 | type: 'object',
44 | properties: { name: { type: 'string', title: 'Username' } }
45 | }
46 | }
47 | }
48 | },
49 | conditions: {
50 | type: 'object',
51 | properties: {
52 | a: { type: 'string', title: 'Your favourite front-end framework?' },
53 | b: { type: 'number', title: 'Amount of likes' }
54 | },
55 | if: {
56 | properties: {
57 | a: {
58 | const: 'Vue'
59 | }
60 | }
61 | },
62 | then: {
63 | properties: {
64 | b: {
65 | minimum: 1
66 | },
67 | c: { type: 'boolean', title: 'Are you sure?' }
68 | }
69 | }
70 | },
71 | conditionsOneOf: {
72 | type: 'object',
73 | properties: {
74 | a: { type: 'string', title: 'Your favourite front-end framework?' },
75 | b: { type: 'number', title: 'Amount of likes' }
76 | },
77 | if: {
78 | oneOf: [{
79 | properties: {
80 | a: {
81 | const: 'Vue'
82 | }
83 | }
84 | }, {
85 | properties: {
86 | a: {
87 | const: 'React'
88 | }
89 | }
90 | }
91 | ]
92 | },
93 | then: {
94 | properties: {
95 | b: {
96 | minimum: 1
97 | },
98 | c: { type: 'boolean', title: 'Are you sure?' }
99 | }
100 | }
101 | },
102 | conditionsAllOf: {
103 | type: 'object',
104 | properties: {
105 | a: { type: 'string', title: 'Your favourite front-end framework?' },
106 | b: { type: 'number', title: 'Amount of likes' },
107 | c: { type: 'boolean', title: 'Are you sure?' }
108 | },
109 | if: {
110 | allOf: [{
111 | properties: {
112 | a: {
113 | const: 'Vue'
114 | }
115 | }
116 | }, {
117 | properties: {
118 | c: {
119 | const: true
120 | }
121 | }
122 | }]
123 | },
124 | then: {
125 | properties: {
126 | x: {
127 | type: 'string', title: 'Field'
128 | }
129 | }
130 | }
131 | },
132 | conditionsAllOfSeveral: {
133 | type: 'object',
134 | properties: {
135 | a: { type: 'string', title: 'Your favourite front-end framework?' },
136 | b: { type: 'number', title: 'Amount of likes' },
137 | c: { type: 'boolean', title: 'Are you sure?' }
138 | },
139 | allOf: [{
140 | if: {
141 | properties: {
142 | a: {
143 | const: 'Vue'
144 | }
145 | }
146 | },
147 | then: {
148 | properties: {
149 | x: {
150 | type: 'string', title: 'Field'
151 | }
152 | }
153 | }
154 | }, {
155 | if: {
156 | properties: {
157 | a: {
158 | const: 'React'
159 | }
160 | }
161 | },
162 | then: {
163 | properties: {
164 | y: {
165 | type: 'boolean', title: 'Another'
166 | }
167 | }
168 | }
169 | }]
170 | },
171 | defaults: {
172 | type: 'object',
173 | properties: {
174 | a: { title: 'User Provider', type: 'string', default: 'Fabric №1' },
175 | b: { type: 'boolean', title: 'Yes?' }
176 | }
177 | },
178 | radio: {
179 | type: 'object',
180 | properties: {
181 | a: {
182 | type: 'string',
183 | title: 'User Type',
184 | enum: ['User', 'Editor', 'Admin']
185 | }
186 | }
187 | },
188 | order: {
189 | type: 'object',
190 | properties: {
191 | a: { type: 'string', title: 'Name' },
192 | b: { type: 'boolean', title: 'Superuser' },
193 | c: { type: 'string', title: 'Surname' }
194 | }
195 | },
196 | selectTitles: {
197 | type: 'object',
198 | properties: {
199 | a: {
200 | type: 'string',
201 | enum: ['option 1', 'option 2', 'option 3']
202 | }
203 | }
204 | },
205 | customComponents: {
206 | type: 'object',
207 | properties: {
208 | a: { title: 'Name', type: 'string', description: 'Very important field' },
209 | confirm: { type: 'boolean' },
210 | c: { title: 'Planet', type: 'string', enum: ['Earth', 'Mars', 'Jupiter'], default: 'Mars' },
211 | date: { type: 'string' }
212 | }
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/docs/.vuepress/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import Vue from 'vue'
3 | export default Vue
4 | }
5 |
--------------------------------------------------------------------------------
/docs/.vuepress/styles.css:
--------------------------------------------------------------------------------
1 | .theme-default-content:not(.custom),
2 | .page-nav {
3 | max-width: 960px!important;
4 | }
5 |
6 | .hljs {
7 | color: hsl(219, 13%, 78%)!important;
8 | }
9 |
10 | .v-application--wrap {
11 | min-height: auto!important;
12 | }
13 |
--------------------------------------------------------------------------------
/docs/.vuepress/ui-schemas.js:
--------------------------------------------------------------------------------
1 | export default {
2 | order: {
3 | properties: {
4 | c: { order: 3 },
5 | b: { order: 2 },
6 | a: { order: 1 }
7 | }
8 | },
9 | radio: {
10 | properties: {
11 | a: {
12 | uiType: 'radio'
13 | }
14 | }
15 | },
16 | selectTitles: {
17 | properties: {
18 | a: {
19 | titles: ['custom title', 'second', 'third one']
20 | }
21 | }
22 | },
23 | customComponents: {
24 | properties: {
25 | date: {
26 | uiType: 'datepicker'
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | home: true
3 | heroImage: /logo.png
4 | actionText: Get Started →
5 | actionLink: /guide/
6 | features:
7 | - title: Zero Configuration
8 | details: Just provide a JSON Schema, and the form will be generated via built-in components
9 | - title: Validation Support
10 | details: Powered by Vuelidate, has built-it validation mechanism and validation errors display
11 | - title: Customizable
12 | details: You can use your own UI components
13 | footer: Vue JSON Schema Form 2020
14 | ---
15 |
16 | ### Example
17 |
18 | ``` vue
19 |
20 | ```
21 |
22 |
23 |
24 | Feel free to play with it on [Codesandbox](https://codesandbox.io/s/vue-jsonschema-form-basic-example-zid04?fontsize=14&hidenavigation=1&module=%2Fsrc%2FApp.vue&theme=dark)
25 |
26 | ---
--------------------------------------------------------------------------------
/docs/examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples
2 | ## Basic Inputs
3 | Full list of built-it components can be found [here](/guide/#built-in-components).
4 |
5 |
6 |
7 | [](https://codesandbox.io/s/vue-jsonschema-form-basic-example-ulwwy?fontsize=14&hidenavigation=1&module=%2Fsrc%2FApp.vue&theme=dark)
8 |
9 | ## Nested Object
10 | Nesting objects is supported. If you want to omit nested object's property name displayed - set it's `title` property to empty string.
11 |
12 |
13 | ## Array of Objects
14 | To specify schema for each array element, provide `items` parameter inside array parameter schema.
15 |
16 |
17 | ## Validations
18 | Supported rules: `minLength` and `maxLength` for strings, `minimum` and `maximum` for numbers.
19 |
20 |
21 | ## Conditions
22 | If condition is met, schema is merged with schema inside `then`. Minimum length (`minLength`) and equality (`const`) conditions are supported. Also `oneOf` (condition is met if only one `if` is met) and `allOf` (condition is met if all `if`s are met) combinations can be used. This is usefull when you want to display different parts of schema based on some parameter's value or when you want to validate some parts of the schema conditionally.
23 |
24 | Try typing in `Vue`.
25 |
26 |
27 | `oneOf` usage example. Condition is met when `a` is `Vue` or `React`.
28 |
29 |
30 | `allOf` usage example. Condition is met when a is `Vue` and `c` is `true`.
31 |
32 |
33 | `allOf` can also be used on the top level to implement multiple conditions. Try setting `a` to `Vue` and to `React`.
34 |
35 |
36 | ## Default Values
37 | Sometimes, usually when initializing a new data instance, you would want to use some default values. Full data model object with default values is emitted after initialization through a `@init-default` event.
38 | ``` vue
39 |
40 | ```
41 |
42 |
43 | ## UI Schema
44 | ### Using specific UI component
45 |
46 | ### Setting display order
47 | Setting `order` property in UI schema will set the order for displaying corresponding components. The bigger `order` - the higher the component will be displayed.
48 |
49 |
50 | ## Custom Components
51 | Example showcases usage with some [Vuetify](https://vuetifyjs.com/) components. Using `uiSchema` is a convinient way to use specific components for some use-cases (e.g. Datepicker).
52 |
53 |
54 |
55 | <<< @/docs/.vuepress/customComponentsConfig.js
56 |
57 |
58 | ## Custom Wrapper Component
59 | In this case we use `CustomWrapper.vue` as componet to align labels and inputs horizontally.
60 |
61 |
62 |
63 | <<< @/docs/.vuepress/components/CustomWrapper.vue
64 |
65 |
66 |
67 | <<< @/docs/.vuepress/customWrapperConfig.js
68 |
--------------------------------------------------------------------------------
/docs/guide/README.md:
--------------------------------------------------------------------------------
1 | ## Introduction
2 | Vue JSON Schema Form library uses JSON presented in a [JSON Schema Standard](https://json-schema.org/) to generate an input form and update provided data model.
3 |
4 | ## Installation
5 | ```
6 | npm install --save @roma219/vue-jsonschema-form
7 | ```
8 |
9 | ## Usage
10 | ``` vue
11 |
12 | ```
13 |
14 | ## JSON Schema
15 | Schema should follow [JSON Schema Standard](https://json-schema.org/). Root shema type should be `object`.
16 |
17 | ## Props
18 | | Prop Name | Value Type | Description |
19 | | ------------- |:-------------:| -----:|
20 | | schema | object | JSON Schema |
21 | | ui-schema | object | [UI Schema](/guide/#custom-components) |
22 | | value | object | Data model object |
23 | | components | array | [Custom Components](/guide/#custom-components) |
24 | | wrapper | object | [Custom Wrapper Component](/guide/#custom-wrapper-component) |
25 |
26 | ## Events
27 | | Event Name | Emitted Value Type | Description |
28 | | ------------- |:-------------:| -----:|
29 | | input | object | Emitted on every data change. The argument is updated data model object (`:value` prop). |
30 | | init-default | object | Initial data model object generated with `default` values provided in schema. Usefull when you have an empty data model at the start. See [example](/examples/#default-values). |
31 | | validated | boolean | Emitted on every validation status change. `true` - data model is valid, `false` - data model is not valid. Usefull when you need to have some indicator of form validity, for example to disable `Save` button.|
32 |
33 | ## Built-in Components
34 | This is the list of built-in components and corresponding JSON Schema blocks. If you want to use different (your own or some UI kit) components, see [Custom Components](/guide/#custom-components).
35 |
36 | ### String Input
37 | ```js
38 | {
39 | type: 'string'
40 | }
41 | ```
42 |
43 |
44 |
45 |
46 | ### Number Input
47 | ```js
48 | {
49 | type: 'number'
50 | }
51 | ```
52 |
53 |
54 |
55 |
56 | ### Boolean Input
57 | ```js
58 | {
59 | type: 'boolean'
60 | }
61 | ```
62 |
63 |
64 |
65 |
66 |
67 | ### Select
68 | ```js
69 | {
70 | enum: ['option 1', 'option 2', 'option 3']
71 | }
72 | ```
73 |
74 |
75 |
76 |
77 |
78 | ### Object
79 | ```js
80 | {
81 | type: 'object',
82 | properties: {
83 | a: { type: 'string' },
84 | b: { type: 'number' }
85 | }
86 | }
87 | ```
88 |
89 |
90 |
91 |
92 | ### Array Of Objects
93 | ```js
94 | {
95 | type: 'array',
96 | items: {
97 | type: 'object',
98 | properties: {
99 | a: { type: 'string' },
100 | b: { type: 'number' }
101 | }
102 | }
103 | }
104 | ```
105 |
106 |
107 |
108 | ## UI Schema
109 | UI Schema is an optional schema which can provide additional UI features that cannot be implemented via regular JSON Schema, such as using specific UI Component.
110 | ```js
111 | // schema
112 | {
113 | type: 'object',
114 | properties: {
115 | a: {
116 | type: 'string',
117 | enum: ['option 1', 'option 2', 'option 3']
118 | }
119 | }
120 | }
121 |
122 | // ui schema
123 | {
124 | properties: {
125 | a: {
126 | uiType: 'radio'
127 | }
128 | }
129 | }
130 | ```
131 |
132 |
133 |
134 |
135 |
136 | ## Custom Components
137 | You can use custom input components with Vue JSON Schema Form. Component is selected for rendering a piece of schema by checking the `matcher` parameter.
138 | ### Requirements
139 | - Each component should be globally registered in `Vue`
140 | - Each component should have a prop `value` to receive corresponding value
141 | - Each component should emit an event on every value change
142 |
143 | ### Usage
144 | ``` vue
145 |
146 | ```
147 | Custom Components config is provided via `components` prop to `JsonSchema` component. `components` should be an array containing each component's config.
148 |
149 | ### Component Config Structure
150 | Component is mapped to JSON Schema piece by using either of the following parameters: `matcher`, `uiSchemaMatcher`, `contains`.
151 |
152 | | Parameter | Value Type | Required | Default |Description |
153 | | ------------- |:-------------:| :-------------:| :-------------:| -----:|
154 | | matcher |object | no | - | Object that should be contained in parameter's schema piece |
155 | | uiSchemaMatcher |object | no | - | Object that should be contained in property's UI Schema piece. Used if `matcher` is not provided. See matchers for [built-in components](/guide/#built-in-components).|
156 | | contains |string | no | - | Name of the parameter that should be present in a parameter's schema piece. Used if `matcher` and `uiSchemaMatcher` are not provided.|
157 | | componentName | string | yes | `TextInput` | Name of the Vue component |
158 | | eventName | string | no | `input` | Name of the event that is gonna be emitted on each value change |
159 | | props | function | no | - | Function that should return an object, that will be bound as props to component. Usefull when you want to provide additional data to component from schema.
`(propName, schema, uiSchema) => ({ ... })`|
160 |
161 | See example [here](/examples/#custom-components).
162 |
163 | ## Custom Wrapper Component
164 | ``` vue
165 |
166 | ```
167 | Each schema component is rendered inside a wrapper component. By default it displays property title and possible validation errors. You can provide your own wrapper. The only requirment is that it should contain a `` where actual input component will be displayed. `props` function is optional and should return an object which will be passed downs as props to each wrapper component.
168 | This is default wrapper component config:
169 | ``` js
170 | {
171 | componentName: 'InputWrapper',
172 | props: (propName, schema, uiSchema) => ({
173 | title: schema.title || schema.title === '' ? schema.title : propName,
174 | disabled: uiSchema && uiSchema.disabled,
175 | vertical: schema.type === 'object' || schema.type === 'array'
176 | })
177 | }
178 | ```
179 | See example [here](/examples/#custom-wrapper-component).
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleFileExtensions: [
3 | 'js',
4 | 'jsx',
5 | 'json',
6 | 'vue',
7 | 'ts',
8 | 'tsx'
9 | ],
10 | transform: {
11 | '^.+\\.vue$': 'vue-jest',
12 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
13 | '^.+\\.tsx?$': 'ts-jest'
14 | },
15 | transformIgnorePatterns: [
16 | '/node_modules/'
17 | ],
18 | moduleNameMapper: {
19 | '^@/(.*)$': '/src/$1'
20 | },
21 | snapshotSerializers: [
22 | 'jest-serializer-vue'
23 | ],
24 | testMatch: [
25 | '/(tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx))'
26 | ],
27 | testURL: 'http://localhost/',
28 | watchPlugins: [
29 | 'jest-watch-typeahead/filename',
30 | 'jest-watch-typeahead/testname'
31 | ],
32 | globals: {
33 | 'ts-jest': {
34 | babelConfig: true
35 | }
36 | },
37 | collectCoverage: true,
38 | collectCoverageFrom: [
39 | 'src/**/*.{ts,vue}'
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@roma219/vue-jsonschema-form",
3 | "description": "Input Form generator based on JSON Schema",
4 | "version": "0.1.16",
5 | "private": false,
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/roma219/vue-jsonschema-form.git"
9 | },
10 | "author": "Roman Sabirov roma219@yandex.ru",
11 | "license": "MIT",
12 | "scripts": {
13 | "serve": "vue-cli-service serve",
14 | "build": "rollup -c --environment BUILD:production",
15 | "lint": "vue-cli-service lint",
16 | "test:unit": "vue-cli-service test:unit",
17 | "coveralls": "npm run test:unit && cat ./coverage/lcov.info | coveralls",
18 | "docs:dev": "vuepress dev docs",
19 | "docs:build": "vuepress build docs"
20 | },
21 | "main": "./dist/entry.js",
22 | "files": [
23 | "dist/*",
24 | "src/*"
25 | ],
26 | "dependencies": {
27 | "@babel/preset-env": "^7.6.3",
28 | "nanoclone": "^0.2.1",
29 | "purecss": "^1.0.1"
30 | },
31 | "peerDependencies": {
32 | "vue": "^2.6.10",
33 | "purecss": "^1.0.1"
34 | },
35 | "devDependencies": {
36 | "@mdi/font": "^5.0.45",
37 | "@types/jest": "^26.0.0",
38 | "@types/json-schema": "^7.0.3",
39 | "@vue/babel-preset-app": "^4.0.4",
40 | "@vue/cli-plugin-babel": "^4.0.4",
41 | "@vue/cli-plugin-eslint": "^4.0.4",
42 | "@vue/cli-plugin-typescript": "^4.0.4",
43 | "@vue/cli-plugin-unit-jest": "^4.1.2",
44 | "@vue/cli-service": "^4.0.4",
45 | "@vue/eslint-config-standard": "^4.0.0",
46 | "@vue/eslint-config-typescript": "^4.0.0",
47 | "@vue/test-utils": "1.0.0-beta.31",
48 | "babel-core": "7.0.0-bridge.0",
49 | "babel-eslint": "^10.0.1",
50 | "core-js": "^2.6.10",
51 | "coveralls": "^3.0.6",
52 | "deepmerge": "^4.2.2",
53 | "dotenv": "^8.2.0",
54 | "eslint": "^5.16.0",
55 | "eslint-plugin-vue": "^6.0.1",
56 | "fibers": "^5.0.0",
57 | "jest": "^25.1.0",
58 | "rollup": "^2.13.1",
59 | "rollup-plugin-buble": "^0.19.8",
60 | "rollup-plugin-commonjs": "^10.1.0",
61 | "rollup-plugin-css-only": "^2.0.0",
62 | "rollup-plugin-node-resolve": "^5.2.0",
63 | "rollup-plugin-progress": "^1.1.1",
64 | "rollup-plugin-replace": "^2.2.0",
65 | "rollup-plugin-terser": "^6.1.0",
66 | "rollup-plugin-typescript": "^1.0.1",
67 | "rollup-plugin-visualizer": "^4.0.4",
68 | "rollup-plugin-vue": "^5.0.1",
69 | "sass": "^1.25.0",
70 | "sass-loader": "^7.3.1",
71 | "ts-jest": "^25.0.0",
72 | "ts-node": "^8.4.1",
73 | "typescript": "^3.6.4",
74 | "vue": "^2.6.10",
75 | "vue-class-component": "^7.0.2",
76 | "vue-highlightjs": "^1.3.3",
77 | "vue-property-decorator": "^9.0.0",
78 | "vue-template-compiler": "^2.6.10",
79 | "vuelidate": "^0.7.4",
80 | "vuepress": "^1.2.0",
81 | "vuepress-plugin-typescript": "^0.3.0",
82 | "vuetify": "^2.2.5"
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {}
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/roma219/vue-jsonschema-form/dcc87157b7773fdc9d0ffe975dfc356d462f1463/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | vue-jsonschema-form
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import vue from 'rollup-plugin-vue'
2 | import commonjs from 'rollup-plugin-commonjs'
3 | import buble from 'rollup-plugin-buble'
4 | import resolve from 'rollup-plugin-node-resolve'
5 | import typescript from 'rollup-plugin-typescript'
6 | import { terser } from 'rollup-plugin-terser'
7 | import visualizer from 'rollup-plugin-visualizer'
8 | import progress from 'rollup-plugin-progress'
9 | import replace from 'rollup-plugin-replace'
10 |
11 | export default {
12 | input: 'src/entry.ts',
13 | output: {
14 | format: 'esm',
15 | dir: 'dist',
16 | name: 'index',
17 | sourcemap: true,
18 | globals: {
19 | vue: 'Vue',
20 | vuelidate: 'vuelidate'
21 | }
22 | },
23 | external: [
24 | 'vue',
25 | 'vuelidate',
26 | 'vuelidate/*'
27 | ],
28 | plugins: [
29 | progress(),
30 | typescript(),
31 | replace({
32 | 'process.env.NODE_ENV': JSON.stringify('production'),
33 | 'process.env.BUILD': JSON.stringify('web')
34 | }),
35 | vue(),
36 | buble({
37 | objectAssign: 'Object.assign'
38 | }),
39 | resolve(),
40 | commonjs(),
41 | terser(),
42 | visualizer({ sourcemap: true })
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
{{ formattedValue }}
10 |
11 |
12 |
13 |
90 |
91 |
106 |
--------------------------------------------------------------------------------
/src/JsonSchema/JsonSchema.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
84 |
85 |
92 |
--------------------------------------------------------------------------------
/src/JsonSchema/JsonSchemaArray.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
19 |
20 |
21 |
22 |
23 |
24 |
79 |
80 |
93 |
--------------------------------------------------------------------------------
/src/JsonSchema/JsonSchemaArrayFormWrap.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
24 |
25 |
38 |
--------------------------------------------------------------------------------
/src/JsonSchema/JsonSchemaForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
14 |
15 |
16 |
17 |
18 |
131 |
132 |
137 |
--------------------------------------------------------------------------------
/src/JsonSchema/__tests__/JsonSchema.spec.ts:
--------------------------------------------------------------------------------
1 | import JsonSchema from '../JsonSchema.vue'
2 | import { mount } from '@vue/test-utils'
3 |
4 | const testSchema = {
5 | type: 'object',
6 | properties: {
7 | b: {
8 | type: 'object',
9 | properties: {
10 | c: { type: 'string', minLength: 1 }
11 | }
12 | },
13 | c: { type: 'string', enum: ['1', '2', '3'] },
14 | e: {
15 | type: 'array',
16 | items: {
17 | type: 'object',
18 | properties: {
19 | f: {
20 | type: 'string',
21 | default: 'kek'
22 | }
23 | }
24 | }
25 | }
26 | }
27 | }
28 |
29 | const testUiSchema = {
30 | properties: {
31 | c: {
32 | uiType: 'radio',
33 | order: 2
34 | },
35 | d: {
36 | order: 1
37 | },
38 | e: {
39 | order: 5
40 | }
41 | }
42 | }
43 |
44 | describe('JsonSchema.vue', () => {
45 | it('should be a vue component', () => {
46 | const wrapper = mount(JsonSchema, { propsData: { schema: testSchema, uiSchema: testUiSchema } })
47 | expect(wrapper.isVueInstance()).toBeTruthy()
48 | })
49 |
50 | it('emits updated data on change', () => {
51 | const wrapper = mount(JsonSchema, { propsData: { schema: testSchema, value: { b: {} } } })
52 | const input = wrapper.find('input')
53 | ;(input.element as any).value = 'aaa'
54 | input.trigger('input')
55 | expect(wrapper.emitted().input[0]).toEqual([{ b: { c: 'aaa' }, c: '1' }])
56 | })
57 | it('creates empty object when inner property was changed', () => {
58 | const wrapper = mount(JsonSchema, { propsData: { schema: testSchema, value: { } } })
59 | const input = wrapper.find('input')
60 | ;(input.element as any).value = 'aaa'
61 | input.trigger('input')
62 | expect(wrapper.emitted().input[0]).toEqual([{ c: '1' }])
63 | })
64 | it('correctly handles array element change', () => {
65 | const wrapper = mount(JsonSchema, { propsData: { schema: testSchema, value: { e: [{ f: 'aaa' }, { f: 'bbb' }] } } })
66 | const moveBtn = wrapper.find('button')
67 | moveBtn.trigger('click')
68 | expect(wrapper.emitted().input[0]).toEqual([ { c: '1', e: [{ f: 'aaa' }, { f: 'bbb' }] } ])
69 | const deleteBtn = wrapper.findAll('button').at(1)
70 | deleteBtn.trigger('click')
71 | expect(wrapper.emitted().input[1]).toEqual([ { e: [{ f: 'bbb' }, { f: 'aaa' }] } ])
72 | const addBtn = wrapper.find('.add-new-arr-item')
73 | addBtn.trigger('click')
74 | expect(wrapper.emitted().input[2]).toEqual([ { e: [{ f: 'bbb' }] } ])
75 | })
76 | })
77 |
--------------------------------------------------------------------------------
/src/JsonSchema/index.ts:
--------------------------------------------------------------------------------
1 | import JsonSchema from './JsonSchema.vue'
2 |
3 | export default JsonSchema
4 |
--------------------------------------------------------------------------------
/src/__tests__/App.spec.ts:
--------------------------------------------------------------------------------
1 | import App from '../App.vue'
2 | import { shallowMount } from '@vue/test-utils'
3 |
4 | describe('App.vuenpm', () => {
5 | it('should be vue component', () => {
6 | const wrapper = shallowMount(App)
7 | expect(wrapper.isVueInstance()).toBeTruthy()
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/src/__tests__/entry.spec.ts:
--------------------------------------------------------------------------------
1 | import entry from '../entry'
2 | import { shallowMount } from '@vue/test-utils'
3 |
4 | describe('entry.ts', () => {
5 | it('should be a vue component', () => {
6 | const wrapper = shallowMount(entry, { propsData: { schema: {} } })
7 | expect(wrapper.isVueInstance()).toBeTruthy()
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/src/__tests__/main.spec.ts:
--------------------------------------------------------------------------------
1 | describe('entry.ts', () => {
2 | it('should render app in #app', () => {
3 | document.body.innerHTML = ''
4 |
5 | require('../main')
6 |
7 | const pElement = document.getElementsByClassName('schema')
8 | expect(pElement.length).toBeTruthy()
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/src/components/Checkbox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
--------------------------------------------------------------------------------
/src/components/InputWrapper.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ error }}
6 |
7 |
8 |
9 |
22 |
23 |
72 |
--------------------------------------------------------------------------------
/src/components/Radio.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
17 |
18 |
19 |
20 |
36 |
37 |
42 |
--------------------------------------------------------------------------------
/src/components/Select.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
35 |
36 |
41 |
--------------------------------------------------------------------------------
/src/components/TextInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
44 |
--------------------------------------------------------------------------------
/src/components/__tests__/Checkbox.spec.ts:
--------------------------------------------------------------------------------
1 | import Checkbox from '../Checkbox.vue'
2 | import { mount } from '@vue/test-utils'
3 |
4 | describe('Checkbox component', () => {
5 | it('should be vue component', () => {
6 | const wrapper = mount(Checkbox)
7 | expect(wrapper.isVueInstance()).toBeTruthy()
8 | })
9 |
10 | it('emits event on input', () => {
11 | const wrapper = mount(Checkbox)
12 | ;(wrapper.vm.$el as any).value = true
13 | wrapper.trigger('input')
14 | expect(wrapper.emitted().input[0]).toEqual([false])
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/src/components/__tests__/InputWrapper.spec.ts:
--------------------------------------------------------------------------------
1 | import InputWrapper from '../InputWrapper.vue'
2 | import { mount } from '@vue/test-utils'
3 |
4 | describe('InputWrapper component', () => {
5 | it('should be vue component', () => {
6 | const wrapper = mount(InputWrapper)
7 | expect(wrapper.isVueInstance()).toBeTruthy()
8 | })
9 |
10 | it('should have title when corresponding prop is provided', () => {
11 | const wrapper = mount(InputWrapper, { propsData: { title: '123' } })
12 | expect((wrapper.vm as any).vertical).toBeFalsy()
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/src/components/__tests__/Radio.spec.ts:
--------------------------------------------------------------------------------
1 | import Radio from '../Radio.vue'
2 | import { mount } from '@vue/test-utils'
3 |
4 | describe('Radio component', () => {
5 | it('should be vue component', () => {
6 | const wrapper = mount(Radio)
7 | expect(wrapper.isVueInstance()).toBeTruthy()
8 | })
9 |
10 | it('should emit input event on change', () => {
11 | const wrapper = mount(Radio, { propsData: { options: ['1', '2', '3'], value: '2' } })
12 | var event = new Event('input', {
13 | bubbles: true,
14 | cancelable: true
15 | })
16 | wrapper.vm.$el.getElementsByTagName('input')[0].dispatchEvent(event)
17 | expect(wrapper.emitted().input[0]).toEqual(['1'])
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/src/components/__tests__/Select.spec.ts:
--------------------------------------------------------------------------------
1 | import Select from '../Select.vue'
2 | import { mount } from '@vue/test-utils'
3 |
4 | describe('Select component', () => {
5 | it('should be vue component', () => {
6 | const wrapper = mount(Select)
7 | expect(wrapper.isVueInstance()).toBeTruthy()
8 | })
9 |
10 | it('emits event on input', () => {
11 | const wrapper = mount(Select, { propsData: { options: ['1', '2', '3'], value: '2' } })
12 | ;(wrapper.vm.$el as any).value = '3'
13 | wrapper.trigger('input')
14 | expect(wrapper.emitted().input[0]).toEqual(['3'])
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/src/components/__tests__/TextInput.spec.ts:
--------------------------------------------------------------------------------
1 | import TextInput from '../TextInput.vue'
2 | import { mount } from '@vue/test-utils'
3 |
4 | describe('TextInput component', () => {
5 | it('should be vue component', () => {
6 | const wrapper = mount(TextInput)
7 | expect(wrapper.isVueInstance()).toBeTruthy()
8 | })
9 |
10 | it('should correctly compute its type', () => {
11 | const wrapper = mount(TextInput, { propsData: { type: 'number' } })
12 | expect((wrapper.vm as any).isNumber).toBe(true)
13 | expect((wrapper.vm as any).isFloat).toBe(false)
14 |
15 | wrapper.setProps({ type: 'float' })
16 | expect((wrapper.vm as any).isNumber).toBe(true)
17 | expect((wrapper.vm as any).isFloat).toBe(true)
18 |
19 | wrapper.setProps({ type: 'qwert' })
20 | expect((wrapper.vm as any).isNumber).toBe(false)
21 | expect((wrapper.vm as any).isFloat).toBe(false)
22 | })
23 |
24 | it('emits event on input', () => {
25 | const wrapper = mount(TextInput)
26 | ;(wrapper.vm.$el as any).value = '123'
27 | wrapper.trigger('input')
28 | expect(wrapper.emitted().input[0]).toEqual(['123'])
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/src/components/__tests__/restrict-directive.spec.ts:
--------------------------------------------------------------------------------
1 | import directive from '../restrict-directive'
2 |
3 | describe('Restrict input directive', () => {
4 | it('should have bind property', () => {
5 | expect(typeof directive).toBe('object')
6 | expect(directive.bind).toBeDefined()
7 | })
8 |
9 | it('prevents non-numbers when corresponding modifier is provided', () => {
10 | const mockedPreventDefaultClb = jest.fn()
11 | const input = document.createElement('input')
12 | input.type = 'text'
13 |
14 | directive.bind(input, { value: { isNumber: true }, modifiers: { number: true } })
15 |
16 | let event = new KeyboardEvent('keydown', { keyCode: 8 } as any)
17 | event.preventDefault = mockedPreventDefaultClb
18 | input.dispatchEvent(event)
19 |
20 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(0)
21 |
22 | event = new KeyboardEvent('keydown', { keyCode: 7 } as any)
23 | event.preventDefault = mockedPreventDefaultClb
24 | input.dispatchEvent(event)
25 |
26 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(1)
27 |
28 | event = new KeyboardEvent('keydown', { keyCode: 2 } as any)
29 | event.preventDefault = mockedPreventDefaultClb
30 | input.dispatchEvent(event)
31 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(2)
32 | })
33 |
34 | it('doesnt prevent non-numbers when corresponding modifier is not provided', () => {
35 | const mockedPreventDefaultClb = jest.fn()
36 | const input = document.createElement('input')
37 | input.type = 'text'
38 | directive.bind(input, { value: { isNumber: true }, modifiers: { } })
39 |
40 | let event = new KeyboardEvent('keydown', { keyCode: 50 } as any)
41 | event.preventDefault = mockedPreventDefaultClb
42 | input.dispatchEvent(event)
43 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(1)
44 |
45 | event = new KeyboardEvent('keydown', { keyCode: 65, ctrlKey: true, metaKey: true } as any)
46 | event.preventDefault = mockedPreventDefaultClb
47 | input.dispatchEvent(event)
48 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(1)
49 |
50 | event = new KeyboardEvent('keydown', { keyCode: 2 } as any)
51 | event.preventDefault = mockedPreventDefaultClb
52 | input.dispatchEvent(event)
53 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(2)
54 |
55 | event = new KeyboardEvent('keydown', { keyCode: 65, ctrlKey: true } as any)
56 | event.preventDefault = mockedPreventDefaultClb
57 | input.dispatchEvent(event)
58 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(2)
59 |
60 | event = new KeyboardEvent('keydown', { keyCode: 65, metaKey: true } as any)
61 | event.preventDefault = mockedPreventDefaultClb
62 | input.dispatchEvent(event)
63 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(2)
64 |
65 | event = new KeyboardEvent('keydown', { keyCode: 67, ctrlKey: true } as any)
66 | event.preventDefault = mockedPreventDefaultClb
67 | input.dispatchEvent(event)
68 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(2)
69 |
70 | event = new KeyboardEvent('keydown', { keyCode: 67, metaKey: true } as any)
71 | event.preventDefault = mockedPreventDefaultClb
72 | input.dispatchEvent(event)
73 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(2)
74 |
75 | event = new KeyboardEvent('keydown', { keyCode: 86, ctrlKey: true } as any)
76 | event.preventDefault = mockedPreventDefaultClb
77 | input.dispatchEvent(event)
78 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(2)
79 |
80 | event = new KeyboardEvent('keydown', { keyCode: 86, metaKey: true } as any)
81 | event.preventDefault = mockedPreventDefaultClb
82 | input.dispatchEvent(event)
83 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(2)
84 |
85 | event = new KeyboardEvent('keydown', { keyCode: 88, ctrlKey: true } as any)
86 | event.preventDefault = mockedPreventDefaultClb
87 | input.dispatchEvent(event)
88 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(2)
89 |
90 | event = new KeyboardEvent('keydown', { keyCode: 88, metaKey: true } as any)
91 | event.preventDefault = mockedPreventDefaultClb
92 | input.dispatchEvent(event)
93 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(2)
94 |
95 | event = new KeyboardEvent('keydown', { keyCode: 36 } as any)
96 | event.preventDefault = mockedPreventDefaultClb
97 | input.dispatchEvent(event)
98 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(2)
99 | })
100 |
101 | it('doesnt stop period symbol when corresponding modifier is provided', () => {
102 | const mockedPreventDefaultClb = jest.fn()
103 | let input = document.createElement('input')
104 | input.type = 'text'
105 |
106 | directive.bind(input, { value: { isNumber: true }, modifiers: { decimal: true } })
107 |
108 | let event = new KeyboardEvent('keydown', { keyCode: 110 } as any)
109 | event.preventDefault = mockedPreventDefaultClb
110 | input.dispatchEvent(event)
111 |
112 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(0)
113 |
114 | input = document.createElement('input')
115 | input.type = 'text'
116 | directive.bind(input, { value: { isNumber: true }, modifiers: {} })
117 |
118 | event = new KeyboardEvent('keydown', { keyCode: 110 } as any)
119 | event.preventDefault = mockedPreventDefaultClb
120 | input.dispatchEvent(event)
121 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(1)
122 | })
123 |
124 | it('escapes minus as first symbol when corresponding option is provided', () => {
125 | const mockedPreventDefaultClb = jest.fn()
126 | let input = document.createElement('input')
127 | input.type = 'text'
128 |
129 | directive.bind(input, { value: { isNumber: true }, modifiers: { decimal: true } })
130 |
131 | let event = new KeyboardEvent('keydown', { keyCode: 189 } as any)
132 | event.preventDefault = mockedPreventDefaultClb
133 | input.dispatchEvent(event)
134 |
135 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(1)
136 |
137 | input = document.createElement('input')
138 | input.type = 'text'
139 | input.value = '123'
140 | input.selectionStart = 2
141 | directive.bind(input, { value: { isNumber: true, negativeNumber: true }, modifiers: {} })
142 |
143 | event = new KeyboardEvent('keydown', { keyCode: 189 } as any)
144 | event.preventDefault = mockedPreventDefaultClb
145 | input.dispatchEvent(event)
146 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(2)
147 |
148 | input = document.createElement('input')
149 | input.type = 'text'
150 | directive.bind(input, { value: { isNumber: true, negativeNumber: true }, modifiers: {} })
151 |
152 | event = new KeyboardEvent('keydown', { keyCode: 189 } as any)
153 | event.preventDefault = mockedPreventDefaultClb
154 | input.value = '123'
155 | input.selectionStart = 0
156 | input.dispatchEvent(event)
157 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(2)
158 | })
159 |
160 | it('allows dot when float option is provided', () => {
161 | const mockedPreventDefaultClb = jest.fn()
162 | let input = document.createElement('input')
163 | input.type = 'text'
164 |
165 | directive.bind(input, { value: { isNumber: true, isFloat: true }, modifiers: { } })
166 | input.value = '12323'
167 | let event = new KeyboardEvent('keydown', { keyCode: 190 } as any)
168 | event.preventDefault = mockedPreventDefaultClb
169 | input.dispatchEvent(event)
170 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(0)
171 |
172 | input = document.createElement('input')
173 | input.type = 'text'
174 | directive.bind(input, { value: { isNumber: true, isFloat: true }, modifiers: { } })
175 | event = new KeyboardEvent('keydown', { keyCode: 190 } as any)
176 | event.preventDefault = mockedPreventDefaultClb
177 | input.value = '1234.34'
178 | input.dispatchEvent(event)
179 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(1)
180 | })
181 |
182 | it('allows numbers without shift', () => {
183 | const mockedPreventDefaultClb = jest.fn()
184 | let input = document.createElement('input')
185 | input.type = 'text'
186 |
187 | directive.bind(input, { value: { isNumber: true }, modifiers: { number: true } })
188 | let event = new KeyboardEvent('keydown', { keyCode: 97 } as any)
189 | event.preventDefault = mockedPreventDefaultClb
190 | input.dispatchEvent(event)
191 | expect(mockedPreventDefaultClb.mock.calls.length).toBe(0)
192 | })
193 | })
194 |
--------------------------------------------------------------------------------
/src/components/restrict-directive.ts:
--------------------------------------------------------------------------------
1 | function checkKeyDown (el: HTMLElement, binding: any) {
2 | el.addEventListener('keydown', (e: KeyboardEvent) => {
3 | // delete, backpsace, tab, escape, enter,
4 | let special = [46, 8, 9, 27, 13]
5 |
6 | if (binding.modifiers['decimal']) {
7 | // decimal(numpad), period
8 | special.push(110, 190)
9 | }
10 |
11 | // special from above
12 | if (
13 | special.indexOf(e.keyCode) !== -1 ||
14 | // Ctrl+A
15 | (e.keyCode === 65 && (e.ctrlKey === true || e.metaKey === true)) ||
16 | // // Ctrl+C
17 | (e.keyCode === 67 && (e.ctrlKey === true || e.metaKey === true)) ||
18 | // // Ctrl+V
19 | (e.keyCode === 86 && (e.ctrlKey === true || e.metaKey === true)) ||
20 | // // Ctrl+X
21 | (e.keyCode === 88 && (e.ctrlKey === true || e.metaKey === true)) ||
22 | // // home, end, left, right
23 | (e.keyCode >= 35 && e.keyCode <= 39)
24 | ) {
25 | return // allow
26 | }
27 |
28 | // escape `-` as first symbol
29 | if (
30 | binding.value.negativeNumber === true &&
31 | ((e.target as any).selectionStart === 0 && e.keyCode === 189)) {
32 | return
33 | }
34 |
35 | // escape `float number`
36 | if (!(e.target as any).value.includes('.') && binding.value.isFloat === true && e.keyCode === 190) {
37 | return
38 | }
39 |
40 | if (
41 | binding.modifiers['number'] &&
42 | // number keys without shift
43 | ((!e.shiftKey && (e.keyCode >= 48 && e.keyCode <= 57)) ||
44 | // numpad number keys
45 | (e.keyCode >= 96 && e.keyCode <= 105))
46 | ) {
47 | return // allow
48 | }
49 |
50 | // otherwise stop the keystroke
51 | e.preventDefault()
52 | })
53 | }
54 |
55 | export default {
56 | bind (el: HTMLElement, binding: any) {
57 | if (binding.value.isNumber) {
58 | checkKeyDown(el, binding)
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/entry.ts:
--------------------------------------------------------------------------------
1 | import JsonSchema from '@/JsonSchema/index.ts'
2 |
3 | export default JsonSchema
4 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './App.vue'
3 |
4 | Vue.config.productionTip = false
5 |
6 | new Vue({
7 | render: h => h(App)
8 | }).$mount('#app')
9 |
--------------------------------------------------------------------------------
/src/nanoclone.d.ts:
--------------------------------------------------------------------------------
1 |
2 | declare module 'nanoclone' {
3 | function clone (obj: any) : any
4 | export default clone
5 | }
6 |
--------------------------------------------------------------------------------
/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/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import Vue from 'vue'
3 | export default Vue
4 | }
5 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | type SchemaTypeName = 'string' | 'number' | 'boolean' | 'object' | 'integer' | 'array'
2 |
3 | // Schema types
4 | export interface ISchemaBase {
5 | definitions?: { [key: string]: ISchemaBase }
6 | $ref?: string
7 | type: SchemaTypeName
8 | title?: string
9 | description?: string
10 | format?: string
11 | minLength?: number
12 | maxLength?: number
13 | minimum?: number
14 | maximum?: number
15 | default?: any
16 | properties?: { [key: string]: ISchema }
17 | enum?: Array
18 | items?: ISchema
19 | allOf?: any[]
20 | oneOf?: any[]
21 | if?: any
22 | then?: any
23 | }
24 |
25 | export interface ISchemaObject extends ISchemaBase {
26 | type: 'object'
27 | properties: { [key: string]: ISchema }
28 | }
29 |
30 | export interface ISchemaSelect extends ISchemaBase {
31 | enum: Array
32 | }
33 |
34 | export interface ISchemaArray extends ISchemaBase {
35 | type: 'array'
36 | items: ISchema
37 | }
38 |
39 | export type ISchema = ISchemaBase | ISchemaObject | ISchemaSelect | ISchemaArray
40 |
41 | export type IDefinition = { [key: string] : ISchema }
42 |
43 | export interface IUiSchema {
44 | titles?: Array
45 | order?: number
46 | properties?: { [key: string]: IUiSchema }
47 | disabled?: boolean
48 | }
49 |
50 | // config types
51 | export type ComponentsConfig = Array
52 |
53 | export type WrapperComponentConfig = IWrapperComponent
54 |
55 | export type ErrorMessagesConfig = any
56 |
57 | type PropsFunction = (propName: string, schema: any, uiSchema?: IUiSchema) => IAnyObject
58 |
59 | export interface IComponent {
60 | componentName: string
61 | eventName: string
62 | props?: PropsFunction
63 | }
64 |
65 | export interface IWrapperComponent {
66 | componentName: string,
67 | props?: PropsFunction
68 | }
69 |
70 | export interface IConfigComponent {
71 | matcher?: IAnyObject,
72 | uiSchemaMatcher?: IAnyObject,
73 | contains?: string,
74 | componentName: string,
75 | eventName: string,
76 | props?: PropsFunction
77 | }
78 |
79 | // common types
80 | export interface IAnyObject {
81 | [key:string] : any
82 | }
83 |
--------------------------------------------------------------------------------
/src/utils/__testData__.ts:
--------------------------------------------------------------------------------
1 | export const testSchemaForDefaultData = {
2 | type: 'object',
3 | properties: {
4 | text: { type: 'string' },
5 | number: { $ref: '#/definitions/numberDef', type: 'integer' },
6 | float: { type: 'number' },
7 | checkbox: { type: 'boolean' },
8 | arr: { type: 'array', items: { type: 'string' } },
9 | nested: {
10 | type: 'object',
11 | properties: {
12 | test2: { type: 'string', default: 'lalala' },
13 | nested2: {
14 | type: 'object',
15 | properties: {
16 | arrOfObjects: {
17 | type: 'array',
18 | items: {
19 | $ref: '#/definitions/itemsRef',
20 | __collapsible__: true,
21 | type: 'object',
22 | properties: {
23 | aaa: { type: 'string' },
24 | bbb: { type: 'integer', default: 5 }
25 | }
26 | }
27 | },
28 | zog: { type: 'zog' }
29 | }
30 | }
31 | }
32 | }
33 | }
34 | }
35 |
36 | export const testSchemaWithRefs = {
37 | type: 'object',
38 | definitions: {
39 | numberDef: {
40 | type: 'integer'
41 | },
42 | itemsRef: {
43 | type: 'object',
44 | properties: {
45 | aaa: { type: 'string' },
46 | bbb: { type: 'integer', default: 5 }
47 | }
48 | }
49 | },
50 | properties: {
51 | text: { type: 'string' },
52 | number: { $ref: '#/definitions/numberDef' },
53 | float: { type: 'number' },
54 | checkbox: { type: 'boolean' },
55 | arr: { type: 'array', items: { type: 'string' } },
56 | nested: {
57 | type: 'object',
58 | properties: {
59 | test2: { type: 'string', default: 'lalala' },
60 | nested2: {
61 | type: 'object',
62 | properties: {
63 | arrOfObjects: {
64 | type: 'array',
65 | items: {
66 | $ref: '#/definitions/itemsRef'
67 | }
68 | },
69 | zog: { type: 'zog' }
70 | }
71 | }
72 | }
73 | }
74 | }
75 | }
76 |
77 | export const testSchemaWithConditions = {
78 | type: 'object',
79 | properties: {
80 | text: { type: 'string' },
81 | number: { type: 'integer' },
82 | float: { type: 'number' },
83 | checkbox: { type: 'boolean' },
84 | arr: { type: 'array', items: { type: 'string' } },
85 | nested: {
86 | type: 'object',
87 | properties: {
88 | test2: { type: 'string', default: 'lalala' },
89 | nested2: {
90 | type: 'object',
91 | properties: {
92 | arrOfObjects: {
93 | type: 'array',
94 | items: {
95 | type: 'object',
96 | properties: {
97 | aaa: { type: 'string' },
98 | bbb: { type: 'integer', default: 5 }
99 | }
100 | }
101 | },
102 | zog: { type: 'zog' }
103 | }
104 | }
105 | }
106 | }
107 | },
108 | if: { properties: { checkbox: { const: true } } },
109 | then: { properties: { newField: { type: 'string' } } }
110 | }
111 |
112 | export const processedTestSchemaWithConditions = {
113 | type: 'object',
114 | properties: {
115 | text: { type: 'string' },
116 | number: { type: 'integer' },
117 | float: { type: 'number' },
118 | checkbox: { type: 'boolean' },
119 | arr: { type: 'array', items: { type: 'string' } },
120 | newField: { type: 'string' },
121 | nested: {
122 | type: 'object',
123 | properties: {
124 | test2: { type: 'string', default: 'lalala' },
125 | nested2: {
126 | type: 'object',
127 | properties: {
128 | arrOfObjects: {
129 | type: 'array',
130 | items: {
131 | type: 'object',
132 | properties: {
133 | aaa: { type: 'string' },
134 | bbb: { type: 'integer', default: 5 }
135 | }
136 | }
137 | },
138 | zog: { type: 'zog' }
139 | }
140 | }
141 | }
142 | }
143 | },
144 | if: { properties: { checkbox: { const: true } } },
145 | then: { properties: { newField: { type: 'string' } } }
146 | }
147 |
148 | export const schemaWithValidations = {
149 | type: 'object',
150 | properties: {
151 | text: { type: 'string', minLength: 1 },
152 | number: { $ref: '#/definitions/numberDef', type: 'integer', minimum: 1 },
153 | float: { type: 'number', minimum: 3 },
154 | checkbox: { type: 'boolean' },
155 | arr: { type: 'array', minLength: 1, items: { type: 'string' } },
156 | nested: {
157 | type: 'object',
158 | properties: {
159 | test2: { type: 'string', default: 'lalala', maxLength: 10 },
160 | nested2: {
161 | type: 'object',
162 | properties: {
163 | arrOfObjects: {
164 | type: 'array',
165 | items: {
166 | $ref: '#/definitions/itemsRef',
167 | type: 'object',
168 | properties: {
169 | aaa: { type: 'string', minLength: 2 },
170 | bbb: { type: 'integer', default: 5, maximum: 10 }
171 | }
172 | }
173 | },
174 | zog: { type: 'zog' }
175 | }
176 | }
177 | }
178 | }
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/src/utils/__tests__/defaultComponents.spec.ts:
--------------------------------------------------------------------------------
1 | import { defaultComponents, inputWrapper } from '../defaultComponents'
2 |
3 | describe('defaultComponents', () => {
4 | it('defaultComponents and inputWrapper should be defined', () => {
5 | expect(defaultComponents).toBeDefined()
6 | expect(inputWrapper).toBeDefined()
7 | })
8 |
9 | it('is array of objects', () => {
10 | expect(Array.isArray(defaultComponents)).toBe(true)
11 | expect(defaultComponents.every(component => typeof component === 'object')).toBe(true)
12 | })
13 |
14 | it('each components has matcher (matcher, uiSchemaMatcher, contains) property', () => {
15 | expect(defaultComponents.every(component => component.matcher || component.uiSchemaMatcher || component.contains)).toBe(true)
16 | })
17 |
18 | it('optional props callback should be a function', () => {
19 | expect(defaultComponents.filter(component => component.props).every(component => typeof component.props === 'function')).toBe(true)
20 | })
21 |
22 | it('optional props callback should return an object', () => {
23 | expect(defaultComponents
24 | .filter(component => component.props)
25 | .every(component => component.props && typeof component.props('q', {}) === 'object')
26 | ).toBe(true)
27 | })
28 |
29 | it('each component should have a componentName and eventName properties', () => {
30 | expect(defaultComponents.every(component => component.componentName && component.eventName)).toBe(true)
31 | })
32 |
33 | it('inputWrapper should have componentName and props properties', () => {
34 | expect(inputWrapper.componentName).toBeDefined()
35 | expect(inputWrapper.props).toBeDefined()
36 | expect(typeof inputWrapper.props).toBe('function')
37 | if (inputWrapper.props) {
38 | expect(typeof inputWrapper.props('q', {}, {})).toBe('object')
39 | expect(inputWrapper.props('q', { title: 'kek', type: 'object' }, {})).toEqual({ title: 'kek', vertical: true })
40 | }
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/src/utils/__tests__/errorMessages.spec.ts:
--------------------------------------------------------------------------------
1 | import { errorMessages } from '../errorMessages'
2 |
3 | describe('errorMessages', () => {
4 | it('errorMessages should be defined', () => {
5 | expect(errorMessages).toBeDefined()
6 | })
7 |
8 | it('errorMessage property should be either string or function', () => {
9 | expect(Object.values(errorMessages).every(errorMessage => typeof errorMessage === 'string' || typeof errorMessage === 'function')).toBe(true)
10 | })
11 |
12 | it('errorMessage callback functions should return string value', () => {
13 | expect(
14 | Object.values(errorMessages).filter(errorMessage => typeof errorMessage === 'function').every((errorMessage: any) => typeof errorMessage() === 'string')
15 | ).toBe(true)
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/src/utils/__tests__/generateDefaultValue.spec.ts:
--------------------------------------------------------------------------------
1 | import { generateDefaultValue } from '../generateDefaultValue'
2 |
3 | describe('generateDefaultValue utility function', () => {
4 | it('type=string is TextInput', () => {
5 | const value = generateDefaultValue({
6 | type: 'object',
7 | properties: {
8 | a: { type: 'string' },
9 | b: { type: 'number', default: 123 },
10 | c: { type: 'object', properties: { d: { type: 'string', default: 'a' } } },
11 | e: { type: 'object' }
12 | }
13 | })
14 |
15 | expect(value).toEqual({ b: 123, c: { d: 'a' }, e: {} })
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/src/utils/__tests__/getComponent.spec.ts:
--------------------------------------------------------------------------------
1 | import { getComponent } from '../getComponent'
2 | import { defaultComponents } from '../defaultComponents'
3 | import { ISchema } from '@/types'
4 |
5 | const getComponentByNameAndType = (name: string, type: string = 'string') => {
6 | const components : any = (defaultComponents || []).filter(component => component.componentName === name)
7 | let component = components[0]
8 | if (components.length > 1) {
9 | let component = components.find((item: any) => item.matcher.type === type)
10 | }
11 | const { componentName, eventName, props } = component
12 |
13 | return { componentName, eventName, props }
14 | }
15 |
16 | describe('getComponent utility function - default config', () => {
17 | it('type=boolean is Checkbox', () => {
18 | const component = getComponent({ type: 'boolean' } as ISchema)
19 |
20 | expect(component).toEqual(getComponentByNameAndType('Checkbox'))
21 | })
22 |
23 | it('type=object is JsonSchemaForm', () => {
24 | const component = getComponent({ type: 'object' } as ISchema)
25 |
26 | expect(component).toEqual(getComponentByNameAndType('JsonSchemaForm'))
27 | })
28 |
29 | it('enum is Select', () => {
30 | const component = getComponent({ enum: [1, 2, 3] } as any)
31 |
32 | expect(component).toEqual(getComponentByNameAndType('Select'))
33 | })
34 |
35 | it('array is special inner component', () => {
36 | const component = getComponent({ type: 'array' } as ISchema)
37 |
38 | expect(component).toEqual(getComponentByNameAndType('JsonSchemaArray'))
39 | })
40 |
41 | it('when component is not found, string input is used', () => {
42 | const component = getComponent({ type: 'kekh' } as any)
43 |
44 | expect(component).toEqual({ componentName: 'TextInput', eventName: 'input' })
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/src/utils/__tests__/getErrorText.spec.ts:
--------------------------------------------------------------------------------
1 | import { getErrorText } from '../getErrorText'
2 | import { errorMessages } from '../errorMessages'
3 |
4 | describe('getErrorText utility function', () => {
5 | it('returns correct messages', () => {
6 | expect(getErrorText({ required: false }, { type: 'object' })).toBe(errorMessages.required)
7 | expect(getErrorText({ minLength: false, $params: { minLength: { min: 3 } } }, { type: 'string', minLength: 3 })).toBe(errorMessages.minLength(3))
8 | expect(getErrorText({ maxLength: false, $params: { maxLength: { max: 3 } } }, { type: 'string', maxLength: 3 })).toBe(errorMessages.maxLength(3))
9 | expect(getErrorText({ minValue: false, $params: { minValue: { min: 3 } } }, { type: 'number', minimum: 3 })).toBe(errorMessages.minValue(3))
10 | expect(getErrorText({ maxValue: false, $params: { maxValue: { max: 3 } } }, { type: 'number', maximum: 3 })).toBe(errorMessages.maxValue(3))
11 | expect(getErrorText({ url: false }, { type: 'object' })).toBe(errorMessages.url)
12 | expect(getErrorText({ }, { type: 'object' })).toBe(errorMessages.default)
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/src/utils/__tests__/mergeDeep.spec.ts:
--------------------------------------------------------------------------------
1 | import { mergeDeep } from '../mergeDeep'
2 |
3 | describe('mergeDeep utility function', () => {
4 | it('merges objects correctly', () => {
5 | expect(mergeDeep({ a: 1, c: { d: 1, t: 7 } }, { b: 2, c: { e: 5 } })).toEqual({ a: 1, b: 2, c: { d: 1, e: 5, t: 7 } })
6 | expect(mergeDeep({ a: 1, c: { d: 1 } }, { b: 2, c: 6, f: { g: 9 } })).toEqual({ a: 1, b: 2, c: 6, f: { g: 9 } })
7 | expect(mergeDeep({ a: 1 }, { a: { f: 3 } })).toEqual({ a: 1 })
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/src/utils/__tests__/processConditions.spec.ts:
--------------------------------------------------------------------------------
1 | import { processSchemaConditions, checkRequierment } from '../processConditions'
2 | import { testSchemaWithConditions, processedTestSchemaWithConditions } from '../__testData__'
3 | import { ISchema, ISchemaObject } from '@/types'
4 |
5 | const testData = {
6 | text: '',
7 | number: null,
8 | float: null,
9 | checkbox: true,
10 | arr: [],
11 | nested: {
12 | test2: 'lalala',
13 | nested2: {
14 | arrOfObjects: [{
15 | aaa: '',
16 | bbb: 5
17 | }],
18 | zog: null
19 | }
20 | }
21 | }
22 |
23 | describe('[JSON-SCHEMA] processConditions helper function', () => {
24 | // it('correctly processes conditions data from schema', () => {
25 | // const newSchema = processSchemaConditions(testSchemaWithCondition as ISchemaObject, testData)
26 |
27 | // expect(newSchema).toEqual(processedTestSchemaWithConditions)
28 | // })
29 | })
30 |
31 | describe('[JSON-SCHEMA] checkRequierment function', () => {
32 | it('checks "const" condition', () => {
33 | const testRequirment = { properties: { a: { const: 5 } } }
34 |
35 | expect(checkRequierment(testRequirment, { a: 5 })).toBe(true)
36 | expect(checkRequierment(testRequirment, { a: 123 })).toBe(false)
37 | })
38 | it('checks "minLength" condition', () => {
39 | const testRequirment = { properties: { a: { minLength: 1 } } }
40 |
41 | expect(checkRequierment(testRequirment, { a: '' })).toBe(false)
42 | expect(checkRequierment(testRequirment, { a: 'asd' })).toBe(true)
43 | expect(checkRequierment(testRequirment, { a: [] })).toBe(false)
44 | expect(checkRequierment(testRequirment, { a: [1] })).toBe(true)
45 | })
46 | it('detects "not" condition', () => {
47 | const testRequirment = { not: { properties: { a: { const: 5 } } } }
48 |
49 | expect(checkRequierment(testRequirment, { a: 5 })).toBe(false)
50 | expect(checkRequierment(testRequirment, { a: 123 })).toBe(true)
51 | })
52 |
53 | it('detects "not" + "minLength" condition', () => {
54 | const testRequirment = { not: { properties: { a: { minLength: 5 } } } }
55 |
56 | expect(checkRequierment(testRequirment, { a: 'aaaaaa' })).toBe(false)
57 | expect(checkRequierment(testRequirment, {})).toBe(true)
58 | })
59 |
60 | it('detects invalid condition', () => {
61 | console.warn = jest.fn()
62 |
63 | const testRequirment = { not: { properties: { b: { asdasd: 5 } } } }
64 |
65 | expect(checkRequierment(testRequirment, { a: 5 })).toBe(false)
66 | expect(checkRequierment({ properties: 5 }, { a: 5 })).toBe(false)
67 | expect(console.warn).toHaveBeenCalledWith('[JSON-SCHEMA] Invalid IF condition: ', testRequirment)
68 | })
69 | it('Not validates unsupported condition', () => {
70 | const testRequirment = { not: { properties: { a: { maximum: 5 } } } }
71 |
72 | expect(checkRequierment(testRequirment, { a: 5 })).toBe(false)
73 | })
74 |
75 | it('Handles empty second parameter', () => {
76 | const testRequirment = { not: { properties: { a: { maximum: 5 } } } }
77 |
78 | expect(checkRequierment(testRequirment)).toBe(false)
79 | })
80 | })
81 |
82 | describe('[JSON-SCHEMA] processSchemaConditions function', () => {
83 | it('processes IF condition', () => {
84 | const testSchema = {
85 | type: 'object',
86 | properties: {
87 | a: { type: 'string' }
88 | },
89 | if: { properties: { a: { minLength: 1 } } },
90 | then: { properties: { b: { type: 'integer' } } }
91 | }
92 | const processedContionSchema = {
93 | type: 'object',
94 | properties: {
95 | a: { type: 'string' },
96 | b: { type: 'integer' }
97 | },
98 | if: { properties: { a: { minLength: 1 } } },
99 | then: { properties: { b: { type: 'integer' } } }
100 | }
101 |
102 | expect(processSchemaConditions((testSchema as ISchemaObject), { a: '' })).toEqual(testSchema)
103 | expect(processSchemaConditions((testSchema as ISchemaObject), { a: 'qwe' })).toEqual(processedContionSchema)
104 | })
105 |
106 | it('processes several conditions via "allOf" condition', () => {
107 | const testCondition = {
108 | allOf: [{
109 | if: { properties: { a: { minLength: 1 } } },
110 | then: { properties: { b: { type: 'integer' } } }
111 | }, {
112 | if: { properties: { a: { const: '234' } } },
113 | then: { properties: { c: { type: 'boolean' } } }
114 | }]
115 | }
116 |
117 | const testSchema : ISchemaObject = {
118 | type: 'object',
119 | properties: {
120 | a: { type: 'string' }
121 | },
122 | ...testCondition
123 | }
124 |
125 | expect(processSchemaConditions(testSchema, { a: '' })).toEqual(testSchema)
126 | expect(processSchemaConditions(testSchema, { a: 'a' })).toEqual({
127 | type: 'object',
128 | properties: {
129 | a: { type: 'string' },
130 | b: { type: 'integer' }
131 | },
132 | ...testCondition
133 | })
134 | expect(processSchemaConditions(testSchema, { a: '234' })).toEqual({
135 | type: 'object',
136 | properties: {
137 | a: { type: 'string' },
138 | b: { type: 'integer' },
139 | c: { type: 'boolean' }
140 | },
141 | ...testCondition
142 | })
143 | })
144 |
145 | it('processes if.allOf conditions', () => {
146 | const testCondition = {
147 | if: {
148 | allOf: [{
149 | properties: { a: { minLength: 1 } }
150 | }, {
151 | properties: { a: { const: '234' } }
152 | }]
153 | },
154 | then: { properties: { b: { type: 'integer' } } }
155 | }
156 |
157 | const testSchema : ISchemaObject = {
158 | type: 'object',
159 | properties: {
160 | a: { type: 'string' }
161 | },
162 | ...testCondition
163 | }
164 |
165 | expect(processSchemaConditions(testSchema, { a: '' })).toEqual(testSchema)
166 | expect(processSchemaConditions(testSchema, { a: 'a' })).toEqual(testSchema)
167 | expect(processSchemaConditions(testSchema, { a: '234' })).toEqual({
168 | type: 'object',
169 | properties: {
170 | a: { type: 'string' },
171 | b: { type: 'integer' }
172 | },
173 | ...testCondition
174 | })
175 | })
176 |
177 | it('processes if.oneOf conditions', () => {
178 | const testCondition = {
179 | if: {
180 | oneOf: [{
181 | properties: { a: { const: '777' } }
182 | }, {
183 | properties: { a: { const: '234' } }
184 | }]
185 | },
186 | then: { properties: { b: { type: 'integer' } } }
187 | }
188 |
189 | const getTestSchema = () : ISchema => ({
190 | type: 'object',
191 | properties: {
192 | a: { type: 'string' }
193 | },
194 | ...testCondition
195 | })
196 |
197 | expect(processSchemaConditions(getTestSchema(), { a: '' })).toEqual(getTestSchema())
198 | expect(processSchemaConditions(getTestSchema(), { a: 'a' })).toEqual(getTestSchema())
199 | expect(processSchemaConditions(getTestSchema(), { a: '777' })).toEqual({
200 | type: 'object',
201 | properties: {
202 | a: { type: 'string' },
203 | b: { type: 'integer' }
204 | },
205 | ...testCondition
206 | })
207 | expect(processSchemaConditions(getTestSchema(), { a: '234' })).toEqual({
208 | type: 'object',
209 | properties: {
210 | a: { type: 'string' },
211 | b: { type: 'integer' }
212 | },
213 | ...testCondition
214 | })
215 | })
216 |
217 | it('processes nested conditions', () => {
218 | const testSchema : ISchemaObject = {
219 | type: 'object',
220 | properties: {
221 | a: { type: 'string' },
222 | eee: {
223 | type: 'object',
224 | properties: {
225 | x: { type: 'boolean' }
226 | },
227 | if: { properties: { x: { const: true } } },
228 | then: { properties: { y: { type: 'integer' } } }
229 | },
230 | ddd: {
231 | type: 'object',
232 | properties: {
233 | z: { type: 'number' }
234 | }
235 | }
236 | },
237 | if: { properties: { eee: { properties: { x: { const: true } } } } },
238 | then: { properties: { ddd: { properties: { rr: { type: 'integer' } } } } }
239 | }
240 |
241 | expect(processSchemaConditions(testSchema, { a: '', eee: { x: false } })).toEqual(testSchema)
242 | expect(processSchemaConditions(testSchema, { a: '', eee: { x: true } })).toEqual({
243 | type: 'object',
244 | properties: {
245 | a: { type: 'string' },
246 | eee: {
247 | type: 'object',
248 | properties: {
249 | x: { type: 'boolean' },
250 | y: { type: 'integer' }
251 | },
252 | if: { properties: { x: { const: true } } },
253 | then: { properties: { y: { type: 'integer' } } }
254 | },
255 | ddd: {
256 | type: 'object',
257 | properties: {
258 | z: { type: 'number' },
259 | rr: { type: 'integer' }
260 | }
261 | }
262 | },
263 | if: { properties: { eee: { properties: { x: { const: true } } } } },
264 | then: { properties: { ddd: { properties: { rr: { type: 'integer' } } } } }
265 | })
266 | })
267 | })
268 |
--------------------------------------------------------------------------------
/src/utils/__tests__/processSchema.spec.ts:
--------------------------------------------------------------------------------
1 | import { processSchema } from '../processSchema'
2 |
3 | describe('processSchema utility function', () => {
4 | it('processes schema', () => {
5 | const value = processSchema({
6 | type: 'object',
7 | properties: {
8 | a: { type: 'null' },
9 | b: { type: 'number', default: 123 },
10 | c: { type: 'object', properties: { d: { type: 'string', default: 'a' } } },
11 | e: { type: 'object' },
12 | f: { type: 'array', items: { type: 'string' } }
13 | },
14 | kek: 5
15 | } as any)
16 |
17 | expect(value).toEqual({
18 | type: 'object',
19 | properties: {
20 | a: { type: 'string' },
21 | b: { type: 'number', default: 123 },
22 | c: { type: 'object', properties: { d: { type: 'string', default: 'a' } } },
23 | e: { type: 'object' },
24 | f: { type: 'array', items: { type: 'string' } }
25 | },
26 | kek: 5
27 | })
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/src/utils/__tests__/setValidators.spec.ts:
--------------------------------------------------------------------------------
1 | import { setValidators } from '../setValidators'
2 | import { schemaWithValidations } from '../__testData__'
3 | import required from 'vuelidate/lib/validators/required'
4 | import minLength from 'vuelidate/lib/validators/minLength'
5 | import maxLength from 'vuelidate/lib/validators/maxLength'
6 | import minValue from 'vuelidate/lib/validators/minValue'
7 | import maxValue from 'vuelidate/lib/validators/maxValue'
8 | import { ISchema } from '@/types'
9 |
10 | const validators = jest.genMockFromModule('vuelidate/lib/validators')
11 |
12 | describe('[JSON-SCHEMA] utils.setValidators helper function', () => {
13 | it('correctly generates validators', () => {
14 | const validations = setValidators(schemaWithValidations as ISchema)
15 |
16 | expect(JSON.stringify(validations)).toMatch(JSON.stringify({
17 | text: {
18 | required,
19 | minLength: minLength(1)
20 | },
21 | number: {
22 | required,
23 | minValue: minValue(1)
24 | },
25 | float: {
26 | required,
27 | minValue: minValue(3)
28 | },
29 | checkbox: {},
30 | arr: {
31 | required,
32 | minLength: minLength(1)
33 | },
34 | nested: {
35 | test2: { maxLength: maxLength(10) },
36 | nested2: {
37 | arrOfObjects: {
38 | $each: {
39 | aaa: {
40 | minLength: minLength(2),
41 | required
42 | },
43 | bbb: {
44 | maxValue: maxValue(10)
45 | }
46 | }
47 | },
48 | zog: {}
49 | }
50 | }
51 | }))
52 | })
53 | it('handles empty schema', () => {
54 | const validations = setValidators({ type: 'object' } as ISchema)
55 | expect(validations).toEqual({})
56 | })
57 | })
58 |
--------------------------------------------------------------------------------
/src/utils/__tests__/unrefSchema.spec.ts:
--------------------------------------------------------------------------------
1 | import { unrefSchema } from '../unrefSchema'
2 |
3 | describe('unrefSchema utility function', () => {
4 | it('handles empty definitions', () => {
5 | const value = unrefSchema({
6 | type: 'object',
7 | properties: {
8 | a: { type: 'string' }
9 | }
10 | } as any)
11 |
12 | expect(value).toEqual({
13 | type: 'object',
14 | properties: {
15 | a: { type: 'string' }
16 | }
17 | })
18 | })
19 |
20 | it('uses definitions', () => {
21 | console.warn = jest.fn()
22 |
23 | const value = unrefSchema({
24 | definitions: {
25 | a: { type: 'string', title: 'myref' },
26 | itemSchema: { type: 'number' }
27 | },
28 | type: 'object',
29 | properties: {
30 | a: { $ref: '#/definitions/a' },
31 | b: { type: 'array', items: { $ref: '#/definitions/itemSchema' } },
32 | b2: { type: 'array', items: { type: 'string' } },
33 | c: { type: 'object', properties: { d: { $ref: '#/definitions/a' } } },
34 | x: { $ref: '#/definitions/x' }
35 | },
36 | if: { properties: { a: { const: 5 } } },
37 | then: { properties: { z: { $ref: '#/definitions/a' } } },
38 | allOf: [{
39 | if: { properties: { a: { const: 5 } } },
40 | then: { properties: { z: { $ref: '#/definitions/a' } } }
41 | }, {
42 | if: { properties: { a: { const: 5 } } },
43 | then: { properties: { z: { minLength: 7 } } }
44 | }, {
45 | if: { properties: { a: { const: 5 } } },
46 | then: { z: { minLength: 7 } }
47 | }]
48 | } as any)
49 |
50 | expect(value).toEqual({
51 | type: 'object',
52 | properties: {
53 | a: { $ref: '#/definitions/a', type: 'string', title: 'myref' },
54 | b: { type: 'array', items: { $ref: '#/definitions/itemSchema', type: 'number' } },
55 | b2: { type: 'array', items: { type: 'string' } },
56 | c: { type: 'object', properties: { d: { $ref: '#/definitions/a', type: 'string', title: 'myref' } } },
57 | x: { $ref: '#/definitions/x', type: 'string' }
58 | },
59 | if: { properties: { a: { const: 5 } } },
60 | then: { properties: { z: { $ref: '#/definitions/a', type: 'string', title: 'myref' } } },
61 | allOf: [{
62 | if: { properties: { a: { const: 5 } } },
63 | then: { properties: { z: { $ref: '#/definitions/a', type: 'string', title: 'myref' } } }
64 | }, {
65 | if: { properties: { a: { const: 5 } } },
66 | then: { properties: { z: { minLength: 7 } } }
67 | }, {
68 | if: { properties: { a: { const: 5 } } },
69 | then: { z: { minLength: 7 } }
70 | }]
71 | })
72 | expect(console.warn).toHaveBeenCalledWith('[JSON-SCHEMA] Ref="#/definitions/x" not found in definitions')
73 | })
74 | })
75 |
--------------------------------------------------------------------------------
/src/utils/defaultComponents.ts:
--------------------------------------------------------------------------------
1 | import { ComponentsConfig, WrapperComponentConfig } from '@/types'
2 |
3 | export const defaultComponents : ComponentsConfig = [{
4 | matcher: {
5 | type: 'array'
6 | },
7 | componentName: 'JsonSchemaArray',
8 | eventName: 'input'
9 | }, {
10 | uiSchemaMatcher: {
11 | uiType: 'radio'
12 | },
13 | componentName: 'Radio',
14 | eventName: 'input',
15 | props: (propName, schema) => ({ options: schema.enum })
16 | }, {
17 | matcher: {
18 | type: 'object'
19 | },
20 | componentName: 'JsonSchemaForm',
21 | eventName: 'input'
22 | }, {
23 | contains: 'enum',
24 | componentName: 'Select',
25 | eventName: 'input',
26 | props: (propName, schema, uiSchema) => ({ options: schema.enum })
27 | }, {
28 | matcher: {
29 | type: 'number'
30 | },
31 | componentName: 'TextInput',
32 | props: () => ({ type: 'number' }),
33 | eventName: 'input'
34 | }, {
35 | matcher: {
36 | type: 'string'
37 | },
38 | componentName: 'TextInput',
39 | eventName: 'input'
40 | }, {
41 | matcher: {
42 | type: 'boolean'
43 | },
44 | componentName: 'Checkbox',
45 | eventName: 'input'
46 | }]
47 |
48 | export const inputWrapper : WrapperComponentConfig = {
49 | componentName: 'InputWrapper',
50 | props: (propName, schema, uiSchema) => ({
51 | title: schema.title || schema.title === '' ? schema.title : propName,
52 | disabled: uiSchema?.disabled,
53 | vertical: schema.type === 'object' || schema.type === 'array'
54 | })
55 | }
56 |
--------------------------------------------------------------------------------
/src/utils/errorMessages.ts:
--------------------------------------------------------------------------------
1 | import { ErrorMessagesConfig } from '@/types'
2 |
3 | export const errorMessages : ErrorMessagesConfig = {
4 | minLength: (value: number) => `Minimal length: ${value}`,
5 | maxLength: (value: number) => `Maximum length: ${value}`,
6 | minValue: (value: number) => `Minimum value: ${value}`,
7 | maxValue: (value: number) => `Maximum value: ${value}`,
8 | required: 'Field is required',
9 | default: 'Invalid Value'
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/generateDefaultValue.ts:
--------------------------------------------------------------------------------
1 | import { ISchema } from '@/types'
2 | import { JSONSchema7 } from 'json-schema'
3 |
4 | export const generateDefaultValue = (schema: ISchema) : { [key: string]: any } => {
5 | let defaultValue = schema.default
6 |
7 | if (schema.type === 'object') {
8 | defaultValue = {}
9 | Object.entries(schema.properties || {}).forEach(([propName, value]) => {
10 | if (value.default || value.type === 'object') defaultValue[propName] = generateDefaultValue(value)
11 | })
12 | }
13 |
14 | return defaultValue
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/getComponent.ts:
--------------------------------------------------------------------------------
1 | import { IComponent, ISchema, IUiSchema, IAnyObject, ComponentsConfig, IConfigComponent } from '@/types'
2 | import { defaultComponents } from './defaultComponents'
3 |
4 | const isMatch = (target : IAnyObject, source : IAnyObject) : boolean => {
5 | return Object.entries(source).every(([propName, value]) => value === target[propName])
6 | }
7 |
8 | function areEqual (obj1: IAnyObject, obj2: IAnyObject) : boolean {
9 | return Object.keys(obj1).every(key => {
10 | return obj2.hasOwnProperty(key) ? typeof obj1[key] === 'object' ? areEqual(obj1[key], obj2[key]) : obj1[key] === obj2[key] : false
11 | })
12 | }
13 |
14 | const canReplaceComponents = (componentA: IConfigComponent, componentB: IConfigComponent) => {
15 | return (componentA.matcher && areEqual(componentA.matcher, componentB.matcher || {})) ||
16 | (componentA.uiSchemaMatcher && areEqual(componentA.uiSchemaMatcher, componentB.uiSchemaMatcher || {})) ||
17 | (componentA?.contains === componentB.contains)
18 | }
19 |
20 | export const mergeComponents = (customComponents: ComponentsConfig) => {
21 | const components = [ ...defaultComponents ]
22 | const filteredCustomComponents = customComponents.filter(component => {
23 | if (component.matcher || component.uiSchemaMatcher || component.contains) {
24 | const index = defaultComponents.findIndex(defaultComponent => canReplaceComponents(defaultComponent, component))
25 | if (index > -1) {
26 | components[index] = component
27 | return false
28 | }
29 | return true
30 | }
31 | })
32 | return [...filteredCustomComponents, ...components]
33 | }
34 |
35 | export const getComponent = (schema: ISchema, components: ComponentsConfig = defaultComponents, uiSchema?: IUiSchema) : IComponent => {
36 | const component = components.find(component => {
37 | if (component.matcher) return isMatch(schema, component.matcher)
38 | if (component.uiSchemaMatcher) return isMatch(uiSchema || {}, component.uiSchemaMatcher)
39 | if (component.contains) return schema.hasOwnProperty(component.contains)
40 | })
41 |
42 | const { componentName = 'TextInput', eventName = 'input', props = undefined } = (component || {})
43 |
44 | return { componentName, eventName, props }
45 | }
46 |
--------------------------------------------------------------------------------
/src/utils/getErrorText.ts:
--------------------------------------------------------------------------------
1 | import { errorMessages } from './errorMessages'
2 | import { ISchema } from '@/types'
3 |
4 | export const getErrorText = (error: any, schema: ISchema) : string => {
5 | if (error.required === false) return errorMessages.required
6 |
7 | if (error.minLength === false) return errorMessages.minLength(schema.minLength)
8 |
9 | if (error.maxLength === false) return errorMessages.maxLength(schema.maxLength)
10 |
11 | if (error.minValue === false) return errorMessages.minValue(schema.minimum)
12 |
13 | if (error.maxValue === false) return errorMessages.maxValue(schema.maximum)
14 |
15 | if (error.url === false) return errorMessages.url
16 |
17 | return errorMessages.default
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/mergeDeep.ts:
--------------------------------------------------------------------------------
1 | import { IAnyObject } from '@/types'
2 |
3 | function isObject (item: any) {
4 | return (item && typeof item === 'object' && !Array.isArray(item))
5 | }
6 |
7 | export function mergeDeep (target: IAnyObject, ...sources: IAnyObject[]) : IAnyObject {
8 | if (!sources.length) return target
9 | const source = sources.shift()
10 |
11 | if (isObject(target) && isObject(source)) {
12 | for (const key in source) {
13 | if (isObject(source[key])) {
14 | if (!target[key]) Object.assign(target, { [key]: {} })
15 | mergeDeep(target[key], source[key])
16 | } else {
17 | Object.assign(target, { [key]: source[key] })
18 | }
19 | }
20 | }
21 |
22 | return mergeDeep(target, ...sources)
23 | }
24 |
--------------------------------------------------------------------------------
/src/utils/processConditions.ts:
--------------------------------------------------------------------------------
1 | import { ISchema, IAnyObject } from '@/types'
2 | import { mergeDeep } from './mergeDeep'
3 |
4 | type AllOfCondition = {
5 | allOf: Array
6 | then: IAnyObject
7 | }
8 |
9 | type OneOfCondition = {
10 | oneOf: Array
11 | then: IAnyObject
12 | }
13 |
14 | type SimpleCondition = {
15 | if: any,
16 | then: IAnyObject
17 | }
18 |
19 | type ICondition = SimpleCondition | AllOfCondition | OneOfCondition
20 |
21 | type ConstRequirment = { const: any }
22 |
23 | type MinLengthRequirment = { minLength: number }
24 |
25 | type IRequirment = ConstRequirment | MinLengthRequirment
26 |
27 | type IRequirmentTree = any
28 |
29 | type BaseRequirment = {
30 | not: IRequirmentTree
31 | } | IRequirmentTree
32 |
33 | export const processSchemaConditions = (schema: ISchema, data: IAnyObject) => {
34 | const newSchema = { ...schema }
35 |
36 | if (schema.allOf) {
37 | schema.allOf.forEach(condition => processCondition(condition, data, newSchema))
38 | }
39 |
40 | if (schema.if) processCondition(schema as SimpleCondition, data, newSchema)
41 |
42 | Object.keys(schema.properties || {}).forEach(property => {
43 | if (schema.properties && schema.properties[property].type === 'object') {
44 | if (newSchema.properties && data && data[property]) newSchema.properties[property] = processSchemaConditions(schema.properties[property], data[property])
45 | }
46 | })
47 |
48 | return newSchema
49 | }
50 |
51 | const processCondition = (condition: SimpleCondition, data: IAnyObject, newSchema: ISchema) => {
52 | // several requirments
53 | if (condition.if.allOf) {
54 | const requirments = condition.if.allOf
55 |
56 | if (requirments.every((requirment: BaseRequirment) => checkRequierment(requirment, data))) {
57 | applyCondition(newSchema, condition.then)
58 | }
59 |
60 | return
61 | }
62 |
63 | // needs one requirment
64 | if (condition.if.oneOf) {
65 | const requirments = condition.if.oneOf
66 |
67 | if (requirments.some((requirment: BaseRequirment) => checkRequierment(requirment, data))) {
68 | applyCondition(newSchema, condition.then)
69 | }
70 |
71 | return
72 | }
73 |
74 | // straighforward one requirment
75 | if (checkRequierment(condition.if, data)) applyCondition(newSchema, condition.then)
76 | }
77 |
78 | export const checkRequierment = (requirment: BaseRequirment, data: IAnyObject = {}) => {
79 | // TODO: different types of requirments
80 | // TODO: allOf, oneOf
81 | // now only minLength and const are supported
82 |
83 | let currentPath = requirment.not ? requirment.not.properties : requirment.properties
84 | let currentValue = data
85 | let currentParamName = Object.keys(currentPath)[0]
86 |
87 | while (currentParamName !== 'const' && currentParamName !== 'minLength') {
88 | if (typeof currentPath !== 'object') {
89 | console.warn('[JSON-SCHEMA] Invalid IF condition: ', requirment)
90 |
91 | return false
92 | }
93 |
94 | currentPath = currentPath[currentParamName]
95 |
96 | if (currentParamName !== 'properties') {
97 | if (typeof currentValue !== 'object') {
98 | console.warn('[JSON-SCHEMA] Invalid IF condition: ', requirment)
99 |
100 | return false
101 | }
102 |
103 | currentValue = currentValue[currentParamName]
104 | }
105 |
106 | currentParamName = Object.keys(currentPath)[0]
107 | }
108 |
109 | if (currentParamName === 'const') {
110 | return requirment.not ? currentPath.const !== currentValue : currentPath.const === currentValue
111 | }
112 |
113 | if (currentParamName === 'minLength') {
114 | return requirment.not
115 | ? currentPath.minLength > (currentValue || '').length
116 | : currentPath.minLength <= (currentValue || '').length
117 | }
118 | }
119 |
120 | const applyCondition = (schema: ISchema, then: IAnyObject) => {
121 | return mergeDeep(schema, then)
122 | }
123 |
--------------------------------------------------------------------------------
/src/utils/processSchema.ts:
--------------------------------------------------------------------------------
1 | import { JSONSchema7 } from 'json-schema'
2 | import { ISchema } from '@/types'
3 |
4 | export const processSchema = (schema: JSONSchema7) : ISchema => {
5 | const strippedSchema : any = {
6 | ...schema,
7 | type: (schema.type !== 'null' && ((typeof schema.type) !== 'object')) ? schema.type : 'string'
8 | }
9 |
10 | if (schema.properties) {
11 | strippedSchema.properties = {}
12 |
13 | Object.entries(schema.properties).forEach(([propName, propSchema]) => {
14 | strippedSchema.properties[propName] = processSchema(propSchema as JSONSchema7)
15 | })
16 | }
17 |
18 | if (schema.items) strippedSchema.items = processSchema(schema.items as JSONSchema7)
19 |
20 | return strippedSchema
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/setValidators.ts:
--------------------------------------------------------------------------------
1 | import required from 'vuelidate/lib/validators/required'
2 | import minLength from 'vuelidate/lib/validators/minLength'
3 | import maxLength from 'vuelidate/lib/validators/maxLength'
4 | import minValue from 'vuelidate/lib/validators/minValue'
5 | import maxValue from 'vuelidate/lib/validators/maxValue'
6 | import { ISchema } from '@/types'
7 |
8 | export const setValidators = (schema: ISchema) : any => {
9 | const validations : any = {}
10 |
11 | if (schema.properties) {
12 | Object.keys(schema.properties).forEach((property) => {
13 | const propertyObject : any = (schema.properties as any)[property]
14 |
15 | validations[property] = {}
16 |
17 | // requireds
18 | if ((propertyObject.type === 'string' && propertyObject.minLength) ||
19 | (propertyObject.type === 'number' && propertyObject.minimum) ||
20 | (propertyObject.type === 'integer' && propertyObject.minimum) ||
21 | (propertyObject.type === 'array' && propertyObject.minLength)) {
22 | validations[property].required = required
23 | }
24 |
25 | if (propertyObject.minLength) validations[property].minLength = minLength(propertyObject.minLength)
26 |
27 | if (propertyObject.maxLength) validations[property].maxLength = maxLength(propertyObject.maxLength)
28 |
29 | if (propertyObject.minimum) validations[property].minValue = minValue(propertyObject.minimum)
30 |
31 | if (propertyObject.maximum) validations[property].maxValue = maxValue(propertyObject.maximum)
32 |
33 | // set nested validators
34 | if (propertyObject.type === 'object') validations[property] = setValidators(propertyObject)
35 |
36 | // set item validators for array of objects
37 | if (propertyObject.type === 'array' && propertyObject?.items?.type === 'object') {
38 | validations[property] = {
39 | ...validations[property],
40 | $each: setValidators(propertyObject.items)
41 | }
42 | }
43 | })
44 | }
45 |
46 | return validations
47 | }
48 |
--------------------------------------------------------------------------------
/src/utils/unrefSchema.ts:
--------------------------------------------------------------------------------
1 | import clone from 'nanoclone'
2 | import { ISchema, ISchemaObject } from '@/types'
3 |
4 | export const unrefSchema = (schema: ISchemaObject) => {
5 | function getSchemaByRef (ref: string) : ISchema {
6 | const refName = ref.substring(ref.lastIndexOf('/') + 1)
7 |
8 | if (!definitionNames.includes(refName)) {
9 | console.warn(`[JSON-SCHEMA] Ref="${ref}" not found in definitions`)
10 | return { type: 'string' } // definition not found - treat it as a string
11 | }
12 |
13 | return clone(definitions[refName])
14 | }
15 |
16 | function checkSchemaForRefs (schema: ISchemaObject) : ISchemaObject {
17 | const newSchema = { ...schema }
18 |
19 | // unref properties
20 | if (newSchema.properties) {
21 | Object.keys(newSchema.properties).forEach(propertyName => {
22 | if (newSchema.properties[propertyName].$ref) {
23 | const definedSchema = getSchemaByRef(newSchema.properties[propertyName].$ref as string)
24 |
25 | newSchema.properties[propertyName] = {
26 | ...newSchema.properties[propertyName],
27 | ...definedSchema
28 | }
29 | }
30 |
31 | if (newSchema.properties[propertyName].type === 'array' &&
32 | newSchema.properties[propertyName].items
33 | ) {
34 | // if (!newSchema.properties[propertyName].items) return
35 | const definedSchema = newSchema.properties[propertyName] && newSchema.properties[propertyName].items && (newSchema.properties[propertyName].items as any).$ref
36 | ? getSchemaByRef((newSchema.properties[propertyName].items as any).$ref as string)
37 | : newSchema.properties[propertyName].items
38 |
39 | ;(newSchema.properties[propertyName].items as any) = {
40 | ...newSchema.properties[propertyName].items,
41 | ...definedSchema
42 | }
43 |
44 | newSchema.properties[propertyName].items = checkSchemaForRefs(newSchema.properties[propertyName].items as ISchemaObject)
45 | }
46 |
47 | if (newSchema.properties[propertyName].type === 'object') {
48 | newSchema.properties[propertyName] = checkSchemaForRefs(newSchema.properties[propertyName] as ISchemaObject)
49 | }
50 | })
51 | }
52 |
53 | // unref conditions
54 | (newSchema.allOf || []).forEach(condition => {
55 | if (condition.then && condition.then.properties) {
56 | condition.then = checkSchemaForRefs(condition.then)
57 | }
58 | })
59 |
60 | if (newSchema.then && newSchema.then.properties) {
61 | newSchema.then = checkSchemaForRefs(newSchema.then)
62 | }
63 |
64 | return newSchema
65 | }
66 |
67 | const definitions : any = schema.definitions || {}
68 | const definitionNames = Object.keys(definitions)
69 | const dereferencedSchema = checkSchemaForRefs(schema)
70 |
71 | delete dereferencedSchema.definitions
72 |
73 | return dereferencedSchema
74 | }
75 |
--------------------------------------------------------------------------------
/src/vuelidate.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | declare module 'vuelidate' {
3 |
4 | import { default as _Vue } from 'vue'
5 |
6 | /**
7 | * @module augmentation to ComponentOptions defined by Vue.js
8 | */
9 | module 'vue/types/options' {
10 | interface ComponentOptions {
11 | validations?: ValidationRuleset<{}>;
12 | }
13 | }
14 |
15 | module 'vue/types/vue' {
16 | interface Vue {
17 | $v: Vuelidate;
18 | }
19 | }
20 |
21 | /**
22 | * Represents an instance of validator class at runtime
23 | */
24 | interface IValidator {
25 | /**
26 | * Indicates the state of validation for given model. becomes true when any of it's child validators specified in options returns a falsy value. In case of validation groups, all grouped validators are considered.
27 | */
28 | readonly $invalid: boolean;
29 | /**
30 | * A flag representing if the field under validation was touched by the user at least once. Usually it is used to decide if the message is supposed to be displayed to the end user. Flag is managed manually. You have to use $touch and $reset methods to manipulate it. The $dirty flag is considered true if given model was $touched or all of it's children are $dirty.
31 | */
32 | $dirty: boolean;
33 | /**
34 | * Convenience flag to easily decide if a message should be displayed. It is a shorthand to $invalid && $dirty.
35 | */
36 | readonly $error: boolean;
37 | /**
38 | * Indicates if any child async validator is currently pending. Always false if all validators are synchronous.
39 | */
40 | $pending: boolean;
41 |
42 | /**
43 | * Sets the $dirty flag of the model and all its children to true recursively.
44 | */
45 | $touch(): void;
46 | /**
47 | * Sets the $dirty flag of the model and all its children to false recursively.
48 | */
49 | $reset(): void;
50 | }
51 |
52 | /**
53 | * Builtin validators
54 | */
55 | interface IDefaultValidators {
56 | /**
57 | * Accepts only alphabet characters.
58 | */
59 | alpha?: boolean;
60 | /**
61 | * Accepts only alphanumerics.
62 | */
63 | alphaNum?: boolean;
64 | /**
65 | * Checks if a number is in specified bounds. Min and max are both inclusive.
66 | */
67 | between?: boolean;
68 | /**
69 | * Accepts valid email addresses. Keep in mind you still have to carefully verify it on your server, as it is impossible to tell if the address is real without sending verification email.
70 | */
71 | email?: boolean;
72 | /**
73 | * Requires the input to have a maximum specified length, inclusive. Works with arrays.
74 | */
75 | maxLength?: boolean;
76 | /**
77 | * Requires the input to have a minimum specified length, inclusive. Works with arrays.
78 | */
79 | minLength?: boolean;
80 | /**
81 | * Requires non-empty data. Checks for empty arrays and strings containing only whitespaces.
82 | */
83 | required?: boolean;
84 | /**
85 | * Checks for equality with a given property. Locator might be either a sibling property name or a function, that will get your component as this and nested model which sibling properties under second parameter.
86 | */
87 | sameAs?: boolean;
88 | /**
89 | * Passes when at least one of provided validators passes.
90 | */
91 | or?: boolean;
92 | /**
93 | * Passes when all of provided validators passes.
94 | */
95 | and?: boolean;
96 | }
97 |
98 | type EachByKey = {
99 | [K in keyof T]: Validator
100 | }
101 |
102 | /**
103 | * Holds all validation models of collection validator. Always preserves the keys of original model, so it can be safely referenced in the v-for loop iterating over your data using the same index.
104 | */
105 | type Each =
106 | & { [key: number]: EachByKey }
107 | & { $trackBy: string | Function }
108 | & IValidator
109 |
110 | global {
111 | interface Array {
112 | /**
113 | * Holds all validation models of collection validator. Always preserves the keys of original model, so it can be safely referenced in the v-for loop iterating over your data using the same index.
114 | */
115 | $each: Each & Vuelidate;
116 | }
117 | }
118 |
119 | /**
120 | * Represents an instance of validator class at runtime
121 | */
122 | type Validator = IValidator & IDefaultValidators & Each;
123 |
124 | interface IPredicate {
125 | (value: any, parentVm?: IValidationRule): boolean | Promise
126 | }
127 |
128 | interface IPredicateGenerator {
129 | (...args: any[]): IPredicate
130 | }
131 |
132 | interface IValidationRule {
133 | [key: string]: ValidationPredicate | IValidationRule | IValidationRule[];
134 | }
135 |
136 | export type ValidationPredicate = IPredicateGenerator | IPredicate;
137 |
138 | /**
139 | * Represents mixin data exposed by Vuelidate instance
140 | */
141 | export type Vuelidate = {
142 | [K in keyof T]?: Vuelidate & Validator;
143 | };
144 |
145 | /**
146 | * Represents component options used by Vuelidate
147 | */
148 | export type ValidationRuleset = {
149 | [K in keyof T]?: ValidationPredicate | IValidationRule | IValidationRule[] | string[];
150 | }
151 |
152 | /**
153 | * Represents Vuelidate mixin data extending a Vue component instance. Have your Vue component options implement this
154 | * @param {Type} T - The interface or type being used to store model data requiring validation
155 | *
156 | * @example
157 | * export class Foo implements IVuelidate {
158 | * data() {
159 | * return { bar: { length: 0 } };
160 | * }
161 | * validations: {
162 | * bar: {
163 | * length: {
164 | * between: between(1,5)
165 | * }
166 | * }
167 | * }
168 | * $v: Vuelidate;
169 | * }
170 | */
171 | export interface IVuelidate {
172 | $v: Vuelidate;
173 | }
174 |
175 | /**
176 | * Mixin object for supplying directly to components
177 | */
178 | export const validationMixin: {
179 | beforeCreate(): void;
180 | }
181 |
182 | /**
183 | * Vuelidate function that creates a validator directly, given a model, and a set of rules
184 | */
185 | export const validateModel: {
186 | (model: T, validations: ValidationRuleset): IVuelidate;
187 | }
188 |
189 | export const withParams: (params: any, callback: any) => boolean
190 |
191 | /**
192 | * Vue plugin object
193 | */
194 | export function Validation(Vue: typeof _Vue): void;
195 |
196 | export default Validation
197 | }
198 |
199 | declare module 'vuelidate/lib/validators' {
200 |
201 | import { ValidationPredicate } from 'vuelidate'
202 |
203 | /**
204 | * Accepts only alphabet characters.
205 | */
206 | function alpha(value: any): boolean;
207 | /**
208 | * Accepts only alphanumerics.
209 | */
210 | function alphaNum(value: any): boolean;
211 | /**
212 | * Checks if a number is in specified bounds. Min and max are both inclusive.
213 | */
214 | function between(min: number, max: number): (value: any) => boolean;
215 | /**
216 | * Accepts valid email addresses. Keep in mind you still have to carefully verify it on your server, as it is impossible to tell if the address is real without sending verification email.
217 | */
218 | function email(value: any): boolean;
219 | /**
220 | * Requires the input to have a maximum specified length, inclusive. Works with arrays.
221 | */
222 | function maxLength(max: number): (value: any) => boolean;
223 | /**
224 | * Requires the input to have a minimum specified length, inclusive. Works with arrays.
225 | */
226 | function minLength(min: number): (value: any) => boolean;
227 | /**
228 | * Requires the input to have a maximum specified length, inclusive. Works with arrays.
229 | */
230 | function maxValue(max: number): (value: any) => boolean;
231 | /**
232 | * Requires the input to have a minimum specified length, inclusive. Works with arrays.
233 | */
234 | function minValue(min: number): (value: any) => boolean;
235 | /**
236 | * Requires non-empty data. Checks for empty arrays and strings containing only whitespaces.
237 | */
238 | function required(value: any): boolean;
239 | /**
240 | * Checks for equality with a given property. Locator might be either a sibling property name or a function, that will get your component as this and nested model which sibling properties under second parameter.
241 | */
242 | function sameAs(locator: string): (value: any, vm?: any) => boolean;
243 | /**
244 | * Passes when at least one of provided validators passes.
245 | */
246 | function or(...validators: ValidationPredicate[]): () => boolean;
247 | /**
248 | * Passes when all of provided validators passes.
249 | */
250 | function and(...validators: ValidationPredicate[]): () => boolean;
251 | }
252 |
253 | declare module 'vuelidate/lib/validators/required'
254 | declare module 'vuelidate/lib/validators/minLength'
255 | declare module 'vuelidate/lib/validators/maxLength'
256 | declare module 'vuelidate/lib/validators/minValue'
257 | declare module 'vuelidate/lib/validators/maxValue'
258 |
--------------------------------------------------------------------------------
/tests/unit/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | jest: true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2019",
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": [
15 | "webpack-env",
16 | "jest"
17 | ],
18 | "paths": {
19 | "@/*": [
20 | "src/*"
21 | ]
22 | },
23 | "lib": [
24 | "esnext",
25 | "dom",
26 | "dom.iterable",
27 | "scripthost"
28 | ]
29 | },
30 | "include": [
31 | "src/**/*.ts",
32 | "src/**/*.tsx",
33 | "src/**/*.vue",
34 | "tests/**/*.ts",
35 | "tests/**/*.tsx"
36 | ],
37 | "exclude": [
38 | "node_modules"
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | // module.exports = {
2 | // publicPath: 'docs/.vuepress/dist/'
3 | // }
4 |
--------------------------------------------------------------------------------