import React, { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import Ajv from 'ajv'
import JsonSchemaDraft04 from 'ajv/lib/refs/json-schema-draft-04.json'
import { warning } from '../../utils/log'
const FormContext = React.createContext(null)
/**
* Context for handling a JSONSchema form.
*
* @component
*/
const JsonSchemaForm = ({
schema,
onChange,
renderLabel,
enumSelectThreshold,
initialValues,
initialValid,
children,
}) => {
const [currentSchema, setCurrentSchema] = useState(null)
const [ajv, setAjv] = useState(null)
const [errors, setErrors] = useState({})
const [values, setValues] = useState(initialValues)
const [isValid, setIsValid] = useState(initialValid)
const validate = newValues => {
if (!ajv) {
setErrors({})
return true
}
// validate against schema
const ajvValidate = ajv.getSchema('schema')
// sometimes the schema loading does not work or is not finished
// before the first edit, this is to prevent crashes
if (ajvValidate) {
const newIsValid = ajvValidate(newValues)
setIsValid(newIsValid)
if (newIsValid) {
setErrors({})
} else {
const newErrors = {}
ajvValidate.errors.forEach(error => {
const field = error.dataPath.substring(1)
if (newErrors[field]) {
newErrors[field].push(error.message)
} else {
newErrors[field] = [error.message]
}
})
setErrors(newErrors)
}
return newIsValid
}
return initialValid
}
const changeHandler = ({ name, value }) => {
const newValues = { ...values, [name]: value }
setValues(newValues)
const newIsValid = validate(newValues)
onChange({ values: newValues, isValid: newIsValid })
}
const getErrorsForField = fieldName => {
return errors[fieldName] || []
}
// Set unchanged values when initialValues changed.
useEffect(() => {
const newValues = { ...initialValues, ...values }
setValues(newValues)
const newIsValid = validate(newValues)
onChange({ values: newValues, isValid: newIsValid })
}, [initialValues])
// Initialize/update AJV instance when schema has changed.
useEffect(() => {
const modifiedSchema = () => {
const objectSchema = { ...schema }
Object.keys(objectSchema.properties).forEach(property => {
if (
objectSchema.properties[property].type === 'media' ||
objectSchema.properties[property].type === 'json_schema_object'
) {
objectSchema.properties[property].type = 'object'
}
if (objectSchema.properties[property].format === 'objectid') {
delete objectSchema.properties[property]
} else if (objectSchema.properties[property].nullable) {
// translate nullable field from OpenAPI specification to
// possible type null in jsonschema
objectSchema.properties[property].type = [
'null',
objectSchema.properties[property].type,
]
if ('enum' in objectSchema.properties[property]) {
objectSchema.properties[property].enum.push(null)
}
}
})
return objectSchema
}
// Prevent recreation of validator instance if only schema reference has changed.
if (currentSchema === JSON.stringify(schema)) return
if (ajv) {
warning(
'JsonSchemaForm: It is not recommended to change the schema of a form during its use!'
)
}
if (!schema) {
setAjv(null)
setErrors({})
setCurrentSchema(null)
warning('JsonSchemaForm: It is not recommended to use without a schema!')
return
}
const newAjv = new Ajv({
// Ajv gives a warning that the schema property `$id` is ignored.
// With draft-06, the property `id` changed to `$id` which seems
// to give some trouble with ajv even when using draft-04 only.
// Setting this to auto just prevents those warnings.
// Validation seems to work fine.
schemaId: 'auto',
missingRefs: 'ignore',
errorDataPath: 'property',
allErrors: true,
})
newAjv.addMetaSchema(JsonSchemaDraft04)
newAjv.addSchema(modifiedSchema(), 'schema')
setAjv(newAjv)
setErrors({})
setCurrentSchema(JSON.stringify(schema))
}, [schema])
return (
<FormContext.Provider
value={{
schema,
enumSelectThreshold,
isValid,
values,
onChange: changeHandler,
renderLabel,
getErrorsForField,
}}
>
{children}
</FormContext.Provider>
)
}
JsonSchemaForm.propTypes = {
/** JSON Schema form description object */
schema: PropTypes.object.isRequired,
/** Callback when the values have changed */
onChange: PropTypes.func,
/** Threshold to switch between RadioGroup and Select for enum fields */
enumSelectThreshold: PropTypes.number,
/** Initial values for the form fields */
initialValues: PropTypes.object,
/** Specifies whether the form is initially valid */
initialValid: PropTypes.bool,
/** Components rendered within this context */
children: PropTypes.node.isRequired,
/** Function to render a given label. */
renderLabel: PropTypes.func,
}
JsonSchemaForm.defaultProps = {
enumSelectThreshold: 4,
initialValues: {},
initialValid: true,
onChange: () => {},
renderLabel: label => label,
}
/**
* React hook to access the form context within a custom component.
*/
const useForm = () => React.useContext(FormContext)
export default JsonSchemaForm
export { useForm }
Source