diff --git a/public/index.html b/public/index.html
index b492f61..d2a9770 100644
--- a/public/index.html
+++ b/public/index.html
@@ -95,7 +95,22 @@
target: document.getElementById('testEditorContainer'),
props: {
doc,
- onChangeJson: doc => console.log('onChangeJson', doc)
+ onChangeJson: doc => console.log('onChangeJson', doc),
+ validate: doc => {
+ if (
+ doc && typeof doc === 'object' &&
+ doc.object && typeof doc.object === 'object' &&
+ doc.object.a === 'b') {
+ return [
+ {
+ path: ['object', 'a'],
+ message: '"a" should not be "b" ;)'
+ }
+ ]
+ }
+
+ return []
+ }
}
})
window.testEditor = testEditor // expose to window for debugging
diff --git a/src/components/JSONEditor.svelte b/src/components/JSONEditor.svelte
index 56afb5f..191d91b 100644
--- a/src/components/JSONEditor.svelte
+++ b/src/components/JSONEditor.svelte
@@ -32,13 +32,19 @@
import jump from '../assets/jump.js/src/jump.js'
import { expandPath, syncState, patchProps } from '../logic/documentState.js'
import Menu from './Menu.svelte'
-import { isObjectOrArray } from '../utils/typeUtils.js'
+ import { isObjectOrArray } from '../utils/typeUtils.js'
+ import { mapValidationErrors } from '../logic/validation.js'
let divContents
let domHiddenInput
+ export let validate = () => []
export let onChangeJson = () => {}
+ export function setValidator (newValidate) {
+ validate = newValidate
+ }
+
export let doc = {}
let state = undefined
@@ -46,6 +52,8 @@ import { isObjectOrArray } from '../utils/typeUtils.js'
let clipboard = null
$: state = syncState(doc, state, [], (path) => path.length < 1)
+ $: validationErrorsList = validate(doc)
+ $: validationErrors = mapValidationErrors(validationErrorsList)
let showSearch = false
let searchText = ''
@@ -469,6 +477,7 @@ import { isObjectOrArray } from '../utils/typeUtils.js'
path={[]}
state={state}
searchResult={searchResult && searchResult.itemsWithActive}
+ validationErrors={validationErrors}
onPatch={handlePatch}
onUpdateKey={handleUpdateKey}
onExpand={handleExpand}
diff --git a/src/components/JSONNode.scss b/src/components/JSONNode.scss
index acfaf70..1617f61 100644
--- a/src/components/JSONNode.scss
+++ b/src/components/JSONNode.scss
@@ -235,4 +235,9 @@ div.empty {
&.active {
background-color: $highlight-active-color;
}
-}
\ No newline at end of file
+}
+
+.validation-error {
+ color: $warning-color;
+ padding-left: $input-padding;
+}
diff --git a/src/components/JSONNode.svelte b/src/components/JSONNode.svelte
index df4c1c5..1646d98 100644
--- a/src/components/JSONNode.svelte
+++ b/src/components/JSONNode.svelte
@@ -10,7 +10,8 @@
STATE_PROPS,
STATE_SEARCH_PROPERTY,
STATE_SEARCH_VALUE,
- INDENTATION_WIDTH
+ INDENTATION_WIDTH,
+ VALIDATION_ERROR
} from '../constants.js'
import {
getPlainText,
@@ -21,7 +22,7 @@
setPlainText
} from '../utils/domUtils.js'
import Icon from 'svelte-awesome'
- import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons'
+ import { faCaretDown, faCaretRight, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'
import classnames from 'classnames'
import { findUniqueName } from '../utils/stringUtils.js'
import { isUrl, stringConvert, valueType } from '../utils/typeUtils'
@@ -33,6 +34,7 @@
export let path
export let state
export let searchResult
+ export let validationErrors
export let onPatch
export let onUpdateKey
export let onExpand
@@ -43,6 +45,7 @@
$: expanded = state && state[STATE_EXPANDED]
$: limit = state && state[STATE_LIMIT]
$: props = state && state[STATE_PROPS]
+ $: validationError = validationErrors && validationErrors[VALIDATION_ERROR]
const escapeUnicode = false // TODO: pass via options
@@ -376,6 +379,15 @@
]
{/if}
+ {#if validationError}
+
+
+
+
+ {/if}
{#if expanded}
@@ -386,6 +398,7 @@
path={path.concat(index)}
state={state && state[index]}
searchResult={searchResult ? searchResult[index] : undefined}
+ validationErrors={validationErrors ? validationErrors[index] : undefined}
onPatch={onPatch}
onUpdateKey={handleUpdateKey}
onExpand={onExpand}
@@ -444,6 +457,15 @@
}
{/if}
+ {#if validationError}
+
+
+
+
+ {/if}
{#if expanded}
@@ -454,6 +476,7 @@
path={path.concat(prop.key)}
state={state && state[prop.key]}
searchResult={searchResult ? searchResult[prop.key] : undefined}
+ validationErrors={validationErrors ? validationErrors[prop.key] : undefined}
onPatch={onPatch}
onUpdateKey={handleUpdateKey}
onExpand={onExpand}
@@ -501,6 +524,12 @@
bind:this={domValue}
title={valueIsUrl ? 'Ctrl+Click or Ctrl+Enter to open url in new window' : null}
>
+ {#if validationError}
+
+
+
+
+ {/if}
{/if}
diff --git a/src/constants.js b/src/constants.js
index ec50275..2481eda 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -4,6 +4,7 @@ export const STATE_LIMIT = Symbol('limit')
export const STATE_PROPS = Symbol('props')
export const STATE_SEARCH_PROPERTY = Symbol('search:property')
export const STATE_SEARCH_VALUE = Symbol('search:value')
+export const VALIDATION_ERROR = Symbol('validation:error')
export const SCROLL_DURATION = 300 // ms
export const DEBOUNCE_DELAY = 300
diff --git a/src/logic/validation.js b/src/logic/validation.js
new file mode 100644
index 0000000..5b1ec3d
--- /dev/null
+++ b/src/logic/validation.js
@@ -0,0 +1,41 @@
+import { initial } from 'lodash-es'
+import { VALIDATION_ERROR } from '../constants.js'
+import { existsIn, setIn } from '../utils/immutabilityHelpers.js'
+
+/**
+ * Create a nested map with validation errors,
+ * and also create error messages for the parent nodes of the nodes having an error.
+ *
+ * @param {ValidationError[]} validationErrors
+ * @return {Object. | undefined} Returns a nested object containing
+ */
+export function mapValidationErrors (validationErrors) {
+ let object = undefined
+
+ validationErrors.forEach(validationError => {
+ const errorPath = validationError.path.concat([VALIDATION_ERROR])
+ object = setIn(object, errorPath, validationError, true)
+ })
+
+ // create error entries for all parent nodes
+ validationErrors.forEach(validationError => {
+ const path = validationError.path
+ let parentPath = path
+
+ while (parentPath.length > 0) {
+ parentPath = initial(parentPath)
+
+ const parentErrorPath = parentPath.concat([VALIDATION_ERROR])
+ if (!existsIn(object, parentErrorPath)) {
+ const error = {
+ isChildError: true,
+ path: parentPath,
+ message: 'Contains invalid data'
+ }
+ object = setIn(object, parentErrorPath, error, true)
+ }
+ }
+ })
+
+ return object
+}
diff --git a/src/logic/validation.test.js b/src/logic/validation.test.js
new file mode 100644
index 0000000..cc782b0
--- /dev/null
+++ b/src/logic/validation.test.js
@@ -0,0 +1,59 @@
+import assert from 'assert'
+import { mapValidationErrors } from './validation.js'
+import { VALIDATION_ERROR } from '../constants.js'
+
+describe('validation', () => {
+ it('should turn a list with validation errors into a nested object', () => {
+ const message1 = 'Number expected'
+ const message2 = 'Year in the past expected'
+ const message3 = 'Contains invalid data'
+
+ const error1 = { path: ['pupils', 2, 'age'], message: message1 }
+ const error2 = { path: ['year'], message: message2 }
+
+ const validationErrorsList = [error1, error2]
+
+ const expected = {
+ pupils: [
+ ,
+ ,
+ {
+ age: {
+ [VALIDATION_ERROR]: error1
+ }
+ }
+ ],
+ year: {
+ [VALIDATION_ERROR]: error2
+ }
+ }
+ expected[VALIDATION_ERROR] = { isChildError: true, path: [], message: message3 }
+ expected.pupils[VALIDATION_ERROR] = { isChildError: true, path: ['pupils'], message: message3 }
+ expected.pupils[2][VALIDATION_ERROR] = { isChildError: true, path: ['pupils', 2], message: message3 }
+
+ const actual = mapValidationErrors(validationErrorsList)
+
+ assert.deepStrictEqual(actual, expected)
+ })
+
+ it('should not override a parent error when creating a validation error object', () => {
+ const message1 = 'Year in the past expected'
+ const message2 = 'Missing required property "month"'
+
+ const error1 = { path: ['year'], message: message1 }
+ const error2 = { path: [], message: message2 }
+
+ const validationErrorsList = [error1, error2]
+
+ const expected = {
+ year: {
+ [VALIDATION_ERROR]: error1
+ },
+ [VALIDATION_ERROR]: error2
+ }
+
+ const actual = mapValidationErrors(validationErrorsList)
+
+ assert.deepStrictEqual(actual, expected)
+ })
+})
\ No newline at end of file
diff --git a/src/types.js b/src/types.js
index ecf5040..0aa0e31 100644
--- a/src/types.js
+++ b/src/types.js
@@ -83,3 +83,7 @@
* @property {string} [title=undefined]
* @property {boolean} [default=false]
*/
+
+ /**
+ * @typedef {{path: Path, message: string, isChildError?: boolean}} ValidationError
+ */
\ No newline at end of file