jsonforms
jsonforms copied to clipboard
Support hiding error messages on load/until field is touched
Is your feature request related to a problem? Please describe.
Dup of https://spectrum.chat/jsonforms/general/hello-everyone-does-anyone-know-how-i-can-verify-that-the-form-is-valid-i-look-in-the-examples-and-i-do-not-see-an-example-of-validation-first-of-all-thanks~204272e3-08c1-495d-a711-f4a27116cc33
We have a use case where we don't want error messages (such as the default "is a required property" for required fields) on load, and only show error messages after the user has started touching the field.
Describe the solution you'd like
You could do
const isValid = typeof data === 'undefined' || errors.length === 0;
here
https://github.com/eclipsesource/jsonforms/blob/master/packages/vanilla/src/controls/InputControl.tsx#L67
Describe alternatives you've considered
You could keep track of dirty/pristine via some state and use that prop
Framework
React
RendererSet
Vanilla
Additional context
No response
Hi @julesterrien thanks for the suggestion. I'm not sure what's the best approach for implementing this. We could adapt the core and introduce another validation mode, e.g. showOnChange
. This way the behavior could be implemented via the bindings and renderer code does not need to be touched, enabling the functionality for all existing and future renderer sets implicitly. However this is inflexible and for example doesn't allow behavior like "showOnTouch".
Alternatively this could just be enabled via UI Schema options. Then the renderers would need to take care of that.
You can implement any behavior you want to have via custom renderers anyway. If you have a nice implementation we could talk about an eventual back contribution to JSON Forms, however for now this is not on our priority list.
This a fairly common use case that I'm personally coming across as well. It's not a good user experience when a bunch of "this field is required" errors are shown when the user hasn't even inputted any data yet.
My current workaround is to conditionally pass the errors
prop to unwrapped controls in my custom renderers:
import * as React from 'react';
import { ControlProps, rankWith, isStringControl } from '@jsonforms/core';
import { withJsonFormsControlProps } from '@jsonforms/react';
import { Unwrapped } from '@jsonforms/material-renderers';
export const Renderer = withJsonFormsControlProps((props: ControlProps) => {
const errors = props.data === undefined ? '' : props.errors;
return <Unwrapped.MaterialTextControl {...props} errors={errors} />;
});
export const tester = rankWith(3, isStringControl);
However, keep in mind that this leads you down a rabbit hole of other issues that you need workarounds for. For example, if you decide to allow forms to be submitted even when all required fields were not filled out yet, then you will want to bypass this conditional errors logic by updating data with initial values such as ""
(empty string) or null
. Another thing to consider is that additionalErrors
won't show for fields which don't have any data.
I like the idea of another validation mode which only shows errors for touched fields. That seems like a non-trivial feature addition, but I wouldn't mind making a contribution if you can point me in the right direction @sdirix. I am also curious how @julesterrien was able to work around this issue.
I am curious why this isn't being brought up more, throwing your users a screen with error messages is .. not ideal.
We've solved it by introducing some very simple pristine/dirty css wrappers to avoid overriding the default resolvers.
I'm not sure what's the best approach for implementing this. We could adapt the core and introduce another validation mode, e.g. showOnChange. This way the behavior could be implemented via the bindings and renderer code does not need to be touched, enabling the functionality for all existing and future renderer sets implicitly. However this is inflexible and for example doesn't allow behavior like "showOnTouch".
Update: I now think a generic support via the JSON Forms Core is not a good solution. The concept of a touched / untouched input is really a UI concept and therefore belongs in the corresponding renderer set.
I am curious why this isn't being brought up more, throwing your users a screen with error messages is .. not ideal.
At the moment we already support hiding all errors via the validationMode: 'NoValidation'
so one of the typical use cases of hiding all errors until Submit is already supported.
My guess to why this is not brought up more is that developers who need to support this specific use case probably have other similar detailed requirements too and therefore maintain their own renderer set anyway in which they then can easily add this behavior if needed.
That seems like a non-trivial feature addition, but I wouldn't mind making a contribution if you can point me in the right direction @sdirix.
An implementation in the renderer set is actually rather straightforward. Just maintain a "touched" state in each renderer and hide the errors until the the respective control is touched. You probably want to also consume some "forceShow" marker in case you need to show an error although the input was not touched. Also if your form can switch context you might need to consume some "reset" marker or force rerender the form to reset the "touched" states.
@sdirix ,
I agree, @jsonforms/core
is not the place to put the logic; however, in my eyes the @jsonforms/material-renderers
can/should be updated with a built-in support for precisely the state/option sets that you've described. Without that support the forms have a broken UX, screaming at the user to fill in required fields before the user has even had a chance to fill in the form.
@sunnysingh gave a solution which corrupts the meaning of undefined
being in the data as meaning "untouched"; however, what I wanted was a solution which defined a touched field as having received the "blur" event. Since the current Unwrapped renderers don't expose any hook to receive the blur event I tried to add a div to receive the bubbled event. I'm not sure if it's the best solution.
That being said I'd prefer the material-renderers to be enhanced - but below I'll give my hack solution in case any others are interested.
import { ControlProps, JsonFormsRendererRegistryEntry } from "@jsonforms/core";
import { withJsonFormsControlProps, withJsonFormsEnumProps, withJsonFormsOneOfEnumProps, withTranslateProps } from "@jsonforms/react";
import merge from 'lodash/merge';
import React, { useState } from "react";
import * as MaterialRenderers from "@jsonforms/material-renderers";
export interface WithWrappedControl {
wrappedControl: any;
}
export type ControlWithTouchedProps = ControlProps & WithWrappedControl;
function ControlWithTouched(props: ControlWithTouchedProps) {
const [touched, setTouched] = useState(false);
const WrappedControl = props.wrappedControl;
return (
<div onBlur={() => setTouched(true)}>
<WrappedControl {...unwrapProps(props, touched)} />
</div>
);
}
function unwrapProps(props: ControlWithTouchedProps, touched: boolean) {
const { errors, config, uischema } = props;
const { forceShow } = merge({}, config, uischema.options);
const showErrors = touched || forceShow;
const result = { ...props, errors: showErrors ? errors : "" };
delete result.wrappedControl;
return result;
}
export const wrapperFunctions: Record<string, any> = {
MaterialEnumControl: (c: any) => withJsonFormsEnumProps(c, false),
MaterialOneOfEnumControl: (c: any) => withJsonFormsOneOfEnumProps(withTranslateProps(React.memo(c)), false),
MaterialOneOfRadioGroupControl: (c: any) => withJsonFormsOneOfEnumProps(c),
};
export function wrapRendererRegistry(entry: JsonFormsRendererRegistryEntry): JsonFormsRendererRegistryEntry {
const tester = entry.tester;
let renderer = entry.renderer;
Object.entries(MaterialRenderers.Unwrapped).some(([key, wrappedControl]) => {
const otherTester = (MaterialRenderers as Record<string, any>)[`m${key.substring(1)}Tester`];
const match = tester === otherTester;
if (match) {
const wrapperFunction = wrapperFunctions[key] || withJsonFormsControlProps;
renderer = wrapperFunction((props: any) => (<ControlWithTouched {...{ ...props, wrappedControl }} />));
}
return match;
});
return { renderer, tester };
}
export const materialRenderers = MaterialRenderers.materialRenderers.map(wrapRendererRegistry);
export const materialCells = MaterialRenderers.materialCells;
To use the above solution, put the contents in a file like material-renderers.ts
and then import that instead. Also it seems impossible to do the same thing for the Cells because there is no UnwrappedCells
export so there's no way to get a handle on the MaterialXXCell
named export of each of the cells.
// BEFORE
import { materialCells, materialRenderers } from "@jsonforms/material-renderers";
// AFTER
import { materialCells, materialRenderers } from "./material-renderers";
@codefactor Sorry for the direct ping, but hoped you might know how to maybe do it with vanilla-renderers
Seems like everything works up until the point where I get the VanillaRenderers.Unwrapped as that does not exist. You wouldn't by chance know of a quick fix I could use in this case?