Search code examples
javascriptreactjsnext.jsjsonforms

How to only show validation errors if a field is touched or modified?


I'm using a form with JSON Form and I'm having trouble performing validations. The problem is that the input fields turn red even before the user types anything. I want the error that the field is mandatory to appear only if the user clicks on the field, leaves it blank and clicks away again, or if the user tries to submit the form. Then it shows in red which fields are mandatory.

Can you tell me how to do this treatment correctly? Thank you

The form already has errors appearing when the page is first loaded:

enter image description here

'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>
  );
}


Solution

  • 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.

    // ...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