├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── book.json ├── docs ├── README.md ├── SUMMARY.md ├── api │ ├── components.md │ ├── options.md │ ├── options │ │ └── ajv.md │ ├── schema.md │ ├── ui-schema.md │ ├── ui-schema │ │ ├── field.md │ │ └── field │ │ │ ├── children.md │ │ │ ├── component.md │ │ │ ├── display-options.md │ │ │ ├── dynamic-options.md │ │ │ ├── error-handler.md │ │ │ ├── error-options.md │ │ │ ├── event-prop.md │ │ │ ├── field-options.md │ │ │ ├── field-options │ │ │ ├── attrs.md │ │ │ ├── class.md │ │ │ ├── dom-props.md │ │ │ ├── key.md │ │ │ ├── native-on.md │ │ │ ├── on.md │ │ │ ├── props.md │ │ │ ├── slot.md │ │ │ └── style.md │ │ │ ├── internal-model.md │ │ │ ├── value-model.md │ │ │ └── value-prop.md │ └── vue-form-json-schema.md ├── getting-started.md └── installation.md ├── examples ├── example-1 │ ├── example-one.js │ └── index.html ├── example-2 │ ├── example-two.js │ └── index.html ├── example-3 │ ├── example-three.js │ ├── index.html │ └── style.css ├── example-4 │ ├── example-four.js │ └── index.html ├── example-5 │ ├── example-five.js │ └── index.html ├── example-6 │ ├── example-six.js │ ├── index.html │ └── style.css ├── example-7 │ ├── example-seven.js │ ├── index.html │ └── style.css ├── example-8 │ ├── example-eight.js │ ├── index.html │ ├── style.css │ └── use-form-fields.js └── helpers │ ├── pretty-print.css │ └── pretty-print.js ├── package-lock.json ├── package.json ├── src ├── constants │ └── index.js ├── index.js ├── plugin.js ├── vfjs-field-component │ └── index.js ├── vfjs-field-mixin │ ├── computed.js │ ├── index.js │ ├── methods │ │ ├── getters.js │ │ ├── helpers.js │ │ ├── index.js │ │ └── setters.js │ └── props.js ├── vfjs-global-component │ └── index.js └── vfjs-global-mixin │ ├── computed.js │ ├── data.js │ ├── index.js │ ├── methods │ ├── index.js │ ├── vfjs-bus │ │ ├── actions.js │ │ └── index.js │ ├── vfjs-helpers │ │ └── index.js │ ├── vfjs-lifecycle │ │ └── index.js │ ├── vfjs-model │ │ ├── getters.js │ │ ├── index.js │ │ └── setters.js │ ├── vfjs-schema │ │ ├── getters.js │ │ ├── index.js │ │ └── setters.js │ ├── vfjs-state │ │ ├── getters.js │ │ ├── index.js │ │ └── setters.js │ ├── vfjs-ui │ │ ├── getters.js │ │ ├── index.js │ │ └── setters.js │ └── vfjs-validation │ │ ├── getters.js │ │ ├── index.js │ │ └── setters.js │ ├── props.js │ └── watch.js ├── webpack.config.common.js ├── webpack.config.dev.js └── webpack.config.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["lodash"], 3 | "presets": [ 4 | "@babel/env" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "airbnb-base", 4 | "parser": "babel-eslint", 5 | "env": { 6 | "browser": true 7 | }, 8 | "rules": { 9 | "max-len": [ 10 | "error", 11 | { 12 | "code": 80, 13 | "ignoreComments": true 14 | } 15 | ], 16 | "arrow-parens": ["error", "always"], 17 | "no-unused-vars": [ 18 | "error", 19 | { 20 | "vars": "all", 21 | "args": "none" 22 | } 23 | ], 24 | "no-plusplus": [ 25 | "error", 26 | { 27 | "allowForLoopAfterthoughts": true 28 | } 29 | ], 30 | "operator-linebreak": [ 31 | "error", 32 | "after", 33 | { 34 | "overrides": { 35 | "?": "before", 36 | ":": "before" 37 | } 38 | } 39 | ], 40 | "implicit-arrow-linebreak": 0, 41 | "function-paren-newline": 0 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Gitbook installation folder 3 | _book 4 | # Prewritten commit message 5 | COMMIT_MSG 6 | # Built files 7 | dist 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "arrowParens": "always", 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Form JSON Schema 2 | 3 | > A [JSON schema](https://json-schema.org) based form generator, bring your components! 4 | 5 | ##### Use any Vue component or HTML element! 6 | 7 | There are *no prebuilt components* for you to puzzle your form together with. Instead you can use any component or element which emits an event, custom or native. 8 | 9 | > Note that essentially all Vue components that uses `v-model` emits an `input` (or similar) event. [See Vue's guide for more info](https://vuejs.org/v2/guide/components.html#Form-Input-Components-using-Custom-Events) 10 | 11 | ## Installation 12 | 13 | ### Upgrading from v1? Check out the [v2 release notes](https://github.com/jarvelov/vue-form-json-schema/releases/tag/v2.0.0) to see if and how migration affects you. 14 | 15 | Install from npm 16 | 17 | `npm install vue-form-json-schema` 18 | 19 | Import to your app 20 | 21 | ```js 22 | import Vue from 'vue'; 23 | import VueFormJsonSchema from 'vue-form-json-schema'; 24 | 25 | Vue.component('vue-form-json-schema', VueFormJsonSchema); 26 | ``` 27 | 28 | > Note if you're not using Webpack / Rollup and want to use the ESM version you need to import VueFormJsonSchema like this: 29 | 30 | ```js 31 | import VueFormJsonSchema from 'vue-form-json-schema/dist/vue-form-json-schema.esm.js'; 32 | ``` 33 | 34 | Check out the demos or see a minimal example in the [usage instructions](#usage) to get started. 35 | 36 | ### UMD 37 | 38 | > If you're using the UMD version you can find more examples in the `examples` folder of the [github repo](https://github.com/jarvelov/vue-form-json-schema/tree/master/examples) and the [UMD demo below](#umd-demos). 39 | 40 | If you want to use `vue-form-json-schema` directly in a browser you can do so by using the UMD version. The UMD version autoinstalls the `vue-form-json-schema` component if Vue is found on the window. The entire module is also available on `window.VueFormJsonSchema` where the named exports such as for example `vfjsFieldMixin` can be accessed. 41 | 42 | #### Hosted by unkpg 43 | 44 | `` 45 | 46 | You can substite `vue-form-json-schema@latest` to a fixed version, such as `vue-form-json-schema@2.3.0` 47 | 48 | #### Installed from npm 49 | 50 | `` 51 | 52 | ## Demo 53 | 54 | > Note that all demos use Bootstrap styling, but no styling is included in this package and it is up to you what styles should be used. 55 | 56 | ### [Minimal demo](https://py6611pr9m.codesandbox.io) 57 | 58 | The least amount of configuration to render an `input` element. 59 | 60 | ### [Nested UI demo](https://882w4v374l.codesandbox.io) 61 | 62 | Using Bootstrap classes to show how layout can be different for devices with different screen sizes. 63 | In this example two input fields will be wrapped inside a div with `col-12 col-sm-6` classes. 64 | Try resizing your browser window too see it in action. 65 | 66 | ### [Conditional visibility and Animation demo](https://k0q8wk946o.codesandbox.io/) 67 | 68 | Sometimes a field should only be shown if a condition is met. Uses `` to provide animation. 69 | 70 | ### [Vue components demo](https://z549j1vxx.codesandbox.io) 71 | 72 | See how to use your own or third party Vue components in `vue-form-json-schema`. 73 | 74 | ### [Vue async loading of form](https://2p51q8q14y.codesandbox.io) 75 | 76 | Loading the form from a backend? Check out this example. 77 | 78 | ### [Registration form with validation](https://4rykx7jj19.codesandbox.io) 79 | 80 | A more complete example with validation and error messages 81 | 82 | ### [Registration form example with nested properties](https://nxn8y.codesandbox.io/) 83 | 84 | The registration form above where the form model keys are nested under another key 85 | 86 | ### UMD demos 87 | 88 | All the examples above are replicated using the UMD version in the `examples` folder of this repo. 89 | 90 | [Online version of the first demo](https://jsfiddle.net/jarvelov/ewg6dfqL/) 91 | 92 | ## Features 93 | 94 | * Supports any HTML element or Vue component 95 | * Small (`32K` uncompressed, `6.5K` gzipped) 96 | * Standardized [JSON schema](json-schema.org) for annotation and validation (by [Ajv](https://github.com/epoberezkin/ajv)) 97 | * Layout is independent from data structure 98 | 99 | ## Documentation 100 | 101 | [Gitbook](https://jarvelov.gitbook.io/vue-form-json-schema/) 102 | 103 | ## Usage 104 | 105 | ### Basic example with one field 106 | 107 | >For using the UMD version, check out the `examples` folder where all the demos above are replicated using the UMD version 108 | 109 | [See demo](https://codesandbox.io/s/py6611pr9m) 110 | 111 | ```js 112 | 120 | 121 | 153 | ``` 154 | 155 | ### Dependencies 156 | 157 | #### Ajv 158 | For form validation using [JSON Schema](http://json-schema.org/) and internal validation 159 | 160 | #### Lodash 161 | `get`, `set` and `merge` are used throughout the package. 162 | Bundle size is very important though and is always considered and so we heavily strip down lodash to only include the absolute necessities 163 | 164 | #### Vue 165 | 166 | Tested with v2.5.9 but will probably work on any version >= v2.4.0 167 | 168 | ### TODO 169 | 170 | * Write tests 171 | * ~~Add i18n support~~ 172 | * Added in 1.15.2 with `options.ajv.locale` setting 173 | * ~~Write this README~~ 174 | * Use Ajv internally to validate: 175 | * `vfs-global` prop `ui-schema` 176 | * `vfs-component` prop `ui-schema` 177 | * ~~Write docs~~ 178 | * ~~Publish with Gitbook~~ 179 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "vue-form-json-schema", 3 | "root": "./docs", 4 | "gitbook": ">3.0.0", 5 | "plugins": ["edit-link", "github"], 6 | "pluginsConfig": { 7 | "edit-link": { 8 | "base": "https://github.com/jarvelov/vue-form-json-schema/edit/master/docs", 9 | "label": "Edit This Page" 10 | }, 11 | "github": { 12 | "url": "https://github.com/jarvelov/vue-form-json-schema/" 13 | } 14 | }, 15 | "links": { 16 | "sharing": { 17 | "facebook": false, 18 | "twitter": false 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Vue Form JSON Schema 2 | 3 | > A [JSON schema](https://json-schema.org) based form generator, bring your components! 4 | 5 | ##### Use any Vue component or HTML element! 6 | 7 | There are *no prebuilt components* for you to puzzle your form together with. Instead you can use any component or element which emits an event, custom or native. 8 | 9 | > Note that essentially all Vue components that uses `v-model` emits an `input` (or similar) event. [See Vue's guide for more info](https://vuejs.org/v2/guide/components.html#Form-Input-Components-using-Custom-Events) 10 | 11 | ## Installation 12 | 13 | ### Upgrading from v1? Check out the [v2 release notes](https://github.com/jarvelov/vue-form-json-schema/releases/tag/v2.0.0) to see if and how migration affects you. 14 | 15 | Install from npm 16 | 17 | `npm install vue-form-json-schema` 18 | 19 | Import to your app 20 | 21 | ```js 22 | import Vue from 'vue'; 23 | import VueFormJsonSchema from 'vue-form-json-schema'; 24 | 25 | Vue.component('vue-form-json-schema', VueFormJsonSchema); 26 | ``` 27 | 28 | Check out the demos or see a minimal example in the [usage instructions](#usage) to get started. 29 | 30 | ### UMD 31 | 32 | > If you're using the UMD version you can find more examples in the `examples` folder of the [github repo](https://github.com/jarvelov/vue-form-json-schema/tree/master/examples) and the [UMD demo below](#umd-demos). 33 | 34 | If you want to use `vue-form-json-schema` directly in a browser you can do so by using the UMD version. The UMD version autoinstalls the `vue-form-json-schema` component if Vue is found on the window. The entire module is also available on `window.VueFormJsonSchema` where the named exports such as for example `vfjsFieldMixin` can be accessed. 35 | 36 | #### Hosted by unkpg 37 | 38 | `` 39 | 40 | You can substite `vue-form-json-schema@latest` to a fixed version, such as `vue-form-json-schema@2.3.0` 41 | 42 | #### Installed from npm 43 | 44 | `` 45 | 46 | ## Demo 47 | 48 | > Note that all demos use Bootstrap styling, but no styling is included in this package and it is up to you what styles should be used. 49 | 50 | ### [Minimal demo](https://py6611pr9m.codesandbox.io) 51 | 52 | The least amount of configuration to render an `input` element. 53 | 54 | ### [Nested UI demo](https://882w4v374l.codesandbox.io) 55 | 56 | Using Bootstrap classes to show how layout can be different for devices with different screen sizes. 57 | In this example two input fields will be wrapped inside a div with `col-12 col-sm-6` classes. 58 | Try resizing your browser window too see it in action. 59 | 60 | ### [Conditional visibility and Animation demo](https://k0q8wk946o.codesandbox.io/) 61 | 62 | Sometimes a field should only be shown if a condition is met. Uses `` to provide animation. 63 | 64 | ### [Vue components demo](https://z549j1vxx.codesandbox.io) 65 | 66 | See how to use your own or third party Vue components in `vue-form-json-schema`. 67 | 68 | ### [Vue async loading of form](https://2p51q8q14y.codesandbox.io) 69 | 70 | Loading the form from a backend? Check out this example. 71 | 72 | ### [Registration form with validation](https://4rykx7jj19.codesandbox.io) 73 | 74 | A more complete example with validation and error messages 75 | 76 | ### UMD demos 77 | 78 | All the examples above are replicated using the UMD version in the `examples` folder of this repo. 79 | 80 | [Online version of the first demo](https://jsfiddle.net/jarvelov/ewg6dfqL/) 81 | 82 | ## Features 83 | 84 | * Supports any HTML element or Vue component 85 | * Small (`32K` uncompressed, `6.5K` gzipped) 86 | * Standardized [JSON schema](json-schema.org) for annotation and validation (by [Ajv](https://github.com/epoberezkin/ajv)) 87 | * Layout is independent from data structure 88 | 89 | ## Documentation 90 | 91 | [Gitbook](https://jarvelov.gitbook.io/vue-form-json-schema/) 92 | 93 | ## Usage 94 | 95 | ### Basic example with one field 96 | 97 | >For using the UMD version, check out the `examples` folder where all the demos above are replicated using the UMD version 98 | 99 | [See demo](https://codesandbox.io/s/py6611pr9m) 100 | 101 | ```js 102 | 110 | 111 | 143 | ``` 144 | 145 | ### Dependencies 146 | 147 | #### Ajv 148 | For form validation using [JSON Schema](http://json-schema.org/) and internal validation 149 | 150 | #### Lodash 151 | `get`, `set` and `merge` are used throughout the package. 152 | Bundle size is very important though and is always considered and so we heavily strip down lodash to only include the absolute necessities 153 | 154 | #### Vue 155 | 156 | Tested with v2.5.9 but will probably work on any version >= v2.4.0 157 | 158 | ### TODO 159 | 160 | * Write tests 161 | * ~~Add i18n support~~ 162 | * Added in 1.15.2 with `options.ajv.locale` setting 163 | * ~~Write this README~~ 164 | * Use Ajv internally to validate: 165 | * `vfs-global` prop `ui-schema` 166 | * `vfs-component` prop `ui-schema` 167 | * ~~Write docs~~ 168 | * ~~Publish with Gitbook~~ 169 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | * [Installation](installation.md) 2 | * [Getting started](getting-started.md) 3 | * API 4 | * [vue-form-json-schema](api/vue-form-json-schema.md) 5 | * [components](api/components.md) 6 | * [options](api/options.md) 7 | * [ajv](api/options/ajv.md) 8 | * [schema](api/schema.md) 9 | * [uiSchema](api/ui-schema.md) 10 | * [field](api/ui-schema/field.md) 11 | * [component](api/ui-schema/field/component.md) 12 | * [children](api/ui-schema/field/children.md) 13 | * [displayOptions](api/ui-schema/field/display-options.md) 14 | * [dynamicOptions](api/ui-schema/field/dynamic-options.md) 15 | * [errorHandler](api/ui-schema/field/error-handler.md) 16 | * [errorOptions](api/ui-schema/field/error-options.md) 17 | * [eventProp](api/ui-schema/field/event-prop.md) 18 | * [fieldOptions](api/ui-schema/field/field-options.md) 19 | * [attrs](api/ui-schema/field/field-options/attrs.md) 20 | * [class](api/ui-schema/field/field-options/class.md) 21 | * [domProps](api/ui-schema/field/field-options/dom-props.md) 22 | * [key](api/ui-schema/field/field-options/key.md) 23 | * [nativeOn](api/ui-schema/field/field-options/native-on.md) 24 | * [on](api/ui-schema/field/field-options/on.md) 25 | * [props](api/ui-schema/field/field-options/props.md) 26 | * [slot](api/ui-schema/field/field-options/slot.md) 27 | * [style](api/ui-schema/field/field-options/style.md) 28 | * [internalModel](api/ui-schema/field/internal-model.md) 29 | * [valueModel](api/ui-schema/field/value-model.md) 30 | * [valueProp](api/ui-schema/field/value-prop.md) 31 | -------------------------------------------------------------------------------- /docs/api/components.md: -------------------------------------------------------------------------------- 1 | # components 2 | 3 | With Vue one can use both locally and globally registered components. If not all the components you want to use are globally registered you can use the `components` property to pass in additional components to `vue-form-json-schema`. 4 | 5 | ## Provide locally registered components 6 | 7 | In this example we re-use the locally registered components and pass them along to `vue-form-json-schema` 8 | 9 | ```html 10 | 18 | 19 | 48 | ``` 49 | 50 | ## Use components directly without registering them 51 | 52 | In this example we pass the components along to `vue-form-json-schema` without registering them first 53 | 54 | ```html 55 | 63 | 64 | 93 | ``` 94 | -------------------------------------------------------------------------------- /docs/api/options.md: -------------------------------------------------------------------------------- 1 | # Options 2 | 3 | | Property | Value | Description | Default | 4 | | -------------------------- | ------- | --------------------------------------------------------------- | -------------------------------- | 5 | | allowInvalidModel | Boolean | If `false` and validation fails the model will not be updated | true | 6 | | validate | Boolean | Enable/disable validation | true | 7 | | validateOnLoad | Boolean | Perform a validation after the component has loaded (`created`) | true | 8 | | showValidationErrors | Boolean | Show validation errors | false | 9 | | castToSchemaType | Boolean | Cast values to the type specified for the model in the schema. | false | 10 | | valueProp | String | Which prop will get passed the model | 'value' | 11 | | [ajv](options/ajv.md) | Object | Options for [Ajv](https://github.com/epoberezkin/ajv) | [See ajv](options/ajv.md) | 12 | -------------------------------------------------------------------------------- /docs/api/options/ajv.md: -------------------------------------------------------------------------------- 1 | # ajv 2 | 3 | Options to configure `Ajv`. A list of all [supported options can be found here](https://ajv.js.org/options.html) 4 | 5 | ## Default value 6 | 7 | ```js 8 | { 9 | keywords: {}, 10 | plugins: [], 11 | locale: null, 12 | options: { 13 | allErrors: true; 14 | } 15 | } 16 | ``` 17 | 18 | > Ajv option `allErrors` is always set to true, there is no way to override this as it is used internally for validation 19 | 20 | ## Examples 21 | 22 | ### Enable $data references 23 | 24 | See [Ajv's documentation about $data references](https://ajv.js.org/guide/combining-schemas.html#data-reference) for more details. 25 | `$data` in this example can be substituted for any other [supported Ajv option](https://ajv.js.org/options.html) 26 | 27 | ```js 28 | data() { 29 | return { 30 | options: { 31 | ajv: { 32 | options: { 33 | $data: true 34 | } 35 | } 36 | }, 37 | model: { 38 | ... 39 | }, 40 | jsonSchema: { 41 | ... 42 | }, 43 | uiSchema: [ 44 | ... 45 | ] 46 | } 47 | } 48 | ``` 49 | 50 | ```html 51 | 59 | ``` 60 | 61 | ### Localize error messages 62 | 63 | This example uses [ajv-i18n](https://github.com/epoberezkin/ajv-i18n). 64 | Any errors will be translated using the locale function provided before the fields gets access to them. 65 | 66 | ```js 67 | // Import swedish localization 68 | const sv = require('ajv-i18n/localize/sv'); 69 | 70 | export default { 71 | data() { 72 | return { 73 | options: { 74 | ajv: { 75 | locale: sv 76 | } 77 | }, 78 | model: { 79 | ... 80 | }, 81 | jsonSchema: { 82 | ... 83 | }, 84 | uiSchema: [ 85 | ... 86 | ] 87 | } 88 | } 89 | } 90 | ``` 91 | 92 | ```html 93 | 101 | ``` 102 | 103 | ### Add Ajv keywords 104 | 105 | In this example we will add a custom keyword which checks that the value of the input is not present in the `blacklist` array 106 | 107 | ```js 108 | // These values are forbidden 109 | const blacklist = ['forbidden', 'values']; 110 | 111 | export default { 112 | data() { 113 | return { 114 | options: { 115 | ajv: { 116 | keywords: { 117 | notBlackListed: { 118 | // Needs to be set to true for the validate function to be able to add custom errors 119 | errors: true, 120 | validate: (schema, value) => { 121 | // This is what Ajv wants, and it's terrible. 122 | // There is no way to add custom error messages without 123 | // modifying the validate function's `errors` property 124 | const self = this.options.ajv.keywords.notBlackListed.validate; 125 | 126 | // Create an empty array to hold any errors 127 | self.errors = []; 128 | 129 | if (this.blacklist.indexOf(value) >= 0) { 130 | // This value is blacklisted 131 | self.errors.push({ 132 | message: 'Value is blacklisted!', 133 | keyword: 'uniqueLabel', 134 | params: { 135 | keyword: 'uniqueLabel', 136 | }, 137 | }); 138 | } 139 | 140 | // Ajv needs a boolean value returned to determine if the validation was a success 141 | // If true is returned, no error is generated, even if the errors array is populated 142 | return self.errors.length === 0; 143 | }, 144 | }, 145 | }, 146 | }, 147 | }, 148 | model: {}, 149 | jsonSchema: { 150 | type: 'object', 151 | properties: { 152 | myValue: { 153 | type: 'string', 154 | notBlackListed: true, 155 | }, 156 | }, 157 | }, 158 | uiSchema: [ 159 | { 160 | component: 'input', 161 | model: 'myValue', 162 | fieldOptions: { 163 | on: ['change'], 164 | }, 165 | }, 166 | // This component will output all the errors for myValue model 167 | { 168 | component: 'div', 169 | errorHandler: true, 170 | model: 'myValue', 171 | }, 172 | ], 173 | }; 174 | }, 175 | }; 176 | ``` 177 | 178 | ```html 179 | 187 | ``` 188 | 189 | ### Extend Ajv instance 190 | 191 | Note that this is neither tested nor supported but in theory this would give extra Ajv features such as [ajv-async](https://github.com/epoberezkin/ajv-async) and [ajv-merge-patch](https://github.com/epoberezkin/ajv-merge-patch) access to the internal `Ajv` instance running inside `vue-form-json-schema`. 192 | 193 | By adding the required plugin to the `plugins` section in the ajv options it is possible to load and apply the required plugin to the ajv instance that is used by `vue-form-json-schema`. This comes in handy when custom error messages must be added with [ajv-errors](https://github.com/epoberezkin/ajv-errors). 194 | 195 | ```js 196 | // Import ajv-errors plugin 197 | const ajvErrors = require('ajv-errors'); 198 | 199 | export default { 200 | data() { 201 | return { 202 | options: { 203 | ajv: { 204 | plugins: { 205 | ajvErrors 206 | } 207 | } 208 | }, 209 | model: { 210 | ... 211 | }, 212 | jsonSchema: { 213 | ... 214 | }, 215 | uiSchema: [ 216 | ... 217 | ] 218 | } 219 | } 220 | } 221 | ``` 222 | 223 | ```html 224 | 232 | ``` 233 | -------------------------------------------------------------------------------- /docs/api/schema.md: -------------------------------------------------------------------------------- 1 | # schema 2 | 3 | Any valid [JSON Schema](http://json-schema.org). 4 | 5 | Good resources if you are new to JSON Schema is: 6 | 7 | * [Understanding JSON schema](https://spacetelescope.github.io/understanding-json-schema/) 8 | * [Online JSON schema validator](http://jsonschemavalidator.net) 9 | * [Ajv documentation (internal JSON schema validation engine)](https://epoberezkin.github.io/ajv/) 10 | * [Ajv github](https://github.com/epoberezkin/ajv) 11 | -------------------------------------------------------------------------------- /docs/api/ui-schema.md: -------------------------------------------------------------------------------- 1 | # uiSchema 2 | 3 | An array of [fields](ui-schema/field.md). 4 | 5 | ## Example 6 | 7 | ```js 8 | data() { 9 | return { 10 | uiSchema: [ 11 | { 12 | component: 'div', 13 | fieldOptions: { 14 | class: ['form-group'] 15 | }, 16 | children: [ 17 | { 18 | component: 'input', 19 | model: 'firstName', 20 | fieldOptions: { 21 | on: ['input'] 22 | } 23 | } 24 | ] 25 | } 26 | ] 27 | } 28 | } 29 | ``` 30 | 31 | The example above will do essentially the same thing as the following Vue template: 32 | 33 | ```html 34 |
35 | 36 |
37 | ``` 38 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field.md: -------------------------------------------------------------------------------- 1 | A `field` is an `object` and must have a [component](field/component.md) property. 2 | 3 | ## Type 4 | 5 | `Object` 6 | 7 | ## Properties 8 | 9 | * [component](field/component.md) \* 10 | * [children](field/children.md) 11 | * [displayOptions](field/display-options.md) 12 | * [dynamicOptions](field/dynamic-options.md) 13 | * [errorHandler](field/error-handler.md) 14 | * [errorOptions](field/error-options.md) 15 | * [eventProp](field/event-prop.md) 16 | * [fieldOptions](field/field-options.md) 17 | * [valueProp](field/value-prop.md) 18 | 19 | \* required 20 | 21 | ## Example 22 | 23 | ```js 24 | data() { 25 | return { 26 | uiSchema: [{ 27 | // component is required 28 | component: 'div' 29 | // other optional properties 30 | fieldOptions: { 31 | domProps: { 32 | innerHTML: 'This is the first component!' 33 | } 34 | } 35 | }] 36 | } 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/children.md: -------------------------------------------------------------------------------- 1 | # children 2 | 3 | An array of [fields](../field.md). The rendered children (vnodes) will be passed in the default slot to the `component`, unless a [slot](field-options/slot.md) is set. 4 | 5 | ```js 6 | data() { 7 | return { 8 | uiSchema: [{ 9 | component: 'div', 10 | children: [ 11 | { 12 | component: 'div', 13 | fieldOptions: { 14 | domProps: { 15 | innerHTML: 'The first second div' 16 | } 17 | } 18 | }, 19 | { 20 | component: 'div', 21 | fieldOptions: { 22 | domProps: { 23 | innerHTML: 'The first second div' 24 | } 25 | } 26 | } 27 | ] 28 | }] 29 | } 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/component.md: -------------------------------------------------------------------------------- 1 | # component 2 | 3 | The `component` property can refer to any [Vue component](https://vuejs.org/v2/guide/components.html) or [HTML element tag name](https://developer.mozilla.org/en-US/docs/Web/HTML/Element). 4 | 5 | ```js 6 | // Option 1 - HTML element tag 7 | data() { 8 | return { 9 | uiSchema: [{ 10 | component: 'div' 11 | }] 12 | } 13 | } 14 | ``` 15 | 16 | ```js 17 | // Option 2 - Globally registered component 18 | data() { 19 | return { 20 | uiSchema: [{ 21 | component: 'my-custom-component' 22 | }] 23 | } 24 | } 25 | ``` 26 | 27 | ```js 28 | // Option 3 - component object 29 | import MyCustomComponent from './my-custom-component' 30 | 31 | ... 32 | 33 | data() { 34 | return { 35 | uiSchema: [{ 36 | component: MyCustomComponent 37 | }] 38 | } 39 | } 40 | ``` 41 | 42 | ```js 43 | // Option 4 - Component from components prop 44 | import MyCustomComponent from './my-custom-component' 45 | 46 | ... 47 | 48 | data() { 49 | return { 50 | components: { 51 | MyCustomComponent 52 | }, 53 | uiSchema: [{ 54 | component: 'MyCustomComponent' 55 | }], 56 | model: { ... }, 57 | schema: { ... } 58 | } 59 | } 60 | 61 | .... 62 | 63 | 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/display-options.md: -------------------------------------------------------------------------------- 1 | # displayOptions 2 | 3 | > Tip! This option plays nicely with `transition` and `transition-group`! See example at the bottom. 4 | 5 | Sometimes a field is only relevant if some condition is met. The `displayOptions` property uses JSON Schema to evaluate if a field should be visible or not. If there are no errors then the field is shown. 6 | 7 | 8 | There are 2 options: Full and Single. 9 | 10 | ## Full 11 | 12 | The full model uses the entire form model as data. 13 | 14 | ```js 15 | // Option 1 - full model 16 | data() { 17 | return { 18 | uiSchema: [{ 19 | component: 'div', 20 | children: [{ 21 | component: 'div', 22 | displayOptions: { 23 | schema: { 24 | type: object, 25 | properties: { 26 | firstName: { 27 | type: 'string', 28 | minLength: 3 29 | } 30 | }, 31 | required: ['firstName'] 32 | } 33 | } 34 | }] 35 | }] 36 | } 37 | } 38 | ``` 39 | 40 | The above is essentially doing the following validation with [Ajv](https://github.com/epoberezkin/ajv) 41 | 42 | ```js 43 | const schema = { 44 | type: object, 45 | properties: { 46 | firstName: { 47 | type: 'string', 48 | minLength: 3 49 | } 50 | }, 51 | required: ['firstName'] 52 | }; 53 | 54 | const data = { 55 | // Entire form model 56 | firstName, 57 | lastName, 58 | address, 59 | ... 60 | } 61 | 62 | // If there are no errors in ajv.errors then the field is shown 63 | ajv.validate(schema, data); 64 | 65 | ``` 66 | 67 | ## Single 68 | 69 | The Full option can be a bit verbose when you only rely on a single field's model, and thus you set the `model` property on the `displayOptions` object to only use the value of that field's model. 70 | 71 | ```js 72 | // Option 2 - single model 73 | data() { 74 | return { 75 | uiSchema: [{ 76 | component: 'div', 77 | children: [{ 78 | component: 'div', 79 | displayOptions: { 80 | // Here we set to use the firstName model as the value to evaluate the schema against 81 | model: 'firstName', 82 | schema: { 83 | type: 'string', 84 | minLength: 3 85 | } 86 | } 87 | }] 88 | }] 89 | } 90 | } 91 | ``` 92 | 93 | ## Usage with transitions 94 | 95 | To get some nice transitions all that is needed is to nest the component in a `transition` or `transition-group` field. 96 | Check out the [Vue.js guide on transitions](https://vuejs.org/v2/guide/transitions.html) for more info. 97 | 98 | ```js 99 | data() { 100 | return { 101 | uiSchema: [{ 102 | component: 'transition', 103 | fieldOptions: { 104 | // Set the name prop to `fade` 105 | props: { 106 | name: 'fade', 107 | // Any transition props such as `enter-class` 108 | // enterClass: '' 109 | }, 110 | on: { 111 | // Even javascript hooks are available 112 | enter: (el) => { 113 | console.log('hello!') 114 | }, 115 | leave: (el) => { 116 | console.log('goodbye!'); 117 | } 118 | } 119 | }, 120 | children: [{ 121 | component: 'div', 122 | displayOptions: { 123 | model: 'firstName' 124 | schema: { 125 | type: 'string', 126 | minLength: 3 127 | } 128 | } 129 | }] 130 | }] 131 | } 132 | } 133 | ``` 134 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/dynamic-options.md: -------------------------------------------------------------------------------- 1 | # dynamicOptions 2 | 3 | Sometimes a field needs to have properties added/removed when a field has a certain value. This is where `dynamicOptions` comes into play. 4 | `dynamicOptions` can alter any of a [field's top level properties](../field.md), like `children`, `component`, `fieldOptions` etc. when the given `schema` is `true` or `false`. The behavior is the same as [displayOptions](displayOptions.md), however in contrast to `displayOptions` you can pass in an array of options by using an array. 5 | 6 | There are 2 options: Full and Single. 7 | 8 | ## Full 9 | 10 | The full model uses the entire form model as data. 11 | 12 | ```js 13 | // Option 1 - full JSON schema 14 | data() { 15 | return { 16 | uiSchema: [{ 17 | component: 'div', 18 | children: [{ 19 | component: 'div', 20 | dynamicOptions: { 21 | schema: { 22 | type: object, 23 | properties: { 24 | firstName: { 25 | type: 'string', 26 | minLength: 3 27 | } 28 | }, 29 | required: ['firstName'] 30 | }, 31 | options: { 32 | // any of a field's top level property is valid here 33 | fieldOptions: { 34 | // any value that is valid in fieldOptions can be used here 35 | class: ['text-success'] 36 | } 37 | } 38 | } 39 | }] 40 | }] 41 | } 42 | } 43 | ``` 44 | 45 | ## Single 46 | 47 | The Full option can be a bit verbose when you only rely on a single field's model, and thus you set the `model` property on the `displayOptions` object to only use the value of that field's model. 48 | 49 | ```js 50 | // Option 2 - single schema 51 | data() { 52 | return { 53 | uiSchema: [{ 54 | component: 'div', 55 | children: [{ 56 | component: 'div', 57 | // Here we use an array of dynamic options, but you don't have to do that just to use the single schema 58 | // You can just as well use an object like in the "Full" example above. 59 | dynamicOptions: [ 60 | { 61 | // Here we set to use the firstName model as the value to evaluate the schema against 62 | model: 'firstName', 63 | schema: { 64 | type: 'string', 65 | minLength: 3 66 | }, 67 | options: { 68 | // any of a field's top level property is valid here 69 | fieldOptions: { 70 | // any value that is valid in fieldOptions can be used here 71 | class: ['text-success'], 72 | domProps: { 73 | innerHTML: 'Looking good, person with a first name with more than 3 characters!' 74 | } 75 | } 76 | } 77 | }, 78 | { 79 | model: 'firstName', 80 | schema: { 81 | type: 'string', 82 | maxLength: 2 83 | }, 84 | options: { 85 | // any of a field's top level property is valid here 86 | fieldOptions: { 87 | // any value that is valid in fieldOptions can be used here 88 | class: ['text-warning'], 89 | domProps: { 90 | innerHTML: 'Hey, that is cool! Your first name is less than or equal to 2 characters!' 91 | } 92 | } 93 | } 94 | } 95 | ] 96 | }] 97 | }] 98 | } 99 | } 100 | ``` 101 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/error-handler.md: -------------------------------------------------------------------------------- 1 | # Error handler 2 | 3 | `errorHandler` is a Boolean, which is set to true for fields which should handle the error messages for a model. 4 | The component will get the the same props as any other component but it cannot receive any [children](children.md) as the default slot is populated by the rendered `errors`. 5 | 6 | The errors are passed as the default slot are rendered individually as a `
`. 7 | If you want to render them differently you can handle the errors in the `vfjsFieldErrors` prop yourself. This is an array of the [Ajv validation errors](https://github.com/epoberezkin/ajv#validation-errors) 8 | 9 | ## Example 10 | 11 | ```js 12 | data() { 13 | return { 14 | jsonSchema: { 15 | type: 'object', 16 | required: ['input1'], 17 | properties: { 18 | input1: { 19 | type: 'string', 20 | minLength: 1 // Value has to be at least 1 character 21 | } 22 | } 23 | }, 24 | uiSchema: [{ 25 | component: 'input', 26 | model: 'input1', 27 | fieldOptions: { 28 | // Optionally set the attributes on the dom element as well 29 | // This can be used to let the browser validate the form 30 | // 31 | // If you don't want the browser to validate the form 32 | // use the `novalidate` attribute on 33 | attrs: { 34 | // Set required attribute on the dom element 35 | required: true 36 | // Set minlength attribute 37 | minlength: 1 38 | }, 39 | on: ['change'] 40 | } 41 | }, { 42 | // This component takes care of the errors from `input1` 43 | // If the form is submitted with the input being empty the error handler 44 | // will render the errors inside it 45 | component: 'div', 46 | model: 'input1', 47 | errorHandler: true, 48 | fieldOptions: { 49 | class: ['bg-danger'] 50 | } 51 | }] 52 | } 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/error-options.md: -------------------------------------------------------------------------------- 1 | # Error options 2 | 3 | Same API as [fieldOptions](field-options.md) but is only applied when the field has errors and has been modified. 4 | 5 | ## Example 6 | 7 | ```js 8 | data() { 9 | return { 10 | uiSchema: [{ 11 | component: 'input', 12 | model: 'input1', 13 | errorOptions: { 14 | class: ['is-invalid'] 15 | }, 16 | fieldOptions: { 17 | class: ['form-control'], 18 | on: ['input'] 19 | } 20 | }] 21 | } 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/event-prop.md: -------------------------------------------------------------------------------- 1 | # Event prop 2 | 3 | Sets which property should be emitted as the field's value when the component emits an event which is based on the [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event) interface. The property specified by `eventProp` is read from the event's `target` property, i.e. `event.target[eventProp]`. Default value is `value`, i.e. `event.target['value']`. 4 | 5 | ## Set globally for all fields 6 | 7 | ```js 8 | data() { 9 | return { 10 | options: { 11 | eventProp: 'target.href' 12 | } 13 | } 14 | } 15 | ``` 16 | 17 | ## Set locally for one field 18 | 19 | ```js 20 | data() { 21 | return { 22 | uiSchema: [{ 23 | component: 'input', 24 | model: 'isChecked', 25 | eventProp: 'checked', 26 | valueProp: 'checked', 27 | fieldOptions: { 28 | type: "checkbox", 29 | on: ['input'] 30 | } 31 | }] 32 | 33 | } 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/field-options.md: -------------------------------------------------------------------------------- 1 | # Field options 2 | 3 | ## Available properties 4 | 5 | > See Vue's guide on render function's [Data Object In-Depth]( https://vuejs.org/v2/guide/render-function.html#The-Data-Object-In-Depth) for more details on all available properties 6 | 7 | Since we're using Vue's [render functions](https://vuejs.org/v2/guide/render-function.html) to create the form elements basically any property that is supported in a render function can be used in the `fieldOptions` object. 8 | 9 | The following properties are tested and supported with `vue-form-json-schema` 10 | 11 | * [attrs](field-options/attrs.md) 12 | * [class](field-options/class.md) 13 | * [domProps](field-options/dom-props.md) 14 | * [key](field-options/key.md) 15 | * [nativeOn](field-options/native-on.md) 16 | * [on](field-options/on.md) 17 | * [props](field-options/props.md) 18 | * [slot](field-options/slot.md) 19 | * [style](field-options/style.md) 20 | * ref 21 | 22 | ## Example 23 | 24 | ```js 25 | data() { 26 | return { 27 | uiSchema: [{ 28 | component: 'input', 29 | fieldOptions: { 30 | class: ['form-control'], 31 | on: ['input'] 32 | } 33 | }] 34 | } 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/field-options/attrs.md: -------------------------------------------------------------------------------- 1 | # attrs 2 | 3 | Essentially any normal html attributes 4 | 5 | ```js 6 | // Normal HTML attributes 7 | data() { 8 | return { 9 | uiSchema: [{ 10 | component: 'div', 11 | fieldOptions: { 12 | attrs: { 13 | id: 'foo', 14 | placeholder: 'bar' 15 | } 16 | } 17 | }] 18 | } 19 | } 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/field-options/class.md: -------------------------------------------------------------------------------- 1 | # class 2 | 3 | The `class` property can be configured in 3 ways. Either as an `Object`, `Array` or simply a `String`. This is essentially the same API as [`v-bind:class`](https://vuejs.org/v2/guide/class-and-style.html) uses. 4 | 5 | ## Object 6 | 7 | An `Object` must have the classes as keys and the keys' value should be a `Boolean`. 8 | 9 | ```js 10 | // Option 1: Object 11 | data() { 12 | uiSchema: [{ 13 | component: 'div', 14 | fieldOptions: { 15 | class: { 16 | 'col-12': true, // Class is included 17 | 'col-md-6': true, // Class is included 18 | 'col-lg-4': false // Class is NOT included 19 | } 20 | } 21 | }] 22 | } 23 | ``` 24 | 25 | ## Array 26 | 27 | An `Array` should be a an array of strings, multidimensional arrays or arrays with any other values other than strings are not supported. 28 | 29 | ```js 30 | // Option 2: Arrays 31 | data() { 32 | uiSchema: [{ 33 | component: 'div', 34 | fieldOptions: { 35 | // All values in array is included 36 | class: [ 37 | 'col-12', 38 | 'col-md-6' 39 | ] 40 | } 41 | }] 42 | } 43 | ``` 44 | 45 | ## String 46 | 47 | ```js 48 | // Option 3: String 49 | data() { 50 | uiSchema: [{ 51 | component: 'div', 52 | fieldOptions: { 53 | class: 'col-12 col-md-6' 54 | } 55 | }] 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/field-options/dom-props.md: -------------------------------------------------------------------------------- 1 | # domProps 2 | 3 | > Be careful about setting `innerHTML` as that effectively disables a field's ability to render children 4 | 5 | ```js 6 | // DOM properties 7 | data() { 8 | return { 9 | uiSchema: [{ 10 | component: 'div', 11 | fieldOptions: { 12 | domProps: { 13 | innerHTML: '
Hello
' 14 | } 15 | } 16 | }] 17 | } 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/field-options/key.md: -------------------------------------------------------------------------------- 1 | # key 2 | 3 | > The key special attribute is primarily used as a hint for Vue’s virtual DOM algorithm to identify VNodes when diffing the new list of nodes against the old list. - [Vue.js guide](https://vuejs.org/v2/api/#key) 4 | 5 | This property is set to each field's unique internal id by default. You can optionally override it should you wish to specifiy it. 6 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/field-options/native-on.md: -------------------------------------------------------------------------------- 1 | # nativeOn 2 | 3 | Register events listeners for native [Events](https://developer.mozilla.org/en-US/docs/Web/Events). 4 | Accepts a `String`, `Array` or an `Object`. 5 | 6 | ## String 7 | 8 | ```js 9 | // Option 1: String 10 | data() { 11 | return { 12 | uiSchema: [{ 13 | component: 'input', 14 | fieldOptions: { 15 | on: 'input' 16 | } 17 | }] 18 | } 19 | } 20 | ``` 21 | 22 | ## Array 23 | 24 | ```js 25 | // Option 2: Arrays 26 | data() { 27 | return { 28 | uiSchema: [{ 29 | component: 'input', 30 | fieldOptions: { 31 | nativeOn: [ 32 | 'input' 33 | ] 34 | } 35 | }] 36 | } 37 | } 38 | ``` 39 | 40 | ## Object 41 | 42 | An `Object` can be used if the value should be manipulated or used elsewhere before set in to the model. 43 | 44 | > Note that the callback must be synchronous 45 | 46 | ```js 47 | // Option 3: Object 48 | data() { 49 | return { 50 | uiSchema: [{ 51 | component: 'input', 52 | fieldOptions: { 53 | nativeOn: { 54 | 'input': event => String(event.target.value).toLowerCase(); 55 | } 56 | } 57 | }] 58 | } 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/field-options/on.md: -------------------------------------------------------------------------------- 1 | # on 2 | 3 | Register events listeners for [Custom Events](https://vuejs.org/v2/guide/components.html#Custom-Events) emitted using [`$emit`](https://vuejs.org/v2/api/#vm-emit). 4 | Accepts a `String`, `Array` or an `Object`. 5 | 6 | ## String 7 | 8 | ```js 9 | // Option 1: String 10 | data() { 11 | return { 12 | uiSchema: [{ 13 | component: 'input', 14 | fieldOptions: { 15 | on: 'input' 16 | } 17 | }] 18 | } 19 | } 20 | ``` 21 | 22 | ## Array 23 | 24 | ```js 25 | // Option 2: Arrays 26 | data() { 27 | return { 28 | uiSchema: [{ 29 | component: 'input', 30 | fieldOptions: { 31 | on: [ 32 | 'input' 33 | ] 34 | } 35 | }] 36 | } 37 | } 38 | ``` 39 | 40 | ## Object 41 | 42 | An `Object` can be used if the value should be manipulated or used elsewhere before set in to the model. 43 | 44 | > Note that the callback must be synchronous 45 | 46 | ```js 47 | // Option 3: Object 48 | data() { 49 | return { 50 | uiSchema: { 51 | component: 'input', 52 | fieldOptions: { 53 | on: { 54 | 'change': value => String(value).toLowerCase(); 55 | } 56 | } 57 | } 58 | } 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/field-options/props.md: -------------------------------------------------------------------------------- 1 | # props 2 | 3 | Plain `Vue` [props](https://vuejs.org/v2/guide/components.html#Props) 4 | 5 | ```js 6 | // A custom component 7 | Vue.component('my-custom-component', { 8 | name: 'my-custom-component', 9 | props: ['message'], 10 | template: '{{ message }}' 11 | }) 12 | 13 | ... 14 | data() { 15 | return { 16 | uiSchema: [{ 17 | component: 'my-custom-component', 18 | fieldOptions: { 19 | props: { 20 | message: 'Hello!' 21 | } 22 | } 23 | }] 24 | } 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/field-options/slot.md: -------------------------------------------------------------------------------- 1 | # slot 2 | 3 | > Used on content inserted into child components to indicate which named slot the content belongs to. - [Vue.js guide](https://vuejs.org/v2/api/#slot) 4 | 5 | MyCustomComponent.vue 6 | 7 | ```html 8 | 13 | ``` 14 | 15 | MyForm.vue 16 | 17 | ```html 18 | 19 | ... 20 | 21 | 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/field-options/style.md: -------------------------------------------------------------------------------- 1 | # style 2 | 3 | The style property is simply an object where the key is the style property and the value is the property value. See Vue's guide on [`v-bind:style`](https://vuejs.org/v2/guide/class-and-style.html#Binding-Inline-Styles) for more information. 4 | 5 | > Note that the [Array syntax](https://vuejs.org/v2/guide/class-and-style.html#Array-Syntax-1) is NOT tested yet as of 2017-12-17 6 | 7 | ```js 8 | data() { 9 | return { 10 | uiSchema: { 11 | component: 'div', 12 | fieldOptions: { 13 | domProps: { 14 | innerHTML: 'This is some red text' 15 | }, 16 | style: { 17 | color: 'red', 18 | fontSize: '13px' 19 | } 20 | } 21 | } 22 | } 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/internal-model.md: -------------------------------------------------------------------------------- 1 | # Internal model 2 | 3 | Some component's require the model to be updated when the next [tick](https://vuejs.org/v2/api/#Vue-nextTick) happens. As there might be several `ticks` until `vue-form-json-schema` has updated the form model and re-renders the component. 4 | An example of this is the `el-input` component from [element-ui](https://element.eleme.io), which loses its cursor position on the next re-render when the value hasn't updated yet. 5 | 6 | VFJS can keep track of when the value changes and store it internally to ensure that on the next `tick` the 7 | 8 | ## Example 1 9 | 10 | Setting `internalModel` to `true` will update the value for all events in `fieldOptions.on` 11 | 12 | ```js 13 | data() { 14 | return { 15 | uiSchema: [{ 16 | component: 'custom-input', 17 | model: 'input', 18 | internalModel: true, 19 | fieldOptions: { 20 | on: ['input'] 21 | } 22 | }] 23 | } 24 | } 25 | ``` 26 | 27 | ## Example 2 28 | 29 | `internalModel` can also be an array with keys for event names. The internal model will only be updated when these events are emitted. 30 | 31 | ```js 32 | data() { 33 | return { 34 | uiSchema: [{ 35 | component: 'custom-input', 36 | model: 'input', 37 | internalModel: ['input'], 38 | fieldOptions: { 39 | on: [ 40 | 'input', 41 | 'custom-event' 42 | ] 43 | } 44 | }] 45 | } 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/value-model.md: -------------------------------------------------------------------------------- 1 | # Value model 2 | 3 | Sets which field's model value is passed down as the `value` prop. Default is the same as the field set in the `model` property. 4 | This is useful if you want to use another field's `model` as the value for another field. Note that the [valueProp](./value-prop.md) can set the `valueModel` to be passed to another prop than `value`. 5 | 6 | You can also use `valueModel: true` to pass the entire form model to the field. 7 | 8 | ## Example 9 | 10 | This example 11 | 12 | ```js 13 | data() { 14 | return { 15 | model: { 16 | options: ['Cat', 'Dog', 'Rabbit'], 17 | selectedOptions: [], 18 | }, 19 | uiSchema: [ 20 | { 21 | component: 'custom-select', 22 | model: 'selectedOptions', , // This sets what property in the form model is passed to this component's 'value' prop and by default this property is the one which will be updated when the 'change' event is emitted 23 | valueModel: 'options', // This overrides what property in the form model will be passed to this component, the default is to pass it to the 'value' prop 24 | valueProp: 'options', // This overrides the prop to which the field's model will be passed and sets it to 'options', this prop will receive the value of 'valueModel' if it is set, or else it will receive the normal 'model' value 25 | fieldOptions: { 26 | on: ['change'], 27 | }, 28 | } 29 | ] 30 | } 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/api/ui-schema/field/value-prop.md: -------------------------------------------------------------------------------- 1 | # Value prop 2 | 3 | Sets the property to which the field's `model` value is passed. Default is `value`. 4 | 5 | ## Example 6 | 7 | ```js 8 | data() { 9 | return { 10 | uiSchema: [{ 11 | component: 'restaurants-component', 12 | model: 'input', 13 | valueProp: 'restaurants', 14 | fieldOptions: { 15 | on: ['input'] 16 | } 17 | }] 18 | } 19 | } 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/api/vue-form-json-schema.md: -------------------------------------------------------------------------------- 1 | # vue-form-json-schema 2 | 3 | ## Props 4 | 5 | | Property | Value | Description | 6 | | ---------|-------| -------------| 7 | | tag | [HTML element tag name](https://developer.mozilla.org/en-US/docs/Web/HTML/Element) | Render the `vue-form-json-schema` element with this tag name | 8 | | model | `Object` | The form values | 9 | | options | `Object` | See [Options](options.md) | 10 | | schema | `Object` | A valid [JSON Schema]( http://json-schema.org/) (validated by [Ajv](https://github.com/epoberezkin/ajv)) | 11 | | ui-schema | `Object` | See [uiSchema](ui-schema.md) | 12 | | components | `Object` | See [components](components.md) | 13 | 14 | ## Events 15 | 16 | | Event | Description | 17 | | ---------|-------| -------------| 18 | | change | When the `model` is updated this function will be called with the new value as the first parameter | 19 | | state-change | When the internal state is updated this function will be called with the new value as the first parameter. The state is an `Object` which contains information such as validation errors. | 20 | | validated | When a validation has been performed this function will be called a `Boolean` as the first parameter describing the overall form valid state | 21 | 22 | ```html 23 | 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ## Option 1 4 | 5 | Import everything and use globally 6 | 7 | ```js 8 | // App.js 9 | import VueFormJsonSchema from 'vue-form-json-schema'; 10 | Vue.component('vue-form-json-schema', VueFormJsonSchema); 11 | ``` 12 | 13 | ```html 14 | 43 | 44 | 52 | ``` 53 | 54 | ## Option 2 55 | 56 | Import and use locally 57 | 58 | ```html 59 | 94 | 102 | ``` 103 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | > `vue-form-json-schema` is distributed as both an UMD and ES module version. If you use CommonJS/AMD or are stuck on an older version of Webpack you should use the UMD build. If you use `webpack`, `parcel` or `rollup` the ESM version will be used automatically. 4 | 5 | 6 | ## Requirements 7 | 8 | `Vue.js >= 2.4.0` 9 | 10 | ## Direct Download / CDN 11 | 12 | https://unpkg.com/vue-form-json-schema/vue-form-json-schema 13 | 14 | This link will always use the latest version. To use a specific version you can update the URL like this: 15 | 16 | https://unpkg.com/vue-form-json-schema@2.3.0 17 | 18 | ## NPM 19 | 20 | `npm install vue-form-json-schema` 21 | -------------------------------------------------------------------------------- /examples/example-1/example-one.js: -------------------------------------------------------------------------------- 1 | const template = ` 2 |
3 |

vue-form-json-schema

4 |

Example #1 Minimal example

5 |

6 | A minimal example showing a simple input field. 7 |

8 | 9 | 17 | 18 | 19 |
20 | 21 |

Model

22 | 23 | 24 |

Schema

25 | 26 | 27 |

UI Schema

28 | 29 | 30 |

State

31 | 32 | 33 |

Valid

34 |
{{ valid }}
35 |
36 | `; 37 | 38 | window.Vue.component('example-one', { 39 | name: 'example-one', 40 | template, 41 | data() { 42 | return { 43 | model: {}, 44 | state: {}, 45 | valid: false, 46 | schema: { 47 | type: 'object', 48 | properties: { 49 | firstName: { 50 | type: 'string', 51 | }, 52 | }, 53 | }, 54 | uiSchema: [ 55 | { 56 | component: 'input', 57 | model: 'firstName', 58 | fieldOptions: { 59 | class: ['form-control'], 60 | on: ['input'], 61 | attrs: { 62 | placeholder: 'Please enter your first name', 63 | }, 64 | }, 65 | }, 66 | ], 67 | }; 68 | }, 69 | methods: { 70 | onChange(value) { 71 | this.model = value; 72 | }, 73 | onChangeState(value) { 74 | this.state = value; 75 | }, 76 | onValidated(value) { 77 | this.valid = value; 78 | }, 79 | }, 80 | }); 81 | 82 | window.Vue.config.productionTip = false; 83 | 84 | /* eslint-disable no-new */ 85 | new window.Vue({ 86 | el: '#app', 87 | template: '', 88 | }); 89 | -------------------------------------------------------------------------------- /examples/example-1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Example One 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/example-2/example-two.js: -------------------------------------------------------------------------------- 1 | const template = ` 2 |
3 |

vue-form-json-schema

4 |

Example #2 Nested UI example

5 | 6 |

Click here to edit this demo

7 |

Click here to show this demo in a separate window

8 | 9 | 17 | 18 | 19 |
20 | This example uses the Bootstrap row and col-* 21 | classes to create a two-column row for devices with larger screens and a single column row for devices with smaller screens. 22 | We also wrap each input in a .form-group 23 |
24 | 25 |
26 | 27 |

Model

28 | 29 | 30 |

Schema

31 | 32 | 33 |

UI Schema

34 | 35 | 36 |

State

37 | 38 | 39 |

Valid

40 |
{{ valid }}
41 |
42 | `; 43 | 44 | window.Vue.component('example-two', { 45 | name: 'example-two', 46 | template, 47 | data() { 48 | return { 49 | model: {}, 50 | state: {}, 51 | valid: false, 52 | schema: { 53 | type: 'object', 54 | properties: { 55 | firstName: { 56 | type: 'string', 57 | }, 58 | lastName: { 59 | type: 'string', 60 | }, 61 | }, 62 | }, 63 | uiSchema: [ 64 | { 65 | component: 'div', 66 | fieldOptions: { 67 | class: 'row', 68 | }, 69 | children: [ 70 | { 71 | component: 'div', 72 | fieldOptions: { 73 | class: 'col-12 col-sm-6', 74 | }, 75 | children: [ 76 | { 77 | component: 'div', 78 | fieldOptions: { 79 | class: 'form-group', 80 | }, 81 | children: [ 82 | { 83 | component: 'input', 84 | model: 'firstName', 85 | fieldOptions: { 86 | class: ['form-control'], 87 | on: ['input'], 88 | attrs: { 89 | placeholder: 'First name', 90 | }, 91 | }, 92 | }, 93 | ], 94 | }, 95 | ], 96 | }, 97 | { 98 | component: 'div', 99 | fieldOptions: { 100 | class: 'col-12 col-sm-6', 101 | }, 102 | children: [ 103 | { 104 | component: 'div', 105 | fieldOptions: { 106 | class: 'form-group', 107 | }, 108 | children: [ 109 | { 110 | component: 'input', 111 | model: 'lastName', 112 | fieldOptions: { 113 | class: ['form-control'], 114 | on: ['input'], 115 | attrs: { 116 | placeholder: 'Last name', 117 | }, 118 | }, 119 | }, 120 | ], 121 | }, 122 | ], 123 | }, 124 | ], 125 | }, 126 | ], 127 | }; 128 | }, 129 | methods: { 130 | onChange(value) { 131 | this.model = value; 132 | }, 133 | onChangeState(value) { 134 | this.state = value; 135 | }, 136 | onValidated(value) { 137 | this.valid = value; 138 | }, 139 | }, 140 | }); 141 | 142 | window.Vue.config.productionTip = false; 143 | 144 | /* eslint-disable no-new */ 145 | new window.Vue({ 146 | el: '#app', 147 | template: '', 148 | }); 149 | -------------------------------------------------------------------------------- /examples/example-2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Example Two 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/example-3/example-three.js: -------------------------------------------------------------------------------- 1 | const template = ` 2 |
3 |

Example #3 Conditional visibility and animation example

4 | 5 |

Click here to edit this demo

6 |

Click here to show this demo in a separate window

7 | 8 |
9 | 17 | 18 |
19 | 20 |
21 | 22 |

23 | In this example some fields remain hidden until a certain criteria is met. 24 | We couple that with the <transition> element to show a nice animation when the element comes into and disappears from the view. 25 |

26 |

27 | To create a condition we use the displayOptions property on a ui-schema field. 28 | The condition is simply a JSON Schema and can make use of any available JSON Schema logic. 29 | To make it easier to write conditions a model property can be set on the displayOptions object to target a specific field's model. 30 |

31 |

32 | Here we target the first animation to appear when firstName is longer than 3 characters. The second animation is set to appear when both firstName and lastName are at least 3 characters or longer 33 |

34 | 35 |
Prevent content being pushed around with a fixed height
36 |

37 | We can set a height to preserve the space where the animated divs will pop in. 38 | This avoids the issue of the added divs pushing the content below down and up when transitioning in and out 39 |

40 | 41 | 42 | 43 |
44 | 45 |

Model

46 | 47 | 48 |

Schema

49 | 50 | 51 |

UI Schema

52 | 53 | 54 |

State

55 | 56 | 57 |

Valid

58 |
{{ valid }}
59 |
60 | `; 61 | 62 | window.Vue.component('example-three', { 63 | name: 'example-three', 64 | template, 65 | data() { 66 | return { 67 | model: {}, 68 | state: {}, 69 | staticHeight: false, 70 | valid: false, 71 | }; 72 | }, 73 | computed: { 74 | schema() { 75 | return { 76 | type: 'object', 77 | properties: { 78 | firstName: { 79 | type: 'string', 80 | }, 81 | lastName: { 82 | type: 'string', 83 | }, 84 | }, 85 | }; 86 | }, 87 | uiSchema() { 88 | return [ 89 | { 90 | component: 'div', 91 | fieldOptions: { 92 | class: 'row mb-3', 93 | }, 94 | children: [ 95 | { 96 | component: 'div', 97 | fieldOptions: { 98 | class: 'col', 99 | }, 100 | children: [ 101 | { 102 | component: 'input', 103 | model: 'firstName', 104 | fieldOptions: { 105 | attrs: { 106 | placeholder: 'Please enter your first name', 107 | }, 108 | class: ['form-control'], 109 | on: ['input'], 110 | }, 111 | }, 112 | ], 113 | }, 114 | { 115 | component: 'div', 116 | fieldOptions: { 117 | class: 'col', 118 | }, 119 | children: [ 120 | { 121 | component: 'input', 122 | model: 'lastName', 123 | fieldOptions: { 124 | attrs: { 125 | placeholder: 'Please enter your last name', 126 | }, 127 | class: ['form-control'], 128 | on: ['input'], 129 | }, 130 | }, 131 | ], 132 | }, 133 | ], 134 | }, 135 | { 136 | component: 'div', 137 | fieldOptions: { 138 | class: 'row mb-3', 139 | style: { 140 | // We can set a height to preserve the space where the animated divs will pop in 141 | // This avoids the issue of the added divs pushing the content below down and up 142 | // when transitioning in and out 143 | height: this.staticHeight ? '24px' : 'auto', 144 | }, 145 | }, 146 | children: [ 147 | { 148 | component: 'div', 149 | fieldOptions: { 150 | class: 'col', 151 | }, 152 | children: [ 153 | { 154 | component: 'transition', 155 | fieldOptions: { 156 | props: { 157 | name: 'fade', 158 | }, 159 | }, 160 | children: [ 161 | { 162 | component: 'div', 163 | displayOptions: { 164 | model: 'firstName', 165 | schema: { 166 | type: 'string', 167 | minLength: 3, 168 | }, 169 | }, 170 | children: [ 171 | { 172 | component: 'div', 173 | fieldOptions: { 174 | domProps: { 175 | innerHTML: 'First animation', 176 | }, 177 | }, 178 | }, 179 | ], 180 | }, 181 | ], 182 | }, 183 | ], 184 | }, 185 | { 186 | component: 'div', 187 | fieldOptions: { 188 | class: 'col', 189 | }, 190 | children: [ 191 | { 192 | component: 'transition', 193 | fieldOptions: { 194 | props: { 195 | name: 'fade', 196 | }, 197 | }, 198 | children: [ 199 | { 200 | component: 'div', 201 | displayOptions: { 202 | schema: { 203 | properties: { 204 | firstName: { 205 | type: 'string', 206 | minLength: 3, 207 | }, 208 | lastName: { 209 | type: 'string', 210 | minLength: 3, 211 | }, 212 | }, 213 | required: ['firstName', 'lastName'], 214 | }, 215 | }, 216 | children: [ 217 | { 218 | component: 'div', 219 | fieldOptions: { 220 | domProps: { 221 | innerHTML: 'Second animation', 222 | }, 223 | }, 224 | }, 225 | ], 226 | }, 227 | ], 228 | }, 229 | ], 230 | }, 231 | ], 232 | }, 233 | ]; 234 | }, 235 | }, 236 | methods: { 237 | onToggleStaticHeight() { 238 | this.staticHeight = !this.staticHeight; 239 | }, 240 | onChange(value) { 241 | this.model = value; 242 | }, 243 | onChangeState(value) { 244 | this.state = value; 245 | }, 246 | onValidated(value) { 247 | this.valid = value; 248 | }, 249 | }, 250 | }); 251 | 252 | window.Vue.config.productionTip = false; 253 | 254 | /* eslint-disable no-new */ 255 | new window.Vue({ 256 | el: '#app', 257 | template: '', 258 | }); 259 | -------------------------------------------------------------------------------- /examples/example-3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Example Three 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/example-3/style.css: -------------------------------------------------------------------------------- 1 | .fade-enter-active, .fade-leave-active { 2 | transition: opacity .5s; 3 | } 4 | .fade-enter, .fade-leave-to { 5 | opacity: 0; 6 | } 7 | -------------------------------------------------------------------------------- /examples/example-4/example-four.js: -------------------------------------------------------------------------------- 1 | const template = ` 2 |
3 |

vue-form-json-schema

4 |

Example #4 Vue components example

5 |

6 | An example showing how to use Vue components in 8 | vue-form-json-schema. 9 | 10 | In this example we use the <b-form-input> component 12 | from Bootstrap Vue 14 |

15 |

16 | First: Any component or DOM element can be used in 18 | vue-form-json-schema. 19 | 20 | Any Vue component which uses the v-model can be 22 | used. 24 | See more info below the demo about how to configure Vue 26 | components. 28 |

29 | 30 | 38 | 39 | 40 |
41 | 42 |

43 | Most Vue components which support the v-model can be used 45 | with a very simple configuration 47 | 48 |

49 |

50 | The vue-component-name should be replaced by the globally 52 | registered name of the component. 54 | A globally registered component is one that is imported and registered 56 | like this: 57 | Vue.component('vue-component-name', VueComponentName) 59 |

60 |

61 | This is pretty much it. 62 | Most Vue components will work right away using this 64 | configuration. 66 |

67 | 68 |
69 | 70 |

Model

71 | 72 | 73 |

Schema

74 | 75 | 76 |

UI Schema

77 | 78 | 79 |

State

80 | 81 | 82 |

Valid

83 |
{{ valid }}
84 |
85 | `; 86 | 87 | window.Vue.component('example-four', { 88 | name: 'example-four', 89 | template, 90 | data() { 91 | return { 92 | model: {}, 93 | state: {}, 94 | valid: false, 95 | schema: { 96 | type: 'object', 97 | properties: { 98 | firstName: { 99 | type: 'string', 100 | }, 101 | }, 102 | }, 103 | uiSchema: [ 104 | { 105 | component: 'b-form-input', 106 | model: 'firstName', 107 | fieldOptions: { 108 | class: ['form-control'], 109 | on: ['input'], 110 | props: { 111 | type: 'text', 112 | placeholder: 'I am the b-form-input component from BootstrapVue!', 113 | }, 114 | }, 115 | }, 116 | ], 117 | componentConfig: { 118 | component: 'vue-component-name', 119 | model: 'firstName', 120 | fieldOptions: { 121 | on: ['input'], 122 | }, 123 | }, 124 | }; 125 | }, 126 | methods: { 127 | onChange(value) { 128 | this.model = value; 129 | }, 130 | onChangeState(value) { 131 | this.state = value; 132 | }, 133 | onValidated(value) { 134 | this.valid = value; 135 | }, 136 | }, 137 | }); 138 | 139 | window.Vue.config.productionTip = false; 140 | 141 | /* eslint-disable no-new */ 142 | new window.Vue({ 143 | el: '#app', 144 | template: '', 145 | }); 146 | -------------------------------------------------------------------------------- /examples/example-4/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Example Four 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/example-5/example-five.js: -------------------------------------------------------------------------------- 1 | const template = ` 2 |
3 |
4 |

vue-form-json-schema

5 |

Example #5 Async loading of form

6 |

7 | An example to show how to use remotely loaded JSON schema/uiSchema and 9 | model 11 |

12 | 13 | 22 | 23 |
24 |

Hang on!

25 |
Loading form...
26 |
27 |
28 | 29 |
30 | 31 |
32 | 33 |

34 | 35 | This demo shows how the schema,  36 | uiSchema and the model can be fetched from an 37 | API and rendered at a later time. 38 | 39 |

40 | 41 |

42 | 43 | In this demo we use setTimeout to simulate an asynchronous 44 | action so that the form can "loaded" after 5 seconds. 45 | 46 |

47 | 48 |
49 | 50 |

Model

51 | 52 | 53 |

Schema

54 | 55 | 56 |

UI Schema

57 | 58 | 59 |

State

60 | 61 | 62 |

Valid

63 |
{{ valid }}
64 |
65 |
66 | `; 67 | 68 | window.Vue.component('example-five', { 69 | name: 'example-five', 70 | template, 71 | data() { 72 | return { 73 | model: {}, 74 | state: {}, 75 | valid: false, 76 | schema: {}, 77 | uiSchema: [], 78 | loaded: false, 79 | delay: 5000, 80 | }; 81 | }, 82 | methods: { 83 | onChange(value) { 84 | this.model = value; 85 | }, 86 | onChangeState(value) { 87 | this.state = value; 88 | }, 89 | onValidated(value) { 90 | this.valid = value; 91 | }, 92 | getUiSchemaFromAPI() { 93 | return new Promise((resolve, reject) => { 94 | setTimeout(() => { 95 | resolve([ 96 | { 97 | component: 'input', 98 | model: 'firstName', 99 | fieldOptions: { 100 | class: ['form-control'], 101 | on: ['input'], 102 | attrs: { 103 | placeholder: 'Please enter your first name', 104 | }, 105 | }, 106 | }, 107 | ]); 108 | }, this.delay); 109 | }); 110 | }, 111 | getSchemaFromAPI() { 112 | return new Promise((resolve, reject) => { 113 | setTimeout(() => { 114 | resolve({ 115 | type: 'object', 116 | required: ['firstName'], 117 | properties: { 118 | firstName: { 119 | type: 'string', 120 | }, 121 | }, 122 | }); 123 | }, this.delay); 124 | }); 125 | }, 126 | getDataFromAPI() { 127 | return new Promise((resolve, reject) => { 128 | setTimeout(() => { 129 | resolve({ 130 | firstName: 'Tobias', 131 | }); 132 | }, this.delay); 133 | }); 134 | }, 135 | getForm() { 136 | // Reset properties 137 | this.uiSchema = []; 138 | this.schema = []; 139 | this.model = []; 140 | 141 | // Set form as not loaded 142 | this.loaded = false; 143 | 144 | // Get the data from the API 145 | return Promise.all([ 146 | this.getUiSchemaFromAPI(), 147 | this.getSchemaFromAPI(), 148 | this.getDataFromAPI(), 149 | ]).then(([uiSchema, schema, model]) => { 150 | // Update the form properties with data from the API 151 | this.uiSchema = uiSchema; 152 | this.schema = schema; 153 | this.model = model; 154 | 155 | // Set form as loaded 156 | this.loaded = true; 157 | }); 158 | }, 159 | }, 160 | created() { 161 | this.getForm(); 162 | }, 163 | }); 164 | 165 | window.Vue.config.productionTip = false; 166 | 167 | /* eslint-disable no-new */ 168 | new window.Vue({ 169 | el: '#app', 170 | template: '', 171 | }); 172 | -------------------------------------------------------------------------------- /examples/example-5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Example Five 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/example-6/example-six.js: -------------------------------------------------------------------------------- 1 | const template = ` 2 |
3 |

vue-form-json-schema

4 |

Example #6 Registration form example

5 |

6 | A medium complex example showing a registration form with validation and validation messages. 7 |

8 | 9 |

10 | In this example we configure the options property castToSchemaType to allow values to be cast to their correct type. Without this property all values would be strings for fields where the component is a regular HTML element, such as input, which would make any validation based on other types fail. 11 |

12 | 13 |

14 | Try submitting the form empty and see what error messages you get. You can try typing a number less than 18 into the Age input field and moving to another field (i.e. causing a blur event). This will show you a different validation message when the condition value < 18 is met. 15 |

16 | 17 |

18 | Complete the form with valid information and you can see how the message below the submit button changes. A different message can be shown when all errors have been fixed to let the user know that it can now submit the form again and it will succeed. 19 |

20 | 21 |

22 | Try filling out the form correctly but enter a value less than 18 into the Age input field. You see that when you navigate away the validation message with the 18 year limit appear immediately. 23 |

24 | 25 |
26 | 35 | 36 | 37 |
38 | 41 |
42 | 43 | 44 |
45 | Form submitted successfully! 46 |
47 |
48 | Form has errors, please fix and resubmit! 49 |
50 |
51 | Form errors have been corrected. You can now resubmit the form. 52 |
53 |
54 |
55 | 56 |
57 | 58 |

Model

59 | 60 | 61 |

Options

62 | 63 | 64 |

Schema

65 | 66 | 67 |

UI Schema

68 | 69 | 70 |

State

71 | 72 | 73 |

Valid

74 |
{{ valid }}
75 |
76 | `; 77 | 78 | window.Vue.component('example-six', { 79 | name: 'example-six', 80 | template, 81 | data() { 82 | return { 83 | model: {}, 84 | state: {}, 85 | valid: false, 86 | options: { 87 | castToSchemaType: true, 88 | }, 89 | submitted: false, 90 | success: false, 91 | schema: { 92 | type: 'object', 93 | required: ['firstName', 'lastName', 'age', 'consent'], 94 | properties: { 95 | firstName: { 96 | type: 'string', 97 | minLength: 1, 98 | }, 99 | lastName: { 100 | type: 'string', 101 | minLength: 1, 102 | }, 103 | age: { 104 | type: 'number', 105 | minimum: 18, 106 | }, 107 | message: { 108 | type: 'string', 109 | }, 110 | consent: { 111 | type: 'boolean', 112 | const: true, 113 | }, 114 | }, 115 | }, 116 | uiSchema: [ 117 | // Input for firstName 118 | { 119 | component: 'div', 120 | fieldOptions: { 121 | class: ['form-group'], 122 | }, 123 | children: [ 124 | { 125 | component: 'label', 126 | fieldOptions: { 127 | attrs: { 128 | for: 'first-name', 129 | }, 130 | class: ['font-weight-bold'], 131 | domProps: { 132 | innerHTML: 'First name', 133 | }, 134 | }, 135 | }, 136 | { 137 | component: 'input', 138 | model: 'firstName', 139 | errorOptions: { 140 | class: ['is-invalid'], 141 | }, 142 | fieldOptions: { 143 | attrs: { 144 | id: 'first-name', 145 | }, 146 | class: ['form-control'], 147 | on: ['input'], 148 | }, 149 | }, 150 | { 151 | component: 'small', 152 | fieldOptions: { 153 | class: ['text-muted'], 154 | domProps: { 155 | innerHTML: 'Please enter your first name', 156 | }, 157 | }, 158 | }, 159 | ], 160 | }, 161 | // Validation messages for firstName 162 | { 163 | component: 'transition', 164 | fieldOptions: { 165 | props: { 166 | name: 'fade', 167 | }, 168 | }, 169 | children: [ 170 | { 171 | component: 'div', 172 | model: 'firstName', 173 | errorHandler: true, 174 | displayOptions: { 175 | model: 'firstName', 176 | schema: { 177 | not: { 178 | type: 'string', 179 | minLength: 1, 180 | }, 181 | }, 182 | }, 183 | fieldOptions: { 184 | class: ['alert alert-danger'], 185 | }, 186 | children: [ 187 | { 188 | component: 'div', 189 | fieldOptions: { 190 | domProps: { 191 | innerHTML: 'This field is required', 192 | }, 193 | }, 194 | }, 195 | ], 196 | }, 197 | ], 198 | }, 199 | // Input for lastName 200 | { 201 | component: 'div', 202 | fieldOptions: { 203 | class: ['form-group'], 204 | }, 205 | children: [ 206 | { 207 | component: 'label', 208 | fieldOptions: { 209 | attrs: { 210 | for: 'last-name', 211 | }, 212 | class: ['font-weight-bold'], 213 | domProps: { 214 | innerHTML: 'Last name', 215 | }, 216 | }, 217 | }, 218 | { 219 | component: 'input', 220 | model: 'lastName', 221 | errorOptions: { 222 | class: ['is-invalid'], 223 | }, 224 | fieldOptions: { 225 | attrs: { 226 | id: 'last-name', 227 | }, 228 | class: ['form-control'], 229 | on: ['input'], 230 | }, 231 | }, 232 | { 233 | component: 'small', 234 | fieldOptions: { 235 | class: ['text-muted'], 236 | domProps: { 237 | innerHTML: 'Please enter your last name', 238 | }, 239 | }, 240 | }, 241 | ], 242 | }, 243 | // Validation messages for lastName 244 | { 245 | component: 'transition', 246 | fieldOptions: { 247 | props: { 248 | name: 'fade', 249 | }, 250 | }, 251 | children: [ 252 | { 253 | component: 'div', 254 | model: 'lastName', 255 | errorHandler: true, 256 | displayOptions: { 257 | model: 'lastName', 258 | schema: { 259 | not: { 260 | type: 'string', 261 | minLength: 1, 262 | }, 263 | }, 264 | }, 265 | fieldOptions: { 266 | class: ['alert alert-danger'], 267 | }, 268 | children: [ 269 | { 270 | component: 'div', 271 | fieldOptions: { 272 | domProps: { 273 | innerHTML: 'This field is required', 274 | }, 275 | }, 276 | }, 277 | ], 278 | }, 279 | ], 280 | }, 281 | // Input for age 282 | { 283 | component: 'div', 284 | fieldOptions: { 285 | class: ['form-group'], 286 | }, 287 | children: [ 288 | { 289 | component: 'label', 290 | fieldOptions: { 291 | attrs: { 292 | for: 'age', 293 | }, 294 | class: ['font-weight-bold'], 295 | domProps: { 296 | innerHTML: 'Age', 297 | }, 298 | }, 299 | }, 300 | { 301 | component: 'input', 302 | model: 'age', 303 | errorOptions: { 304 | class: ['is-invalid'], 305 | }, 306 | fieldOptions: { 307 | attrs: { 308 | id: 'age', 309 | type: 'number', 310 | min: 0, 311 | }, 312 | class: ['form-control'], 313 | on: ['input'], 314 | }, 315 | }, 316 | { 317 | component: 'small', 318 | fieldOptions: { 319 | class: ['text-muted'], 320 | domProps: { 321 | innerHTML: 'Please confirm that you are over 18 years of age', 322 | }, 323 | }, 324 | }, 325 | ], 326 | }, 327 | // Validation messages for age 328 | { 329 | component: 'transition', 330 | fieldOptions: { 331 | props: { 332 | name: 'fade', 333 | mode: 'out-in', 334 | }, 335 | }, 336 | children: [ 337 | // Validation message shown when value is empty 338 | { 339 | component: 'div', 340 | model: 'age', 341 | errorHandler: true, 342 | displayOptions: { 343 | model: 'age', 344 | schema: { 345 | not: { 346 | type: 'number', 347 | }, 348 | }, 349 | }, 350 | fieldOptions: { 351 | class: ['alert alert-danger'], 352 | }, 353 | children: [ 354 | { 355 | component: 'div', 356 | fieldOptions: { 357 | domProps: { 358 | innerHTML: 'This field is required', 359 | }, 360 | }, 361 | }, 362 | ], 363 | }, 364 | // Validation message when value < 18 365 | { 366 | component: 'div', 367 | model: 'age', 368 | errorHandler: true, 369 | displayOptions: { 370 | model: 'age', 371 | schema: { 372 | type: 'number', 373 | not: { 374 | minimum: 18, 375 | }, 376 | }, 377 | }, 378 | fieldOptions: { 379 | class: ['alert alert-danger'], 380 | }, 381 | children: [ 382 | { 383 | component: 'div', 384 | fieldOptions: { 385 | domProps: { 386 | innerHTML: 'You must be 18 or older to submit this form', 387 | }, 388 | }, 389 | }, 390 | ], 391 | }, 392 | ], 393 | }, 394 | { 395 | component: 'div', 396 | fieldOptions: { 397 | class: ['form-group'], 398 | }, 399 | children: [ 400 | { 401 | component: 'div', 402 | fieldOptions: { 403 | class: ['font-weight-bold'], 404 | domProps: { 405 | innerHTML: 'Message (optional)', 406 | }, 407 | }, 408 | }, 409 | { 410 | component: 'textarea', 411 | model: 'message', 412 | fieldOptions: { 413 | attrs: { 414 | placeholder: 'Type a message here...', 415 | }, 416 | class: ['form-control'], 417 | on: ['input'], 418 | }, 419 | }, 420 | ], 421 | }, 422 | // Radio buttons for consent 423 | { 424 | component: 'div', 425 | fieldOptions: { 426 | class: ['form-group'], 427 | }, 428 | children: [ 429 | { 430 | component: 'div', 431 | fieldOptions: { 432 | class: ['font-weight-bold'], 433 | domProps: { 434 | innerHTML: 'Terms and conditions', 435 | }, 436 | }, 437 | }, 438 | { 439 | component: 'div', 440 | children: [ 441 | { 442 | component: 'span', 443 | fieldOptions: { 444 | domProps: { 445 | innerHTML: 'Please acknowledge that you have read and accept our ', 446 | }, 447 | }, 448 | }, 449 | { 450 | component: 'a', 451 | fieldOptions: { 452 | attrs: { 453 | href: '#', 454 | }, 455 | domProps: { 456 | innerHTML: 'Terms and conditions', 457 | }, 458 | }, 459 | }, 460 | ], 461 | }, 462 | // "Yes" radio button 463 | { 464 | component: 'div', 465 | fieldOptions: { 466 | class: ['form-check'], 467 | }, 468 | children: [ 469 | { 470 | component: 'input', 471 | model: 'consent', 472 | errorOptions: { 473 | class: ['is-invalid'], 474 | }, 475 | fieldOptions: { 476 | class: ['form-check-input'], 477 | on: 'change', 478 | attrs: { 479 | id: 'consent-yes', 480 | name: 'name', 481 | type: 'radio', 482 | }, 483 | domProps: { 484 | value: true, 485 | }, 486 | }, 487 | }, 488 | { 489 | component: 'label', 490 | fieldOptions: { 491 | attrs: { 492 | for: 'consent-yes', 493 | }, 494 | class: ['form-check-label'], 495 | domProps: { 496 | innerHTML: 'Yes, I agree', 497 | }, 498 | }, 499 | }, 500 | ], 501 | }, 502 | // "No" radio button 503 | { 504 | component: 'div', 505 | fieldOptions: { 506 | class: ['form-check'], 507 | }, 508 | children: [ 509 | { 510 | component: 'input', 511 | model: 'consent', 512 | errorOptions: { 513 | class: ['is-invalid'], 514 | }, 515 | fieldOptions: { 516 | class: ['form-check-input'], 517 | on: 'change', 518 | attrs: { 519 | id: 'consent-no', 520 | name: 'name', 521 | type: 'radio', 522 | }, 523 | domProps: { 524 | value: false, 525 | }, 526 | }, 527 | }, 528 | { 529 | component: 'label', 530 | fieldOptions: { 531 | attrs: { 532 | for: 'consent-no', 533 | }, 534 | class: ['form-check-label'], 535 | domProps: { 536 | innerHTML: 'No, I do not agree', 537 | }, 538 | }, 539 | }, 540 | ], 541 | }, 542 | ], 543 | }, 544 | // Validation messages for consent 545 | { 546 | component: 'transition', 547 | fieldOptions: { 548 | props: { 549 | name: 'fade', 550 | mode: 'out-in', 551 | }, 552 | }, 553 | children: [ 554 | // Validation message shown when an input has not been selected 555 | { 556 | component: 'div', 557 | model: 'consent', 558 | errorHandler: true, 559 | displayOptions: { 560 | model: 'consent', 561 | schema: { 562 | not: { 563 | type: 'boolean', 564 | }, 565 | }, 566 | }, 567 | fieldOptions: { 568 | class: ['alert alert-danger'], 569 | }, 570 | children: [ 571 | { 572 | component: 'div', 573 | fieldOptions: { 574 | domProps: { 575 | innerHTML: 'This field is required', 576 | }, 577 | }, 578 | }, 579 | ], 580 | }, 581 | // Validation message shown when the "No" input has been selected 582 | { 583 | component: 'div', 584 | model: 'consent', 585 | errorHandler: true, 586 | displayOptions: { 587 | model: 'consent', 588 | schema: { 589 | const: false, 590 | }, 591 | }, 592 | fieldOptions: { 593 | class: ['alert alert-danger'], 594 | }, 595 | children: [ 596 | { 597 | component: 'div', 598 | fieldOptions: { 599 | domProps: { 600 | innerHTML: 601 | 'You must consent to our terms and conditions to submit this form.', 602 | }, 603 | }, 604 | }, 605 | ], 606 | }, 607 | ], 608 | }, 609 | ], 610 | }; 611 | }, 612 | methods: { 613 | onChange(value) { 614 | this.model = value; 615 | }, 616 | onChangeState(value) { 617 | this.state = value; 618 | }, 619 | onValidated(value) { 620 | this.valid = value; 621 | }, 622 | onSubmit(e) { 623 | e.preventDefault(); 624 | 625 | this.submitted = true; 626 | this.options = { 627 | ...this.options, 628 | showValidationErrors: true, 629 | }; 630 | 631 | if (this.valid) { 632 | this.success = true; 633 | } 634 | }, 635 | }, 636 | }); 637 | 638 | window.Vue.config.productionTip = false; 639 | 640 | /* eslint-disable no-new */ 641 | new window.Vue({ 642 | el: '#app', 643 | template: '', 644 | }); 645 | -------------------------------------------------------------------------------- /examples/example-6/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Example Six 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/example-6/style.css: -------------------------------------------------------------------------------- 1 | .fade-enter-active, .fade-leave-active { 2 | transition: opacity .5s; 3 | } 4 | .fade-enter, .fade-leave-to { 5 | opacity: 0; 6 | } 7 | -------------------------------------------------------------------------------- /examples/example-7/example-seven.js: -------------------------------------------------------------------------------- 1 | const template = ` 2 |
3 |

vue-form-json-schema

4 |

Example #7 Registration form example with nested properties

5 |

6 | This is an implementation of the form from example 6 but with nested all properties nested under user. 7 |

8 | 9 |
10 | 19 | 20 | 21 |
22 | 25 |
26 | 27 | 28 |
29 | Form submitted successfully! 30 |
31 |
32 | Form has errors, please fix and resubmit! 33 |
34 |
35 | Form errors have been corrected. You can now resubmit the form. 36 |
37 |
38 |
39 | 40 |
41 | 42 |

Model

43 | 44 | 45 |

Options

46 | 47 | 48 |

Schema

49 | 50 | 51 |

UI Schema

52 | 53 | 54 |

State

55 | 56 | 57 |

Valid

58 |
{{ valid }}
59 |
60 | `; 61 | 62 | window.Vue.component('example-seven', { 63 | name: 'example-seven', 64 | template, 65 | data() { 66 | return { 67 | model: {}, 68 | state: {}, 69 | valid: false, 70 | options: { 71 | castToSchemaType: true, 72 | }, 73 | submitted: false, 74 | success: false, 75 | schema: { 76 | type: 'object', 77 | required: ['user'], 78 | properties: { 79 | user: { 80 | type: 'object', 81 | required: ['firstName', 'lastName', 'age', 'consent'], 82 | properties: { 83 | firstName: { 84 | type: 'string', 85 | }, 86 | lastName: { 87 | type: 'string', 88 | }, 89 | age: { 90 | type: 'number', 91 | minimum: 18, 92 | }, 93 | message: { 94 | type: 'string', 95 | }, 96 | consent: { 97 | type: 'boolean', 98 | const: true, 99 | }, 100 | }, 101 | }, 102 | }, 103 | }, 104 | uiSchema: [ 105 | // Input for firstName 106 | { 107 | component: 'div', 108 | fieldOptions: { 109 | class: ['form-group'], 110 | }, 111 | children: [ 112 | { 113 | component: 'label', 114 | fieldOptions: { 115 | attrs: { 116 | for: 'first-name', 117 | }, 118 | class: ['font-weight-bold'], 119 | domProps: { 120 | innerHTML: 'First name', 121 | }, 122 | }, 123 | }, 124 | { 125 | component: 'input', 126 | model: 'user.firstName', 127 | errorOptions: { 128 | class: ['is-invalid'], 129 | }, 130 | fieldOptions: { 131 | attrs: { 132 | id: 'first-name', 133 | }, 134 | class: ['form-control'], 135 | on: ['input'], 136 | }, 137 | }, 138 | { 139 | component: 'small', 140 | fieldOptions: { 141 | class: ['text-muted'], 142 | domProps: { 143 | innerHTML: 'Please enter your first name', 144 | }, 145 | }, 146 | }, 147 | ], 148 | }, 149 | // Validation messages for firstName 150 | { 151 | component: 'transition', 152 | fieldOptions: { 153 | props: { 154 | name: 'fade', 155 | }, 156 | }, 157 | children: [ 158 | { 159 | component: 'div', 160 | model: 'user.firstName', 161 | errorHandler: true, 162 | displayOptions: { 163 | model: 'user.firstName', 164 | schema: { 165 | not: { 166 | type: 'string', 167 | }, 168 | }, 169 | }, 170 | fieldOptions: { 171 | class: ['alert alert-danger'], 172 | }, 173 | children: [ 174 | { 175 | component: 'div', 176 | fieldOptions: { 177 | domProps: { 178 | innerHTML: 'This field is required', 179 | }, 180 | }, 181 | }, 182 | ], 183 | }, 184 | ], 185 | }, 186 | // Input for lastName 187 | { 188 | component: 'div', 189 | fieldOptions: { 190 | class: ['form-group'], 191 | }, 192 | children: [ 193 | { 194 | component: 'label', 195 | fieldOptions: { 196 | attrs: { 197 | for: 'last-name', 198 | }, 199 | class: ['font-weight-bold'], 200 | domProps: { 201 | innerHTML: 'Last name', 202 | }, 203 | }, 204 | }, 205 | { 206 | component: 'input', 207 | model: 'user.lastName', 208 | errorOptions: { 209 | class: ['is-invalid'], 210 | }, 211 | fieldOptions: { 212 | attrs: { 213 | id: 'last-name', 214 | }, 215 | class: ['form-control'], 216 | on: ['input'], 217 | }, 218 | }, 219 | { 220 | component: 'small', 221 | fieldOptions: { 222 | class: ['text-muted'], 223 | domProps: { 224 | innerHTML: 'Please enter your last name', 225 | }, 226 | }, 227 | }, 228 | ], 229 | }, 230 | // Validation messages for lastName 231 | { 232 | component: 'transition', 233 | fieldOptions: { 234 | props: { 235 | name: 'fade', 236 | }, 237 | }, 238 | children: [ 239 | { 240 | component: 'div', 241 | model: 'user.lastName', 242 | errorHandler: true, 243 | displayOptions: { 244 | model: 'user.lastName', 245 | schema: { 246 | not: { 247 | type: 'string', 248 | }, 249 | }, 250 | }, 251 | fieldOptions: { 252 | class: ['alert alert-danger'], 253 | }, 254 | children: [ 255 | { 256 | component: 'div', 257 | fieldOptions: { 258 | domProps: { 259 | innerHTML: 'This field is required', 260 | }, 261 | }, 262 | }, 263 | ], 264 | }, 265 | ], 266 | }, 267 | // Input for age 268 | { 269 | component: 'div', 270 | fieldOptions: { 271 | class: ['form-group'], 272 | }, 273 | children: [ 274 | { 275 | component: 'label', 276 | fieldOptions: { 277 | attrs: { 278 | for: 'age', 279 | }, 280 | class: ['font-weight-bold'], 281 | domProps: { 282 | innerHTML: 'Age', 283 | }, 284 | }, 285 | }, 286 | { 287 | component: 'input', 288 | model: 'user.age', 289 | errorOptions: { 290 | class: ['is-invalid'], 291 | }, 292 | fieldOptions: { 293 | attrs: { 294 | id: 'age', 295 | type: 'number', 296 | min: 0, 297 | }, 298 | class: ['form-control'], 299 | on: ['input'], 300 | }, 301 | }, 302 | { 303 | component: 'small', 304 | fieldOptions: { 305 | class: ['text-muted'], 306 | domProps: { 307 | innerHTML: 'Please confirm that you are over 18 years of age', 308 | }, 309 | }, 310 | }, 311 | ], 312 | }, 313 | // Validation messages for age 314 | { 315 | component: 'transition', 316 | fieldOptions: { 317 | props: { 318 | name: 'fade', 319 | mode: 'out-in', 320 | }, 321 | }, 322 | children: [ 323 | // Validation message shown when value is empty 324 | { 325 | component: 'div', 326 | model: 'user.age', 327 | errorHandler: true, 328 | displayOptions: { 329 | model: 'user.age', 330 | schema: { 331 | not: { 332 | type: 'number', 333 | }, 334 | }, 335 | }, 336 | fieldOptions: { 337 | class: ['alert alert-danger'], 338 | }, 339 | children: [ 340 | { 341 | component: 'div', 342 | fieldOptions: { 343 | domProps: { 344 | innerHTML: 'This field is required', 345 | }, 346 | }, 347 | }, 348 | ], 349 | }, 350 | // Validation message when value < 18 351 | { 352 | component: 'div', 353 | model: 'user.age', 354 | errorHandler: true, 355 | displayOptions: { 356 | model: 'user.age', 357 | schema: { 358 | type: 'number', 359 | not: { 360 | minimum: 18, 361 | }, 362 | }, 363 | }, 364 | fieldOptions: { 365 | class: ['alert alert-danger'], 366 | }, 367 | children: [ 368 | { 369 | component: 'div', 370 | fieldOptions: { 371 | domProps: { 372 | innerHTML: 'You must be 18 or older to submit this form', 373 | }, 374 | }, 375 | }, 376 | ], 377 | }, 378 | ], 379 | }, 380 | { 381 | component: 'div', 382 | fieldOptions: { 383 | class: ['form-group'], 384 | }, 385 | children: [ 386 | { 387 | component: 'div', 388 | fieldOptions: { 389 | class: ['font-weight-bold'], 390 | domProps: { 391 | innerHTML: 'Message (optional)', 392 | }, 393 | }, 394 | }, 395 | { 396 | component: 'textarea', 397 | model: 'user.message', 398 | fieldOptions: { 399 | attrs: { 400 | placeholder: 'Type a message here...', 401 | }, 402 | class: ['form-control'], 403 | on: ['input'], 404 | }, 405 | }, 406 | ], 407 | }, 408 | // Radio buttons for consent 409 | { 410 | component: 'div', 411 | fieldOptions: { 412 | class: ['form-group'], 413 | }, 414 | children: [ 415 | { 416 | component: 'div', 417 | fieldOptions: { 418 | class: ['font-weight-bold'], 419 | domProps: { 420 | innerHTML: 'Terms and conditions', 421 | }, 422 | }, 423 | }, 424 | { 425 | component: 'div', 426 | children: [ 427 | { 428 | component: 'span', 429 | fieldOptions: { 430 | domProps: { 431 | innerHTML: 'Please acknowledge that you have read and accept our ', 432 | }, 433 | }, 434 | }, 435 | { 436 | component: 'a', 437 | fieldOptions: { 438 | attrs: { 439 | href: '#', 440 | }, 441 | domProps: { 442 | innerHTML: 'Terms and conditions', 443 | }, 444 | }, 445 | }, 446 | ], 447 | }, 448 | // "Yes" radio button 449 | { 450 | component: 'div', 451 | fieldOptions: { 452 | class: ['form-check'], 453 | }, 454 | children: [ 455 | { 456 | component: 'input', 457 | model: 'user.consent', 458 | errorOptions: { 459 | class: ['is-invalid'], 460 | }, 461 | fieldOptions: { 462 | class: ['form-check-input'], 463 | on: 'change', 464 | attrs: { 465 | id: 'consent-yes', 466 | name: 'name', 467 | type: 'radio', 468 | }, 469 | domProps: { 470 | value: true, 471 | }, 472 | }, 473 | }, 474 | { 475 | component: 'label', 476 | fieldOptions: { 477 | attrs: { 478 | for: 'consent-yes', 479 | }, 480 | class: ['form-check-label'], 481 | domProps: { 482 | innerHTML: 'Yes, I agree', 483 | }, 484 | }, 485 | }, 486 | ], 487 | }, 488 | // "No" radio button 489 | { 490 | component: 'div', 491 | fieldOptions: { 492 | class: ['form-check'], 493 | }, 494 | children: [ 495 | { 496 | component: 'input', 497 | model: 'user.consent', 498 | errorOptions: { 499 | class: ['is-invalid'], 500 | }, 501 | fieldOptions: { 502 | class: ['form-check-input'], 503 | on: 'change', 504 | attrs: { 505 | id: 'consent-no', 506 | name: 'name', 507 | type: 'radio', 508 | }, 509 | domProps: { 510 | value: false, 511 | }, 512 | }, 513 | }, 514 | { 515 | component: 'label', 516 | fieldOptions: { 517 | attrs: { 518 | for: 'consent-no', 519 | }, 520 | class: ['form-check-label'], 521 | domProps: { 522 | innerHTML: 'No, I do not agree', 523 | }, 524 | }, 525 | }, 526 | ], 527 | }, 528 | ], 529 | }, 530 | // Validation messages for consent 531 | { 532 | component: 'transition', 533 | fieldOptions: { 534 | props: { 535 | name: 'fade', 536 | mode: 'out-in', 537 | }, 538 | }, 539 | children: [ 540 | // Validation message shown when an input has not been selected 541 | { 542 | component: 'div', 543 | model: 'user.consent', 544 | errorHandler: true, 545 | displayOptions: { 546 | model: 'user.consent', 547 | schema: { 548 | not: { 549 | type: 'boolean', 550 | }, 551 | }, 552 | }, 553 | fieldOptions: { 554 | class: ['alert alert-danger'], 555 | }, 556 | children: [ 557 | { 558 | component: 'div', 559 | fieldOptions: { 560 | domProps: { 561 | innerHTML: 'This field is required', 562 | }, 563 | }, 564 | }, 565 | ], 566 | }, 567 | // Validation message shown when the "No" input has been selected 568 | { 569 | component: 'div', 570 | model: 'user.consent', 571 | errorHandler: true, 572 | displayOptions: { 573 | model: 'user.consent', 574 | schema: { 575 | const: false, 576 | }, 577 | }, 578 | fieldOptions: { 579 | class: ['alert alert-danger'], 580 | }, 581 | children: [ 582 | { 583 | component: 'div', 584 | fieldOptions: { 585 | domProps: { 586 | innerHTML: 587 | 'You must consent to our terms and conditions to submit this form.', 588 | }, 589 | }, 590 | }, 591 | ], 592 | }, 593 | ], 594 | }, 595 | ], 596 | }; 597 | }, 598 | methods: { 599 | onChange(value) { 600 | this.model = value; 601 | }, 602 | onChangeState(value) { 603 | this.state = value; 604 | }, 605 | onValidated(value) { 606 | this.valid = value; 607 | }, 608 | onSubmit(e) { 609 | e.preventDefault(); 610 | 611 | this.submitted = true; 612 | this.options = { 613 | ...this.options, 614 | showValidationErrors: true, 615 | }; 616 | 617 | if (this.valid) { 618 | this.success = true; 619 | } 620 | }, 621 | }, 622 | }); 623 | 624 | window.Vue.config.productionTip = false; 625 | 626 | /* eslint-disable no-new */ 627 | new window.Vue({ 628 | el: '#app', 629 | template: '', 630 | }); 631 | -------------------------------------------------------------------------------- /examples/example-7/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Example Seven 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/example-7/style.css: -------------------------------------------------------------------------------- 1 | .fade-enter-active, .fade-leave-active { 2 | transition: opacity .5s; 3 | } 4 | .fade-enter, .fade-leave-to { 5 | opacity: 0; 6 | } 7 | -------------------------------------------------------------------------------- /examples/example-8/example-eight.js: -------------------------------------------------------------------------------- 1 | const template = ` 2 |
3 |

vue-form-json-schema

4 |

5 | Example #8 6 | Registration form example using Vue's composition API 7 |

8 | 9 |
10 | 19 | 20 |
21 | 22 |
23 | 24 | 25 |
Form submitted successfully!
30 |
Form has errors, please fix and resubmit!
40 |
Form errors have been corrected. You can now resubmit the form.
50 |
51 |
52 | 53 |
54 | 55 |

Valid

56 |
{{mixinState.valid}}
57 | 58 |

Model

59 | 60 | 61 |

Options

62 | 63 | 64 |

Schema

65 | 66 | 67 |

UI Schema

68 | 69 | 70 |

State

71 | 72 |
73 | `; 74 | 75 | window.Vue.use(window.vueCompositionApi.default); 76 | 77 | window.Vue.component('example-eight', { 78 | name: 'example-eight', 79 | template, 80 | setup() { 81 | const { mixinState } = window.useFormFields(); 82 | 83 | function onChangeState(value) { 84 | mixinState.state = value; 85 | } 86 | 87 | function getDataFromAPI() { 88 | return new Promise((resolve, reject) => { 89 | setTimeout(() => { 90 | resolve({ 91 | firstName: 'Firstname', 92 | lastName: 'Lastname', 93 | age: 18, 94 | }); 95 | }, 1000); 96 | }); 97 | } 98 | 99 | function onValidated(value) { 100 | mixinState.valid = value; 101 | } 102 | 103 | function onSubmit(e) { 104 | e.preventDefault(); 105 | 106 | mixinState.submitted = true; 107 | mixinState.options = { 108 | ...mixinState.options, 109 | showValidationErrors: true, 110 | }; 111 | 112 | if (mixinState.valid) { 113 | mixinState.success = true; 114 | } 115 | } 116 | 117 | window.vueCompositionApi.onMounted(() => { 118 | getDataFromAPI().then((response) => { 119 | mixinState.model = { ...response }; 120 | }); 121 | }); 122 | 123 | return { 124 | mixinState, 125 | onChangeState, 126 | onValidated, 127 | onSubmit, 128 | }; 129 | }, 130 | }); 131 | 132 | window.Vue.config.productionTip = false; 133 | 134 | /* eslint-disable no-new */ 135 | new window.Vue({ 136 | el: '#app', 137 | template: '', 138 | }); 139 | -------------------------------------------------------------------------------- /examples/example-8/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Example Eight 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/example-8/style.css: -------------------------------------------------------------------------------- 1 | .fade-enter-active, .fade-leave-active { 2 | transition: opacity .5s; 3 | } 4 | .fade-enter, .fade-leave-to { 5 | opacity: 0; 6 | } 7 | -------------------------------------------------------------------------------- /examples/example-8/use-form-fields.js: -------------------------------------------------------------------------------- 1 | window.useFormFields = () => { 2 | const mixinState = window.vueCompositionApi.reactive({ 3 | model: {}, 4 | state: {}, 5 | valid: false, 6 | options: { 7 | castToSchemaType: true, 8 | }, 9 | submitted: false, 10 | success: false, 11 | schema: { 12 | type: 'object', 13 | required: ['firstName', 'lastName', 'age'], 14 | properties: { 15 | firstName: { 16 | type: 'string', 17 | }, 18 | lastName: { 19 | type: 'string', 20 | }, 21 | age: { 22 | type: 'number', 23 | minimum: 18, 24 | }, 25 | }, 26 | }, 27 | uiSchema: [ 28 | // Input for firstName 29 | { 30 | component: 'div', 31 | fieldOptions: { 32 | class: ['form-group'], 33 | }, 34 | children: [ 35 | { 36 | component: 'label', 37 | fieldOptions: { 38 | attrs: { 39 | for: 'first-name', 40 | }, 41 | class: ['font-weight-bold'], 42 | domProps: { 43 | innerHTML: 'First name', 44 | }, 45 | }, 46 | }, 47 | { 48 | component: 'input', 49 | model: 'firstName', 50 | errorOptions: { 51 | class: ['is-invalid'], 52 | }, 53 | fieldOptions: { 54 | attrs: { 55 | id: 'first-name', 56 | }, 57 | class: ['form-control'], 58 | on: ['input'], 59 | }, 60 | }, 61 | { 62 | component: 'small', 63 | fieldOptions: { 64 | class: ['text-muted'], 65 | domProps: { 66 | innerHTML: 'Please enter your first name', 67 | }, 68 | }, 69 | }, 70 | ], 71 | }, 72 | // Validation messages for firstName 73 | { 74 | component: 'transition', 75 | fieldOptions: { 76 | props: { 77 | name: 'fade', 78 | }, 79 | }, 80 | children: [ 81 | { 82 | component: 'div', 83 | model: 'firstName', 84 | errorHandler: true, 85 | displayOptions: { 86 | model: 'firstName', 87 | schema: { 88 | not: { 89 | type: 'string', 90 | }, 91 | }, 92 | }, 93 | fieldOptions: { 94 | class: ['alert alert-danger'], 95 | }, 96 | children: [ 97 | { 98 | component: 'div', 99 | fieldOptions: { 100 | domProps: { 101 | innerHTML: 'This field is required', 102 | }, 103 | }, 104 | }, 105 | ], 106 | }, 107 | ], 108 | }, 109 | // Input for lastName 110 | { 111 | component: 'div', 112 | fieldOptions: { 113 | class: ['form-group'], 114 | }, 115 | children: [ 116 | { 117 | component: 'label', 118 | fieldOptions: { 119 | attrs: { 120 | for: 'last-name', 121 | }, 122 | class: ['font-weight-bold'], 123 | domProps: { 124 | innerHTML: 'Last name', 125 | }, 126 | }, 127 | }, 128 | { 129 | component: 'input', 130 | model: 'lastName', 131 | errorOptions: { 132 | class: ['is-invalid'], 133 | }, 134 | fieldOptions: { 135 | attrs: { 136 | id: 'last-name', 137 | }, 138 | class: ['form-control'], 139 | on: ['input'], 140 | }, 141 | }, 142 | { 143 | component: 'small', 144 | fieldOptions: { 145 | class: ['text-muted'], 146 | domProps: { 147 | innerHTML: 'Please enter your last name', 148 | }, 149 | }, 150 | }, 151 | ], 152 | }, 153 | // Validation messages for lastName 154 | { 155 | component: 'transition', 156 | fieldOptions: { 157 | props: { 158 | name: 'fade', 159 | }, 160 | }, 161 | children: [ 162 | { 163 | component: 'div', 164 | model: 'lastName', 165 | errorHandler: true, 166 | displayOptions: { 167 | model: 'lastName', 168 | schema: { 169 | not: { 170 | type: 'string', 171 | }, 172 | }, 173 | }, 174 | fieldOptions: { 175 | class: ['alert alert-danger'], 176 | }, 177 | children: [ 178 | { 179 | component: 'div', 180 | fieldOptions: { 181 | domProps: { 182 | innerHTML: 'This field is required', 183 | }, 184 | }, 185 | }, 186 | ], 187 | }, 188 | ], 189 | }, 190 | // Input for age 191 | { 192 | component: 'div', 193 | fieldOptions: { 194 | class: ['form-group'], 195 | }, 196 | children: [ 197 | { 198 | component: 'label', 199 | fieldOptions: { 200 | attrs: { 201 | for: 'age', 202 | }, 203 | class: ['font-weight-bold'], 204 | domProps: { 205 | innerHTML: 'Age', 206 | }, 207 | }, 208 | }, 209 | { 210 | component: 'input', 211 | model: 'age', 212 | errorOptions: { 213 | class: ['is-invalid'], 214 | }, 215 | fieldOptions: { 216 | attrs: { 217 | id: 'age', 218 | type: 'number', 219 | min: 0, 220 | }, 221 | class: ['form-control'], 222 | on: ['input'], 223 | }, 224 | }, 225 | { 226 | component: 'small', 227 | fieldOptions: { 228 | class: ['text-muted'], 229 | domProps: { 230 | innerHTML: 'Please confirm that you are over 18 years of age', 231 | }, 232 | }, 233 | }, 234 | ], 235 | }, 236 | // Validation messages for age 237 | { 238 | component: 'transition', 239 | fieldOptions: { 240 | props: { 241 | name: 'fade', 242 | mode: 'out-in', 243 | }, 244 | }, 245 | children: [ 246 | // Validation message shown when value is empty 247 | { 248 | component: 'div', 249 | model: 'age', 250 | errorHandler: true, 251 | displayOptions: { 252 | model: 'age', 253 | schema: { 254 | not: { 255 | type: 'number', 256 | }, 257 | }, 258 | }, 259 | fieldOptions: { 260 | class: ['alert alert-danger'], 261 | }, 262 | children: [ 263 | { 264 | component: 'div', 265 | fieldOptions: { 266 | domProps: { 267 | innerHTML: 'This field is required', 268 | }, 269 | }, 270 | }, 271 | ], 272 | }, 273 | // Validation message when value < 18 274 | { 275 | component: 'div', 276 | model: 'age', 277 | errorHandler: true, 278 | displayOptions: { 279 | model: 'age', 280 | schema: { 281 | type: 'number', 282 | not: { 283 | minimum: 18, 284 | }, 285 | }, 286 | }, 287 | fieldOptions: { 288 | class: ['alert alert-danger'], 289 | }, 290 | children: [ 291 | { 292 | component: 'div', 293 | fieldOptions: { 294 | domProps: { 295 | innerHTML: 'You must be 18 or older to submit this form', 296 | }, 297 | }, 298 | }, 299 | ], 300 | }, 301 | ], 302 | }, 303 | { 304 | component: 'div', 305 | fieldOptions: { 306 | class: ['form-group'], 307 | }, 308 | children: [ 309 | { 310 | component: 'div', 311 | fieldOptions: { 312 | class: ['font-weight-bold'], 313 | domProps: { 314 | innerHTML: 'Message (optional)', 315 | }, 316 | }, 317 | }, 318 | { 319 | component: 'textarea', 320 | model: 'message', 321 | fieldOptions: { 322 | attrs: { 323 | placeholder: 'Type a message here...', 324 | }, 325 | class: ['form-control'], 326 | on: ['input'], 327 | }, 328 | }, 329 | ], 330 | }, 331 | ], 332 | }); 333 | 334 | return { mixinState }; 335 | }; 336 | -------------------------------------------------------------------------------- /examples/helpers/pretty-print.css: -------------------------------------------------------------------------------- 1 | pre { 2 | outline: 1px solid #ccc; 3 | padding: 5px; 4 | margin: 5px; 5 | } 6 | 7 | .string { 8 | color: #e91e63; 9 | } 10 | 11 | .number { 12 | color: #00bcd4; 13 | } 14 | 15 | .boolean { 16 | color: #ff00ff; 17 | } 18 | 19 | .null { 20 | color: #9e9e9e; 21 | } 22 | 23 | .key { 24 | color: #3753a9; 25 | } 26 | -------------------------------------------------------------------------------- /examples/helpers/pretty-print.js: -------------------------------------------------------------------------------- 1 | window.Vue.component('pretty-print', { 2 | name: 'JsonPrettyPrint', 3 | template: '
',
 4 |   props: {
 5 |     value: {
 6 |       type: null,
 7 |     },
 8 |   },
 9 |   methods: {
10 |     prettyPrint(json) {
11 |       if (json) {
12 |         const stringified = JSON.stringify(json, null, 2);
13 |         const stringifiedReplaced = stringified.replace(/&/g, '&').replace(//g, '>');
14 |         const regex = /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g;
15 | 
16 |         return stringifiedReplaced.replace(regex, (match) => {
17 |           let className = 'number';
18 | 
19 |           if (/^"/.test(match)) {
20 |             if (/:$/.test(match)) {
21 |               className = 'key';
22 |             } else {
23 |               className = 'string';
24 |             }
25 |           } else if (/true|false/.test(match)) {
26 |             className = 'boolean';
27 |           } else if (/null/.test(match)) {
28 |             className = 'null';
29 |           }
30 | 
31 |           return `${match}`;
32 |         });
33 |       }
34 | 
35 |       return '';
36 |     },
37 |   },
38 | });
39 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "vue-form-json-schema",
 3 |   "version": "2.9.5",
 4 |   "description": "Vue package for using forms with JSON schema",
 5 |   "main": "dist/vue-form-json-schema.umd.js",
 6 |   "module": "dist/vue-form-json-schema.esm.js",
 7 |   "scripts": {
 8 |     "build": "NODE_ENV=production webpack -p --config webpack.config.prod.js",
 9 |     "dev": "NODE_ENV=development webpack --config webpack.config.dev.js --watch",
10 |     "lint": "eslint src",
11 |     "prepublish": "npm run build",
12 |     "test": "echo \"Error: no test specified\" && exit 1"
13 |   },
14 |   "author": "Tobias Järvelöv ",
15 |   "license": "MIT",
16 |   "dependencies": {
17 |     "ajv": "^6.12.6",
18 |     "lodash": "^4.17.20"
19 |   },
20 |   "devDependencies": {
21 |     "@babel/core": "^7.2.2",
22 |     "@babel/preset-env": "^7.2.3",
23 |     "babel-eslint": "^10.0.1",
24 |     "babel-loader": "^8.0.4",
25 |     "babel-plugin-lodash": "^3.3.4",
26 |     "eslint": "^5.11.0",
27 |     "eslint-config-airbnb": "^17.1.0",
28 |     "eslint-plugin-import": "^2.14.0",
29 |     "lodash-webpack-plugin": "^0.11.5",
30 |     "vue": "^2.6.11",
31 |     "webpack": "^4.28.2",
32 |     "webpack-cli": "^3.1.2",
33 |     "webpack-merge": "^4.1.5",
34 |     "webpack-node-externals": "^1.7.2"
35 |   },
36 |   "peerDependencies": {
37 |     "vue": "^2.x"
38 |   }
39 | }
40 | 


--------------------------------------------------------------------------------
/src/constants/index.js:
--------------------------------------------------------------------------------
 1 | export const VFJS_EVENT_FIELD_STATE_UPDATE = 'VFJS_EVENT_FIELD_STATE_UPDATE';
 2 | export const VFJS_EVENT_FIELD_MODEL_VALIDATE = 'VFJS_EVENT_FIELD_MODEL_VALIDATE';
 3 | export const VFJS_EVENT_FIELD_MODEL_UPDATE = 'VFJS_EVENT_FIELD_MODEL_UPDATE';
 4 | export const VFJS_EVENT_FIELD_MODEL_CLEAR_HIDDEN = 'VFJS_EVENT_FIELD_MODEL_CLEAR_HIDDEN';
 5 | export const VFJS_EVENT_FIELD_MODELS_VALIDATE = 'VFJS_EVENT_FIELD_MODELS_VALIDATE';
 6 | export const VFJS_EVENT_MODEL_UPDATE = 'VFJS_EVENT_MODEL_UPDATE';
 7 | export const VFJS_EVENT_MODEL_UPDATED = 'VFJS_EVENT_MODEL_UPDATED';
 8 | export const VFJS_EVENT_MODEL_VALIDATE = 'VFJS_EVENT_MODEL_VALIDATE';
 9 | export const VFJS_EVENT_STATE_UPDATE = 'VFJS_EVENT_STATE_UPDATE';
10 | export const VFJS_EVENT_STATE_UPDATED = 'VFJS_EVENT_STATE_UPDATED';
11 | export const VFJS_EVENT_UI_FIELDS_UPDATE = 'VFJS_EVENT_UI_FIELDS_UPDATE';
12 | export const VFJS_EXTERNAL_EVENT_CHANGE = 'change';
13 | export const VFJS_EXTERNAL_EVENT_STATE_CHANGE = 'state-change';
14 | export const VFJS_EXTERNAL_EVENT_VALIDATED = 'validated';
15 | export const VFJS_EXTERNAL_MODEL_PROP = 'model';
16 | 


--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
 1 | import vfjsField from './vfjs-field-component';
 2 | import vfjsFieldMixin from './vfjs-field-mixin';
 3 | import vfjsGlobalMixin from './vfjs-global-mixin';
 4 | import vfjsGlobal from './vfjs-global-component';
 5 | import vfjsPlugin from './plugin';
 6 | 
 7 | export {
 8 |   vfjsField,
 9 |   vfjsFieldMixin,
10 |   vfjsGlobal,
11 |   vfjsGlobalMixin,
12 |   vfjsPlugin,
13 | };
14 | 
15 | export default vfjsGlobal;
16 | 


--------------------------------------------------------------------------------
/src/plugin.js:
--------------------------------------------------------------------------------
 1 | import vfjsGlobalComponent from './vfjs-global-component';
 2 | 
 3 | // Declare install function executed by Vue.use()
 4 | export function install(
 5 |   Vue,
 6 |   options = {
 7 |     component: 'vue-form-json-schema',
 8 |   },
 9 | ) {
10 |   if (!install.installed) {
11 |     install.installed = true;
12 |     Vue.component(options.component, vfjsGlobalComponent);
13 |   }
14 | }
15 | 
16 | // Create module definition for Vue.use()
17 | const plugin = {
18 |   install,
19 | };
20 | 
21 | // Auto-install when vue is found (eg. in browser via