onValidationError option (#854)

* provide onValidationError callback

* linter fixes

* docu fixes

* textmode - invoke callback also when no errors
This to cover situation of changes that fixes validations

* fixed cautom validation example
This commit is contained in:
Meir Rotstein 2019-12-01 17:21:16 +02:00 committed by Jos de Jong
parent 87691e6693
commit 266eeec21a
7 changed files with 242 additions and 4 deletions

View File

@ -163,6 +163,27 @@ Constructs a new JSONEditor.
Also see the option `schema` for JSON schema validation. Also see the option `schema` for JSON schema validation.
- `{function} onValidationError(errors)`
Set a callback function for validation errors. Available in all modes.
On validation of the json, if errors were found this callback is invoked with the validation errors data.
Between validations, the callback will be invoked only if the validation errors were changed.
Example:
```js
var options = {
/**
* @param {Array} errors validation errors
*/
onValidationError: function (errors) {
...
}
}
```
- `{function} onCreateMenu(items, node)` - `{function} onCreateMenu(items, node)`
Customize context menus in tree mode. Customize context menus in tree mode.

View File

@ -0,0 +1,151 @@
<!DOCTYPE HTML>
<html>
<head>
<title>JSONEditor | onValidationError</title>
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
<script src="../dist/jsoneditor.js"></script>
<style type="text/css">
body {
width: 600px;
font: 11pt sans-serif;
}
#jsoneditor {
width: 100%;
height: 500px;
}
#onValidationOutput {
margin: 5px;
}
/* custom bold styling for non-default JSON schema values */
.jsoneditor-is-not-default {
font-weight: bold;
}
</style>
</head>
<body>
<h1>JSON schema validation</h1>
<p>
This example demonstrates onValidationError callback.
</p>
<div id="jsoneditor"></div>
<div id="onValidationOutput"></div>
<script>
var schema = {
"title": "Employee",
"description": "Object containing employee details",
"type": "object",
"properties": {
"firstName": {
"title": "First Name",
"description": "The given name.",
"examples": [
"John"
],
"type": "string"
},
"lastName": {
"title": "Last Name",
"description": "The family name.",
"examples": [
"Smith"
],
"type": "string"
},
"gender": {
"title": "Gender",
"enum": ["male", "female"]
},
"availableToHire": {
"type": "boolean",
"default": false
},
"age": {
"description": "Age in years",
"type": "integer",
"minimum": 0,
"examples": [28, 32]
},
"job": {
"$ref": "job"
}
},
"required": ["firstName", "lastName"]
};
var job = {
"title": "Job description",
"type": "object",
"required": ["address"],
"properties": {
"company": {
"type": "string",
"examples": [
"ACME",
"Dexter Industries"
]
},
"role": {
"description": "Job title.",
"type": "string",
"examples": [
"Human Resources Coordinator",
"Software Developer"
],
"default": "Software Developer"
},
"address": {
"type": "string"
},
"salary": {
"type": "number",
"minimum": 120,
"examples": [100, 110, 120]
}
}
};
var json = {
firstName: 'John',
lastName: 'Doe',
gender: null,
age: "28",
availableToHire: true,
job: {
company: 'freelance',
role: 'developer',
salary: 100
}
};
var options = {
schema: schema,
schemaRefs: {"job": job},
mode: 'code',
modes: ['code', 'text', 'tree', 'preview'],
onValidationError: function(errors) {
console.error('onValidationError', errors);
const outputEL = document.getElementById('onValidationOutput')
outputEL.innerHTML = '<code>onValidationError</code> was called with ' + errors.length + ' errors <br> ' +
'open the browser console to see the error objects';
},
onValidate: function (json) {
var errors = [];
if(!isNaN(json.age) && json.age < 30) {
errors.push({ path: ['age'], message: 'Member age must be 30 or higher' });
}
return errors;
}
};
// create the editor
var container = document.getElementById('jsoneditor');
var editor = new JSONEditor(container, options, json);
</script>
</body>
</html>

