Skip to content

Instantly share code, notes, and snippets.

@doncatnip
Created September 20, 2019 12:56
Show Gist options
  • Save doncatnip/58b35b1576143d203c1daa05d166ecde to your computer and use it in GitHub Desktop.
Save doncatnip/58b35b1576143d203c1daa05d166ecde to your computer and use it in GitHub Desktop.
A json schema validation wrapper for vue composition API
import _ from 'lodash'
import {reactive, ref} from '@vue/composition-api'
import Ajv from 'ajv'
const ajv = new Ajv({allErrors:true})
/***
* TODO: proper documentation including nested properties and arrays
*
* json schema validation
*
* usage:
* const {$v, validate, revalidate} = useValidation(schema, messages)
*
* schema is an official json schema draft-7
* messages can contain associated custom error messages
* if messages are ommitted, default json schema messages are used instead
*
* example:
* const {...} = useValidation
* ( { required: ['id']
* , properties:
* { id: {type: 'number'}
* }
* }
* , { required:
* { id: 'Please provide an ID'
* }
* , properties:
* { id: {type: 'ID must be a number.'}
}
* }
* )
*
******/
const createReactiveValidity = properties => {
const validity = {'$isInvalid':undefined}
let state
for (const [key,value] of Object.entries(properties)) {
if (value.type=='array') {
state = idx => {
if (validity[key].arrayState[idx]===undefined)
validity[key].arrayState[idx] = createReactiveValidity(value.items.properties)
return validity[key].arrayState[idx]
}
state.arrayState = {}
} else state = {state: undefined, feedback: undefined}
validity[key] = state
}
return reactive(validity)
}
const clearErrorFeedback = validity => {
for (const [key,err] of Object.entries(validity)) {
if (key==='$isInvalid') {
validity.$isInvalid = undefined
continue
}
if (err.arrayState) {
for (const subValidity of Object.values(err.arrayState))
clearErrorFeedback(subValidity)
continue
}
err.state = undefined
err.feedback = undefined
}
}
const unsetEmptyFields = data => {
for (const [key,value] of Object.entries(data)) {
if (Array.isArray(value)) {
for (const subData of value)
if (_.isObject(subData))
unsetEmptyFields(subData)
continue
}
if (!value)
data[key] = undefined
}
}
const ARRAY_PROPERTY = /^(.*)\[([0-9]+)\]$/
export const useValidation = (schema,messages) => {
//compile json schema
const validateSchema = ajv.compile(schema)
//create reactive state for fields and nested fields recursively
const $v = createReactiveValidity(schema.properties)
const validate = (data,options={}) => {
data = _.cloneDeep(data)
if (options.invalidOnly && $v.$isInvalid!==true)
return;
//clear current error feedback recursively
clearErrorFeedback($v)
//set empty fields to undefined recursively
//so that 'required' validators trigger
unsetEmptyFields(data)
const valid = validateSchema(data)
if (!valid) {
console.log('errors', validateSchema.errors)
let invalidLayers = new Set([$v])
for (const err of validateSchema.errors) {
let feedback = messages
let path = err.dataPath.split('.')
let validityLayer = $v
for (const p of path) {
if (!p) continue
let m = p.match(ARRAY_PROPERTY)
if (m&&m.length==3) {
validityLayer = validityLayer[m[1]](Number(m[2]))
invalidLayers.add(validityLayer)
feedback = _.has(feedback, ['properties', m[1], 'items'])
&&feedback.properties[m[1]].items
} else {
validityLayer = validityLayer[p]
feedback = _.has(feedback, ['properties',p])
&&feedback.properties[p]
}
}
if (err.keyword=='required') {
validityLayer = validityLayer[err.params.missingProperty]
feedback = _.has(feedback,['required',err.params.missingProperty])
&&feedback.required[err.params.missingProperty]
} else
feedback = _.has(feedback,err.keyword)&&feedback[err.keyword]
validityLayer.state = false
validityLayer.feedback = feedback||err.message
}
for (const layer of invalidLayers)
layer.$isInvalid = true
}
return valid
}
const revalidate = data => validate(data,{invalidOnly:true})
return {$v, validate, revalidate}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment