formio.js icon indicating copy to clipboard operation
formio.js copied to clipboard

[Bug] Error on save button after drag drop field

Open pheeca opened this issue 9 months ago • 3 comments

Describe the bug When a text field component is dragged and dropped, the popup for details open. when you click save it throws error and breaks, stop working

Here is the error, Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'hasExtraPages') at Webform.validationProcessor (NestedComponent.js:688:1) at Object.validationProcessorProcess (NestedComponent.js:715:1) at processOneSync (processOne.js:86:1) at process.js:64:1 at eachComponentData.js:14:1 at eachComponent.js:53:1 at Array.forEach (<anonymous>) at eachComponent (eachComponent.js:22:1) at eachComponentData (eachComponentData.js:11:1) at processSync (process.js:59:1)

The exact line of code from where error origins, /lib/cjs/components/_classes/nested/NestedComponent.js if (this.root.hasExtraPages && this.page !== this.root.page) {

my code for UI,

return ( <FormioProvider> <Paper sx={{ height: 400, width: '100%' }}> <DataGrid rows={rows} columns={columns} columnVisibilityModel={{ formTypeID: false, StructuredJson: false, status: false, }} initialState={{

	pagination: {
	  paginationModel: {
		pageSize: 5,
	  },
	},
  }}
  pageSizeOptions={[5]}
  checkboxSelection
  disableRowSelectionOnClick
 />

    <Box sx={{ mb: 2, p: 2, border: '1px solid #ccc', borderRadius: '4px' }}>
      <Typography variant="h6" gutterBottom>
        Configure DataGrid
      </Typography>
      <Box component="form" noValidate autoComplete="off">
        <TextField
          label="Form Name"
          value={selectedForm?.Name || ''}
          onChange={(e) => {
            setSelectedForm((prev) => ({ ...prev, Name: e.target.value }));
            console.log('Name updated:', e.target.value);
          }}
          sx={{ mr: 2, mb: 2 }}
        />
        <TextField
          label="Description"
          value={selectedForm?.Description || ''}
          onChange={(e) => {
            setSelectedForm((prev) => ({ ...prev, Description: e.target.value }));
            console.log('Description updated:', e.target.value);
          }}
          sx={{ mr: 2, mb: 2 }}
        />
        <TextField
          label="Purpose"
          value={selectedForm?.Purpose || ''}
          onChange={(e) => {
            setSelectedForm((prev) => ({ ...prev, Purpose: e.target.value }));
            console.log('Purpose updated:', e.target.value);
          }}
          sx={{ mr: 2, mb: 2 }}
        />
        <Button
          variant="contained"
          onClick={() => {
          const updatedForm = {
                    ...selectedForm,
                    StructuredJson: JSON.stringify(formDefinition), // Update with FormBuilder's JSON
                  };
            console.log('Configuration saved:', updatedForm);
            debugger;
            if (!selectedForm) return;
            setLoading(true);
            if (selectedForm.FormTypeID>0)
            {
             api
                    .put(`FormTypes/${selectedForm.FormTypeID}`, updatedForm)
                    .then(() => {
                      alert('Configuration saved successfully!');
                    })
                    .catch((error) => {
                      console.error('Error saving configuration:', error);
                      alert('Error saving configuration. Please try again.');
                    });
            }else{
            }
            alert('Configuration saved successfully!');
          }}
        >
          Save Configuration
        </Button>
      </Box>
    </Box>
 <FormBuilder  initialForm={formDefinition || { type: 'form', display: 'form', components: [] }} options={{
builder: {
  ace: {
    workerSrc: '/worker-json.js', // Use the local script
  },
},

}}

onChange={(schema) => { runCount++; console.log(Run #${runCount}:, schema); try { if (!schema || !schema.components || schema.components.length === 0) return;

  setFormDefinition(schema);
 } catch (error) {
   console.error('Error updating form definition:', error);
}

}}/> </Paper> </FormioProvider> );

Version/Branch What release version or branch are you experiencing the behavior npm list @formio/js

├── @formio/[email protected] └─┬ @formio/[email protected] └── @formio/[email protected] deduped

To Reproduce Steps to reproduce the behavior:

  1. Create form...
  2. Add component "Text Field"
  3. Click Save button
  4. See error

Expected behavior On save the component changes should be made instead of throwing error

Screenshots

Image

Additional context From my investigation it seems this.root is undefined which is triggering this error

pheeca avatar Apr 09 '25 02:04 pheeca

Could you please give a minimally reproducible example for recreating this issue. Does this only happen when using react? Are you able to reproduce the issue without using @formio/react? A codesandbox or jsfiddle would be helpful to debug this issue

ZenMasterJacob20011 avatar Apr 21 '25 20:04 ZenMasterJacob20011

@pheeca I had the same issue while upgrading to the next version, in my case: "@formio/core": "2.3.2", "@formio/js": "5.0.1", "@formio/react": "6.0.1",

After some testing, I think that the issue is linked to unstable reference to the props passed to the FormBuilder component, especially the options props, when you pass the options directly:

(The example here is based on the first comment, normally you shouldn't pass a new formDefinition on each onChange)

function App() {
    const [formDefinition, setFormDefinition] = useState();
    return (
        <>
            <FormBuilder
                initialForm={formDefinition || { type: "form", display: "form", components: [] }}
                options={{
                    builder: {
                        ace: {
                            workerSrc: "/worker-json.js", // Use the local script
                        },
                    },
                }}
                onChange={(schema) => {
                    try {
                        if (!schema || !schema.components || schema.components.length === 0) return;
                        setFormDefinition(schema);
                    } catch (error) {
                        console.error("Error updating form definition:", error);
                    }
                }}
            />
        </>
    );
}

every onChanges causes a re-render which passes a new object to the options, which internally recreates the builder instance. If your options have a stable reference, the exception doesn't happen anymore:

const options = {
    builder: {
        ace: {
            workerSrc: "/worker-json.js", // Use the local script
        },
    },
};

function App() {
    const [formDefinition, setFormDefinition] = useState();
    return (
        <>
            <FormBuilder
                initialForm={formDefinition || { type: "form", display: "form", components: [] }}
                options={options}
                onChange={(schema) => {
                    try {
                        if (!schema || !schema.components || schema.components.length === 0) return;
                        setFormDefinition(schema);
                    } catch (error) {
                        console.error("Error updating form definition:", error);
                    }
                }}
            />
        </>
    );
}

Most likely, this should be documented in the list of props

llemire-exp avatar May 30 '25 15:05 llemire-exp

@llemire-exp I dont think it has to do with that. I have reproduced it with your suggested changes as well.

https://codesandbox.io/p/sandbox/s2nvtw

@ZenMasterJacob20011 Ive attached reproduced code, I have not used it without react.

pheeca avatar Jun 02 '25 04:06 pheeca

Had one of the devs take a look and they had this to say:

The issue in project configuration itself. The initialForm property of the form builder is set to formDefinition which should reflect the current form definition. Each time we add a component, formDefinition gets updated, this means initialForm property gets updated as well which is not true, so FormBuilder tries to update itself because it thinks that the initialForm got changed. This leads to infinite loop and causes builder to stack when we are trying to save component. The solution is to create a separate object form initial form whick won’t be changed.

Here is an example of it formio_bug_6084 (forked)

lane-formio avatar Jul 31 '25 14:07 lane-formio

Thankyou, this worked.

pheeca avatar Aug 01 '25 21:08 pheeca