View File

@ -183,7 +183,7 @@ JSONEditor.VALID_OPTIONS = [
'timestampTag', 'timestampTag',
'escapeUnicode', 'history', 'search', 'mode', 'modes', 'name', 'indentation', 'escapeUnicode', 'history', 'search', 'mode', 'modes', 'name', 'indentation',
'sortObjectKeys', 'navigationBar', 'statusBar', 'mainMenuBar', 'languages', 'language', 'enableSort', 'enableTransform', 'sortObjectKeys', 'navigationBar', 'statusBar', 'mainMenuBar', 'languages', 'language', 'enableSort', 'enableTransform',
'maxVisibleChilds' 'maxVisibleChilds', 'onValidationError'
] ]
/** /**

View File

@ -21,7 +21,8 @@ import {
parse, parse,
repair, repair,
sort, sort,
sortObjectKeys sortObjectKeys,
isValidationErrorChanged
} from './util' } from './util'
import { DEFAULT_MODAL_ANCHOR } from './constants' import { DEFAULT_MODAL_ANCHOR } from './constants'
import { tryRequireThemeJsonEditor } from './tryRequireThemeJsonEditor' import { tryRequireThemeJsonEditor } from './tryRequireThemeJsonEditor'
@ -87,6 +88,7 @@ textmode.create = function (container, options = {}) {
this.textarea = undefined // plain text editor (fallback when Ace is not available) this.textarea = undefined // plain text editor (fallback when Ace is not available)
this.validateSchema = null this.validateSchema = null
this.annotations = [] this.annotations = []
this.lastSchemaErrors = undefined
// create a debounced validate function // create a debounced validate function
this._debouncedValidate = debounce(this.validate.bind(this), this.DEBOUNCE_INTERVAL) this._debouncedValidate = debounce(this.validate.bind(this), this.DEBOUNCE_INTERVAL)
@ -790,12 +792,19 @@ textmode.validate = function () {
if (seq === me.validationSequence) { if (seq === me.validationSequence) {
const errors = schemaErrors.concat(parseErrors).concat(customValidationErrors) const errors = schemaErrors.concat(parseErrors).concat(customValidationErrors)
me._renderErrors(errors) me._renderErrors(errors)
if (typeof this.options.onValidationError === 'function') {
if (isValidationErrorChanged(errors, this.lastSchemaErrors)) {
this.options.onValidationError.call(this, errors)
}
this.lastSchemaErrors = errors
}
} }
}) })
.catch(err => { .catch(err => {
console.error('Custom validation function did throw an error', err) console.error('Custom validation function did throw an error', err)
}) })
} catch (err) { } catch (err) {
this.lastSchemaErrors = undefined
if (this.getText()) { if (this.getText()) {
// try to extract the line number from the jsonlint error message // try to extract the line number from the jsonlint error message
const match = /\w*line\s*(\d+)\w*/g.exec(err.message) const match = /\w*line\s*(\d+)\w*/g.exec(err.message)

View File

