A json pointer and rxjs form manager for React
npm install @tokololo/react-json-ptr-form const {
value,
values,
setValue,
removeValue,
resetValue,
rerefValue,
error,
errors,
errorCount,
touched,
setTouched,
valid,
dirty,
form
} = useJsonPtrForm
defaultValues: Partial
schemaValidator?: {
schema: ISchema,
validator: IJsonPrtFormValidator
},
postValidator?: (
values: T,
errors: { [path: string]: IPrtFormError[] }
) => Promise<{ [path: string]: IPrtFormError[] }>,
options?: {
async?: boolean,
validatePreClean?: CleanOptions
},
deps: DependencyList = []);
The internal dependency list is set as follows:
[
defaultValue,
schemaValidator?.schema,
schemaValidator?.validator,
postValidator,
options,
...deps
]
so please take care to provide static instances between renders, ie by wrapping these values in useMemo, useEffect or useCallback. Refer to the full example at the end.
The parameters are as follows:
interface ISchema {
tag: string
}
The schema validator has the following interface:
interface IJsonPrtFormValidator<
T,
U extends ISchema = ISchema,
V extends IPrtFormError = IPrtFormError> {
addSchema(schema: U): Promise
validateSchema(tag: string, value: T): Promise<{ [ptr: string]: V[] }>;
removeSchema(tag: string): Promise
}validateSchema returns an error object the keys of which are the json pointers of the values that failed validation. For instance if you send in the following value object:
{
my: {
notes: [{
heading: 5,
text: 'some text'
}]
}
}
the error object might look like this:
{
'/my/notes': { type: 'minItems', expected: 4, message: 'Please enter at least 4 items' },
'/my/notes/0/heading': { type: 'invalidType', expected: "string", message: 'Heading must be a string' }
}
postValidator?: (
values: T,
errors: { [path: string]: V[] }
) => Promise<{ [path: string]: V[] }>
The post validator is used for:
- the rare cases in which the schema validator is unable to provide the complex validation needed
- as your main validation entry if you do not want to write a custom schema validator
It receives the values to be validated and the errors from the schema validator and must return the complete error object. The important take-away is that the return error object is indexed by ptr strings exactly in the same format that the schema validator returns it in. Note that if you do not have a schema validator the postValidator will receive an empty error object.
async?: boolean
Set this only in the rare cases of using the form manager without the JsonPtrFormControl renderer. Setting this value introduces an extra render needed to manage input field asynchronous updates which is due to the asynchronous nature of the validation. Using the preferred JsonPtrFormControl renderer takes care of this under the hood in a more performant and flexible way.
validatePreClean?: CleanOptions,
Set this is you need some pre-cleaning of the form values you are sending to the validator. For instance, one of the child objects in your form state might have all undefined yet required values. Leaving it as is will cause the validator to error each of the required properties. If you pre-clean it the child object will itself be removed from the form opject and if the child object is not required validation will pass. Pre-cleaning is just a convenience for not manually removing the child object within the form logic.
You can reset the form manager by adding a state variable to the deps dependency list.
useAjvValidator:
opts?: Options,
plugins?: {
ajvFormats?: boolean;
ajvErrors?: boolean;
}) => IJsonPrtFormValidator
If you do not specify opts it will default to { allErrors: true }.
The following plugins can be enabled: ajv-formats and ajv-errors.
It is used like this in your React functional code:
const validator = useAjvValidator(undefined, { ajvFormats: true });
This will either create the singleton instance or return it if it has already been created. You can also create it outside of your React functional code which allows you to add auxiliary schemas, the same options as for useAjvValidator as well as to pass in your own Ajv instance:
createAjv: (
schemas?: IAjvSchema[],
opts?: Options,
plugins?: {
ajvFormats?: boolean;
ajvErrors?: boolean;
},
ajv?: Ajv) => void
You instantiate it like this:
createAjv(
[auxSchema1, auxSchema2],
undefined,
{ ajvFormats: true, ajvErrors: true }
);
The ability to pass in your own Ajv instance allows you to configure it with additional plugins beyond the defaults available. Please note that if you pass in your own instance the above opts and plugins are ignored.
Whether you pass in your own Ajv instance or use the default there will always just be a single instance, hence any opts and plugins you pass in are only applied on the first initialisation.
errors below. JsonPtrFormControl:
###
props has the following definition:
interface IJsonPtrFormControl
slot?: string,
ptr: string,
form: IJsonPtrFormResult
render: (args: IJsonPtrFormControlRender) => JSX.Element
}
It takes 4 properties:
- slot: Optional slot used by various UI frameworks
- ptr: The ptr to the form element's state within the form state
- form: The form object returned by useJsonPtrForm
- render: A render method for the form control
The render method has the following definition:
interface IJsonPtrFormControlRender {
valid: (ptr?: string) => boolean,
value:
setValue: (val: any, ptr?: string) => void,
removeValue: (ptr?: string) => void,
resetValue: (value: any, ptr?: string) => void,
rerefValue: (ptr?: string) => void,
error: (ptr?: string) => string | undefined,
errorCount: (ptr?: string) => number,
touched: (ptr?: string) => boolean,
setTouched: (ptr?: string) => void,
dirty: (ptr?: string) => boolean
}
It is used like this:
const { form } = useJsonPtrForm
form={form}
render={({ value, setValue, error, touched, setTouched }) =>
type='text'
value={value
onChange={(e) => setValue(e.target.value)}
onBlur={() => setTouched()}
onInputClear={() => setValue(undefined)}
errorMessage={error()?.schemaPath}
errorMessageForce={touched()} />} />
The render properties mirrors the return value of useJsonPtrForm with the difference that the ptr parameter is optional.
It has the following benefits:
- It takes care of async issues
- It is performant
- It provides convenience
You provide JsonPtrFormControl with the ptr only once and it provides many of the same functions useJsonPtrForm provides but with greater convenience. As stated most of these functions take an optional ptr. If you do not provide the ptr it is defaulted to the ptr set on JsonPtrFormControl yet you can still provide a ptr in order to access other parts of the form state. You are also not limited to the use of these functions and are free to use the functions useJsonPtrForm provides within the scope of JsonPtrFormControl.
{
value
ptr={/options/${idx}}
form={form}
render={({ value, setValue, error, touched, setTouched }) =>
Option ${idx + 1}}
type='text'
value={value
onChange={(e) => setValue(e.target.value)}
onBlur={() => setTouched()}
onInputClear={() => setValue(undefined)}
errorMessage={error()}
errorMessageForce={touched()}
/>} />)
}
To append an array item to the above options list you can do as follows:
() => {
setValue("Cheese", '/options/-');
}
To remove the 4th item in the above options array you do the following:
() => {
removeValue("/options/3");
}
If for some reason you need to provide a new array reference you can do as follows:
() => {
setValue("Cheese", '/options/-');
rerefValue('/options');
}
or if you prefer to do it manually:
() => {
const options = value
setValue([...options, "Cheese"], '/options');
}
Why would you want to provide a new reference?
In the above setValue("Cheese", '/options/-') you appended a value to an array. If the array already existed no new reference is provided. The form will still rerender with the new correct values but dependent form controls might not render correctly. Let's imagine you have a select list that internally has a useMemo hook with a dependency list for its options and you pass value('/options') to it. If you setValue("Cheese", '/options/-') the altered array will still be passed to it but it will not rerun its useMemo hook as the dependency list detected no change.
const { valid, form, values, setTouched } = useJsonPtrForm(
defaultValues,
useValidator(getSchema)
);
const submit = () => {
if (!valid())
setTouched();
else
submitForm(values);
}
cleanDeepPtrs:
Allows you to deep clean your form value before you send it to the backend.
Parameters are:
- source: T
- ptrs: string[]
- options?: CleanOptions
If you do not provide options it defaults to:
{
emptyArrays: true,
emptyObjects: true,
emptyStrings: true,
NaNValues: false,
nullValues: true,
undefinedValues: true
}
It returns a new object with all the values at the provided ptr array cleaned.
You would most likely use this if you set the validatePreClean option in your form manager.
const getSchema = (): IAjvSchema => {
return {
tag: 'ui_test',
schema: {
$schema: "http://json-schema.org/draft-07/schema#",
$id: "http://my-test.com/schemas/ui_test.json",
type: "object",
properties: {
title: {type: "string" },
body: {
type: "object",
properties: {
header: { type: "string" },
footer: { type: "string" }
},
required: ["header", "footer"]
}
},
required: ['title', "body"]
}
}
}
interface IArticle {
title?: string,
body?: {
header?: string,
footer?: string
}
}
const getDefaultValue = (article?: IArticle): IArticle => {
return article ?
{
...article
} :
{
title: undefined,
body: {
header: undefined,
footer: undefined
}
};
}
const TestPage = () => {
const validator = useAjvValidator();
const schema = useMemo(() => getSchema(), []);
const defaultValues = useMemo(() =>
getDefaultValue({
body: {
footer: 'my footer',
header: 'my header'
},
title: 'Hello'
}), []);
const postValidator = useCallback(async(
values: IArticle,
errors: { [ptr: string]: IAjvError[] }
) => {
if (values.title?.toLowerCase().startsWith('my') {
errors['/title'] = errors['/title'] || [];
errors['/title'].push({
instancePath: "/title",
keyword: "errorMessage",
message: "may not start with 'my'",
schemaPath: "#/properties/title/errorMessage"
});
return errors;
}, []);
const {
setValue,
setTouched,
touched,
valid,
values,
form,
dirty,
errors
} = useJsonPtrForm
defaultValues,
{
schema,
validator
},
postValidator);
const submit = () => {
sendToMyServer(cleanDeepPtrs(values, ['/'])).then(...)
}
return (
backLink="Back">
onClick={() => {
if (valid())
submit();
else
setTouched();
} />
form={form}
render={({ value, setValue, error, touched, setTouched }) =>
type='text'
value={value
onChange={(e) => setValue(e.target.value)}
onBlur={() => setTouched()}
onInputClear={() => setValue(undefined)}
errorMessage={error()}
errorMessageForce={touched()}
/>} />
form={form}
render={({ value, setValue, error, touched, setTouched }) =>
type='textarea'
resizable
value={value
onChange={(e) => setValue(e.target.value)}
onBlur={() => setTouched()}
onInputClear={() => setValue(undefined)}
errorMessage={error()}
errorMessageForce={touched()}
/>} />
form={form}
render={({ value, setValue, error, touched, setTouched }) =>
type='textarea'
resizable
value={value
onChange={(e) => setValue(e.target.value)}
onBlur={() => setTouched()}
onInputClear={() => setValue(undefined)}
errorMessage={error()}
errorMessageForce={touched()}
/>} />
};Change Log
version 2.1.0
- Make ptr parameter optional for touched() on useJsonPtrForm
- valid() to return the valid status for the ptr as well as any child ptrs further down for both useJsonPtrForm and JsonPtrFormControl
- touched() to return the touched status for the ptr as well as any child ptrs further down for both useJsonPtrForm and JsonPtrFormControl
- Added the convenience function cleanDeepPtrs()version 2.0.3
- Fixed a bug on resetValueversion 2.0.2
- Fixed the dirty flag from reporting true whilst the form is initialisingversion 2.0.1
- Fixed bug where in very rare circumstances postValidator errors may be overwritten.version 2.0.0
- add ajv-errors to package.json
- removed option fullError
- altered the schema validator validateSchema to return an array of errors for each error ptr
- altered the postValidator signature to receive an array of errors for each error ptr and return an array of errors for each error ptr
- removed second parameter clean?: CleanOptions from value property on useJsonPtrForm return value
- removed second parameter clean?: CleanOptions from JsonPtrFormControl render properties
- alter error property on useJsonPtrForm return value to return string | undefined
- alter error property on JsonPtrFormControl render properties to return string | undefined
- alter errors property on useJsonPtrForm return value to return an array of error objects for each error
- added property errorCount to useJsonPtrForm return value properties
- added property errorCount to JsonPtrFormControl render properties
- altered useAjvValidator to set options and plugins
- altered createAjv to set options, plugins and an instance of Ajvversion 1.0.8
- Add rerefValue to JsonPtrFormControl and useJsonPtrFormversion 1.0.7
- Update json-ptr-store to version 1.1.5 which removed undefined set limitation