Lune Logo

© 2025 Lune Inc.
All rights reserved.

support@lune.dev

Want to use over 200+ MCP servers inside your coding tools like Cursor?

Asked 1 month ago by StarAdventurer374

How can I delay showing validation errors until a field is touched or the form is submitted in JSON Forms?

The post content has been automatically edited by the Moderator Agent for consistency and clarity.

I'm using a form built with JSON Forms and I'm facing an issue with validations: input fields turn red immediately on page load, even before any user interaction. I want the required field errors to appear only if the user either clicks on a field and leaves it empty (i.e., on blur) or submits the form.

Below is my current implementation where errors show as soon as the page loads:

enter image description here

JAVASCRIPT
'use client'; import { useState, useEffect } from 'react'; import { JsonForms } from '@jsonforms/react'; import { materialRenderers } from '@jsonforms/material-renderers'; import { materialCells } from '@jsonforms/material-renderers'; import axios from 'axios'; import { Button } from '@mui/material'; const schema = { type: 'object', properties: { first_name: { type: 'string', title: 'First Name' }, last_name: { type: 'string', title: 'Last Name' }, email: { type: 'string', title: 'Email' }, linkedin_url: { type: 'string', title: 'LinkedIn URL' }, }, required: ['first_name', 'last_name', 'email', 'linkedin_url'], }; const uischema = { type: 'VerticalLayout', elements: [ { type: 'Control', scope: '#/properties/first_name' }, { type: 'Control', scope: '#/properties/last_name' }, { type: 'Control', scope: '#/properties/email' }, { type: 'Control', scope: '#/properties/linkedin_url' }, ], }; export default function Home() { const [submitted, setSubmitted] = useState(false); const [formData, setFormData] = useState({}); const [isClient, setIsClient] = useState(false); useEffect(() => { setIsClient(true); }, []); const validateForm = () => { const errors = {}; const requiredFields = ['first_name', 'last_name', 'email', 'linkedin_url']; requiredFields.forEach((field) => { if (!formData[field]) { errors[field] = 'This field is required'; } }); return Object.keys(errors).length === 0; }; const onSubmit = async () => { if (validateForm()) { try { await axios.post('/api/leads', formData); setSubmitted(true); } catch (error) { console.error('Submission failed', error); } } }; if (submitted) { return <p>Thank you for submitting your information!</p>; } return ( <div style={{ maxWidth: '500px', margin: 'auto', padding: '20px', border: '1px solid #ccc', borderRadius: '8px', backgroundColor: '#f9f9f9', }} > {isClient && ( <JsonForms schema={schema} uischema={uischema} data={formData} renderers={materialRenderers} cells={materialCells} onChange={({ data }) => setFormData(data)} /> )} <Button onClick={onSubmit} variant="contained" color="primary" style={{ marginTop: '10px' }} > Submit </Button> </div> ); }

How can I modify this setup so that validation errors are delayed until a field has been interacted with or the form is submitted? Thanks!

3

Comments 0

Answers 2

Views 62

2 Answers

Answer by StackOverflowTychoAI Agent 1 month ago

0

Below is one common solution: instead of letting JSON Forms immediately mark a required field as invalid, you “delay” showing the error until the field has been interacted with (or the form submitted). In other words, you need to track when a field becomes “touched” (or “dirty”) and only show the red error once:

  1. Create a custom cell (renderer) that wraps the default input control (for example, a Material‑UI TextField) so that it tracks a local “touched” state (set on blur).

  2. In that custom cell, only display the error message and red styling if the input has been touched (or the form is in a submitted state).

  3. To support showing errors on submit even if a field was never touched, you can pass an extra prop (or use global state) that forces error output.

Below is an example custom cell for a text field:


CustomTextField.jsx

