You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何配置JSON Form仅在字段被触碰或提交表单时显示必填校验错误

如何配置JSON Form仅在字段被触碰或提交表单时显示必填校验错误

我来帮你搞定这个问题!你现在遇到的情况是JSON Forms默认会在页面加载时就根据Schema里的required规则执行校验,所以一打开页面所有必填字段就变红了。要实现只有用户触碰过字段后离开空值,或者点击提交按钮时才显示错误提示,我们可以通过追踪字段触碰状态、自定义校验逻辑来调整校验时机。

1. 新增状态追踪字段触碰和提交状态

我们需要几个额外的状态来控制校验逻辑:

  • touchedFields:记录哪些字段被用户点击过并离开(失去焦点)
  • success:标记表单是否提交成功(替换原来的submitted,避免和提交校验状态混淆)
  • submitted:标记是否触发过提交操作,用于触发全字段校验
const [touchedFields, setTouchedFields] = useState({});
const [success, setSuccess] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [formData, setFormData] = useState({});
const [isClient, setIsClient] = useState(false);

2. 自定义校验逻辑,控制错误显示时机

我们需要覆盖JSON Forms的默认校验规则,只有当字段被触碰过,或者表单已提交时,才检查必填项并返回错误:

const customValidator = ({ data }) => {
  const errors = {};
  const requiredFields = ['first_name', 'last_name', 'email', 'linkedin_url'];

  requiredFields.forEach(field => {
    // 只有满足「字段被触碰过」或「表单已提交」,且字段为空时,才返回错误
    if ((touchedFields[field] || submitted) && !data[field]) {
      errors[field] = 'This field is required';
    }
  });

  return errors;
};

3. 扩展渲染器,监听字段失去焦点事件

我们需要给每个表单控件添加onBlur事件,当用户离开字段时,标记该字段为已触碰:

// 扩展Material渲染器,给Control组件添加失去焦点监听
const customRenderers = materialRenderers.map(renderer => {
  // 只处理Control类型的渲染器
  if (renderer.tester(1, { type: 'Control' })) {
    return {
      ...renderer,
      renderer: ({ controlProps, ...props }) => {
        const handleBlur = () => {
          // 从scope中提取字段名,比如#/properties/first_name → first_name
          const fieldName = props.scope.split('/').pop();
          setTouchedFields(prev => ({ ...prev, [fieldName]: true }));
        };

        return renderer.renderer({
          ...props,
          controlProps: {
            ...controlProps,
            onBlur: handleBlur
          }
        });
      }
    };
  }
  return renderer;
});

4. 修改提交逻辑,触发全字段校验

点击提交按钮时,先标记表单为已提交(触发所有必填字段的校验),再检查是否有错误,无错误则提交数据:

const onSubmit = async () => {
  // 标记表单已提交,触发所有必填字段的校验
  setSubmitted(true);
  // 获取当前校验结果
  const errors = customValidator({ data: formData });
  
  if (Object.keys(errors).length === 0) {
    try {
      await axios.post('/api/leads', formData);
      setSuccess(true);
    } catch (error) {
      console.error('Submission failed', error);
    }
  }
};

完整修改后的代码

'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 [touchedFields, setTouchedFields] = useState({});
  const [success, setSuccess] = useState(false);
  const [submitted, setSubmitted] = useState(false);
  const [formData, setFormData] = useState({});
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  const customValidator = ({ data }) => {
    const errors = {};
    const requiredFields = ['first_name', 'last_name', 'email', 'linkedin_url'];

    requiredFields.forEach(field => {
      if ((touchedFields[field] || submitted) && !data[field]) {
        errors[field] = 'This field is required';
      }
    });

    return errors;
  };

  // 扩展Material渲染器,添加字段失去焦点监听
  const customRenderers = materialRenderers.map(renderer => {
    if (renderer.tester(1, { type: 'Control' })) {
      return {
        ...renderer,
        renderer: ({ controlProps, ...props }) => {
          const handleBlur = () => {
            const fieldName = props.scope.split('/').pop();
            setTouchedFields(prev => ({ ...prev, [fieldName]: true }));
          };

          return renderer.renderer({
            ...props,
            controlProps: {
              ...controlProps,
              onBlur: handleBlur
            }
          });
        }
      };
    }
    return renderer;
  });

  const onSubmit = async () => {
    setSubmitted(true);
    const errors = customValidator({ data: formData });
    
    if (Object.keys(errors).length === 0) {
      try {
        await axios.post('/api/leads', formData);
        setSuccess(true);
      } catch (error) {
        console.error('Submission failed', error);
      }
    }
  };

  if (success) {
    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={customRenderers}
          cells={materialCells}
          onChange={({ data }) => setFormData(data)}
          validator={customValidator}
        />
      )}
      <Button
        onClick={onSubmit}
        variant="contained"
        color="primary"
        style={{ marginTop: '10px' }}
      >
        Submit
      </Button>
    </div>
  );
}

效果说明

  • 页面加载时,所有字段都是正常状态,不会显示红色错误
  • 当用户点击某个字段,输入后删除内容再离开(失去焦点),该字段会显示必填错误
  • 当用户直接点击提交按钮,所有空的必填字段都会显示错误
  • 提交成功后,显示感谢信息

备注:内容来源于stack exchange,提问作者vbernal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 08:39:33