@ -24,7 +24,8 @@ import {
removeEventListener, removeEventListener,
repair, repair,
selectContentEditable, selectContentEditable,
setSelectionOffset setSelectionOffset,
isValidationErrorChanged
} from './util' } from './util'
import { autocomplete } from './autocomplete' import { autocomplete } from './autocomplete'
import { setLanguage, setLanguages, translate } from './i18n' import { setLanguage, setLanguages, translate } from './i18n'
@ -52,6 +53,7 @@ treemode.create = function (container, options) {
this.validateSchema = null // will be set in .setSchema(schema) this.validateSchema = null // will be set in .setSchema(schema)
this.validationSequence = 0 this.validationSequence = 0
this.errorNodes = [] this.errorNodes = []
this.lastSchemaErrors = undefined
this.node = null this.node = null
this.focusTarget = null this.focusTarget = null
@ -581,6 +583,12 @@ treemode.validate = function () {
if (seq === me.validationSequence) { if (seq === me.validationSequence) {
const errorNodes = [].concat(schemaErrors, customValidationErrors || []) const errorNodes = [].concat(schemaErrors, customValidationErrors || [])
me._renderValidationErrors(errorNodes) me._renderValidationErrors(errorNodes)
if (typeof this.options.onValidationError === 'function') {
if (isValidationErrorChanged(errorNodes, this.lastSchemaErrors)) {
this.options.onValidationError.call(this, errorNodes)
}
this.lastSchemaErrors = errorNodes
}
} }
}) })
.catch(err => { .catch(err => {

View File

@ -1453,6 +1453,26 @@ export function contains (array, item) {
return array.indexOf(item) !== -1 return array.indexOf(item) !== -1
} }
/**
* Checkes if validation has changed from the previous execution
* @param {Array} currErr current validation errors
* @param {Array} prevErr previous validation errors
*/
export function isValidationErrorChanged (currErr, prevErr) {
if (!prevErr && !currErr) { return false }
if ((prevErr && !currErr) || (!prevErr && currErr)) { return true }
if (prevErr.length !== currErr.length) { return true }
for (let i = 0; i < currErr.length; ++i) {
const pErr = prevErr.find(p => p.dataPath === currErr[i].dataPath && p.schemaPath === currErr[i].schemaPath)
if (!pErr) {
return true
}
}
return false
}
function hasOwnProperty (object, key) { function hasOwnProperty (object, key) {
return Object.prototype.hasOwnProperty.call(object, key) return Object.prototype.hasOwnProperty.call(object, key)
} }

View File

@ -15,7 +15,8 @@ import {
repair, repair,
sort, sort,
sortObjectKeys, sortObjectKeys,
stringifyPath stringifyPath,
isValidationErrorChanged
} from '../src/js/util' } from '../src/js/util'
describe('util', () => { describe('util', () => {
@ -200,6 +201,34 @@ describe('util', () => {
}) })
}) })
describe('isValidationErrorChanged', () => {
const err1 = { keyword: 'enum', dataPath: '.gender', schemaPath: '#/properties/gender/enum', params: { allowedValues: ['male', 'female'] }, message: 'should be equal to one of: "male", "female"', schema: ['male', 'female'], parentSchema: { title: 'Gender', enum: ['male', 'female'] }, data: null, type: 'validation' }
const err2 = { keyword: 'type', dataPath: '.age', schemaPath: '#/properties/age/type', params: { type: 'integer' }, message: 'should be integer', schema: 'integer', parentSchema: { description: 'Age in years', type: 'integer', minimum: 0, examples: [28, 32] }, data: '28', type: 'validation' }
const err3 = { dataPath: '.gender', message: 'Member must be an object with properties "name" and "age"' }
it('empty value for both current and previoues error should return false', () => {
assert.strictEqual(isValidationErrorChanged(), false)
})
it('empty value for one of current and previoues error should return true', () => {
assert.strictEqual(isValidationErrorChanged([err1]), true)
assert.strictEqual(isValidationErrorChanged(undefined, [err1]), true)
})
it('different length of current and previoues errors should return true', () => {
assert.strictEqual(isValidationErrorChanged([err1], []), true)
assert.strictEqual(isValidationErrorChanged([err1], [err1, err2]), true)
})
it('same values for current and previoues errors should return false', () => {
assert.strictEqual(isValidationErrorChanged([err1, err2, err3], [err2, err3, err1]), false)
})
it('different values for current and previoues errors should return true', () => {
assert.strictEqual(isValidationErrorChanged([err1, err2], [err3, err1]), true)
})
})
describe('get', () => { describe('get', () => {
it('should get a nested property from an object', () => { it('should get a nested property from an object', () => {
const obj = { const obj = {