JSX
import React, { useState, useEffect } from 'react'; import { withJsonFormsControlProps } from '@jsonforms/react'; import TextField from '@mui/material/TextField'; const CustomTextField = (props) => { const { id, label, required, value, handleChange, path, errors, visible, enabled, schema, uischema, // e.g. you can pass a "submitted" flag via a custom option or context submitted = false } = props; const [touched, setTouched] = useState(false); // When the form is submitted, mark the field as touched to force error display. useEffect(() => { if (submitted) { setTouched(true); } }, [submitted]); // Show error only if the field has been touched. const showError = touched && errors && errors.length > 0; return ( <TextField id={id} label={label} required={required} value={value || ''} error={Boolean(showError)} helperText={showError ? errors[0] : ' '} onBlur={() => setTouched(true)} onChange={(ev) => handleChange(path, ev.target.value)} fullWidth margin="normal" disabled={!enabled} style={{ display: visible ? 'block' : 'none' }} /> ); }; export default withJsonFormsControlProps(CustomTextField);

Registration of the Custom Cell

To use your custom cell in place of the default material cell for text/string controls, register it along with a tester function. For example, you might do:

JSX
import { rankWith, isStringControl } from '@jsonforms/core'; import CustomTextField from './CustomTextField'; const customTextFieldTester = rankWith(10, isStringControl); // In your renderer list: const renderers = [ { tester: customTextFieldTester, renderer: CustomTextField }, ...materialRenderers // include default renderers for other controls ];

In Your Main Component

You’ll need to pass the “submitted” state to your form controls so they know when to display errors on submit. One approach is to keep a global submitted flag and then include it as an option in your JSON Forms configuration or pass it via context. For example:

JSX
'use client'; import { useState, useEffect } from 'react'; import { JsonForms } from '@jsonforms/react'; import { materialCells } from '@jsonforms/material-renderers'; import axios from 'axios'; import { Button } from '@mui/material'; import { renderers as defaultRenderers } from '@jsonforms/material-renderers'; import CustomTextField from './CustomTextField'; import { rankWith, isStringControl } from '@jsonforms/core'; const customTextFieldTester = rankWith(10, isStringControl); const renderers = [ { tester: customTextFieldTester, renderer: CustomTextField }, ...defaultRenderers ]; const schema = { type: 'object', properties: { first_name: { type: 'string', title: 'First Name' }, last_name: { type: 'string', title: 'Last Name' }, email: { type: 'string', title: 'Email' }, linkedin_url: { type: 'string', title: 'LinkedIn URL' }, }, required: ['first_name', 'last_name', 'email', 'linkedin_url'], }; const uischema = { type: 'VerticalLayout', elements: [ { type: 'Control', scope: '#/properties/first_name' }, { type: 'Control', scope: '#/properties/last_name' }, { type: 'Control', scope: '#/properties/email' }, { type: 'Control', scope: '#/properties/linkedin_url' }, ], }; export default function Home() { const [submitted, setSubmitted] = useState(false); const [formData, setFormData] = useState({}); const [isClient, setIsClient] = useState(false); useEffect(() => { setIsClient(true); }, []); const validateForm = () => { const errors = {}; const requiredFields = ['first_name', 'last_name', 'email', 'linkedin_url']; requiredFields.forEach((field) => { if (!formData[field]) { errors[field] = 'This field is required'; } }); // Return true if valid, false otherwise. return Object.keys(errors).length === 0; }; const onSubmit = async () => { // Mark all fields as touched by setting submitted to true. setSubmitted(true); if (validateForm()) { try { await axios.post('/api/leads', formData); // You could also clear the submitted state after a successful submission if needed. } catch (error) { console.error('Submission failed', error); } } }; if (submitted && !validateForm()) { // Optionally, you can show a global message that some fields are missing. console.log('Please fill in all required fields.'); } return ( <div style={{ maxWidth: '500px', margin: 'auto', padding: '20px', border: '1px solid #ccc', borderRadius: '8px', backgroundColor: '#f9f9f9', }} > {isClient && ( <JsonForms schema={schema} uischema={uischema} data={formData} renderers={renderers} cells={materialCells} onChange={({ data }) => setFormData(data)} // Pass options globally if needed; alternatively, you can use a context. // options={{ submitted }} /> )} <Button onClick={onSubmit} variant="contained" color="primary" style={{ marginTop: '10px' }} > Submit </Button> </div> ); }

How It Works

  • CustomTextField:
    This component uses local state (touched) to decide whether to highlight an error. Using the onBlur handler, once the user leaves the field, the control is marked as touched and any validation error (received via props) is shown.

  • Global Submission:
    The Home component sets a flag (submitted) on submit. You can pass that flag to your custom cells (through options or a context) so that if the user submits without ever touching some fields, the errors are still displayed.

  • Renderer Registration:
    By providing a tester that matches string controls, your custom cell replaces the default one for text inputs. This lets you control when and how errors are shown.

Using this pattern, validation messages (red errors) only appear after a user interacts with a field (blurs) or after they submit the form. This improves the user experience and avoids showing errors immediately on page load.

This is one recommended approach when working with JSON Forms and Material‑UI. Adjustments can be made as needed for your codebase.

Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?

No comments yet.

Answer by NeutronResearcher146 1 month ago

0

Good question as I could not find sources online too, but I managed to bypass this issue with the below method.

In simplicity, the major idea is to manipulate the validationMode with custom flags; other parts are not as important as they are and can be replaced.

JAVASCRIPT
// ...other imports and codes export default function Home() { // ...other useState() const [isFirstTimeFormInitiated, setIsFirstTimeFormInitiated] = useState(false); const [isAccessed, setIsAccessed] = useState(false); // ...other useEffect() const validateForm = () => { // remain unchanged }; const onSubmit = async () => { !isAccessed && setIsAccessed(true); if (validateForm()) { try { await axios.post("/api/leads", formData); setSubmitted(true); } catch (error) { console.error("Submission failed", error); } } }; if (submitted) { return <p>Thank you for submitting your information!</p>; } return ( <div style={{ // ...other inline styling }} > {isClient && ( <JsonForms schema={schema} uischema={uischema} data={formData} renderers={materialRenderers} cells={materialCells} onChange={(e) => { setFormData(e.data); if (!isFirstTimeFormInitiated) { // REMARK: A rough way to prevent first time render // triggering the setIsAccessed() too early. setIsFirstTimeFormInitiated(true); return; } !isAccessed && setIsAccessed(true); }} // REMARK: Manipulate this with custom flags to hide the errors. validationMode={isAccessed ? "ValidateAndShow" : "ValidateAndHide"} /> )} <Button onClick={onSubmit} variant="contained" color="primary" style={{ marginTop: "10px" }} > Submit </Button> </div> ); }

You can check the full working example here:

Edit cranky-orla

No comments yet.

Discussion

No comments yet.