Skip to content

Commit 6d95843

Browse files
committed
$'syncing commit from monorepo. PR: 465, Title: FIO-11128: fix strange state bugs in efb demo'
1 parent 501a5ac commit 6d95843

1 file changed

Lines changed: 152 additions & 129 deletions

File tree

src/components/FormBuilder.tsx

Lines changed: 152 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
1-
import { useEffect, useRef } from 'react';
2-
import { FormBuilder as FormioFormBuilder } from '@formio/js';
1+
import { useEffect, useRef, useState } from 'react';
2+
import { FormBuilder as FormioFormBuilder, Utils } from '@formio/js';
33
import { Component } from '@formio/core';
44
import structuredClone from '@ungap/structured-clone';
55

6-
import { FormType } from './Form';
6+
import { FormType, FormSource } from './Form';
77

88
interface BuilderConstructor {
9-
new(
9+
new (
1010
element: HTMLDivElement,
11-
form: FormType,
11+
formSource: FormSource | undefined,
1212
options: FormioFormBuilder['options'],
1313
): FormioFormBuilder;
1414
}
1515
export type FormBuilderProps = {
1616
options?: FormioFormBuilder['options'];
1717
Builder?: BuilderConstructor;
18-
initialForm?: FormType;
18+
initialForm?: FormSource;
1919
onBuilderReady?: (builder: FormioFormBuilder) => void;
2020
onChange?: (form: FormType) => void;
2121
onSaveComponent?: (
2222
component: Component,
2323
parent: Component,
2424
index: number,
25-
originalComponentSchema: Component) => void,
25+
originalComponentSchema: Component,
26+
) => void;
2627
onAddComponent?: (
2728
component: Component,
2829
parent: Component,
@@ -39,162 +40,184 @@ export type FormBuilderProps = {
3940
) => void;
4041
};
4142

43+
const toggleEventHandlers = (
44+
builder: FormioFormBuilder,
45+
handlers: Omit<FormBuilderProps, 'options' | 'form' | 'Builder'>,
46+
shouldAttach: boolean = true,
47+
) => {
48+
const fn = shouldAttach ? 'on' : 'off';
49+
const {
50+
onSaveComponent,
51+
onEditComponent,
52+
onUpdateComponent,
53+
onDeleteComponent,
54+
onChange,
55+
} = handlers;
56+
builder.instance?.[fn](
57+
'saveComponent',
58+
(
59+
component: Component,
60+
original: Component,
61+
parent: Component,
62+
path: string,
63+
index: number,
64+
isNew: boolean,
65+
originalComponentSchema: Component,
66+
) => {
67+
onSaveComponent?.(
68+
component,
69+
parent,
70+
index,
71+
originalComponentSchema,
72+
);
73+
onChange?.(structuredClone(builder.instance?.form));
74+
},
75+
);
76+
builder.instance?.[fn]('updateComponent', (component: Component) => {
77+
onUpdateComponent?.(component);
78+
onChange?.(structuredClone(builder.instance.form));
79+
});
80+
builder.instance?.[fn](
81+
'removeComponent',
82+
(
83+
component: Component,
84+
parent: Component,
85+
path: string,
86+
index: number,
87+
) => {
88+
onDeleteComponent?.(component, parent, path, index);
89+
onChange?.(structuredClone(builder.instance?.form));
90+
},
91+
);
92+
93+
builder.instance?.[fn]('cancelComponent', (component: Component) => {
94+
onUpdateComponent?.(component);
95+
onChange?.(structuredClone(builder.instance?.form));
96+
});
97+
98+
builder.instance?.[fn]('editComponent', (component: Component) => {
99+
onEditComponent?.(component);
100+
onChange?.(structuredClone(builder.instance?.form));
101+
});
102+
103+
builder.instance?.[fn]('addComponent', () => {
104+
onChange?.(structuredClone(builder.instance?.form));
105+
});
106+
107+
builder.instance?.[fn]('pdfUploaded', () => {
108+
onChange?.(structuredClone(builder.instance?.form));
109+
});
110+
builder.instance?.[fn]('setDisplay', () => {
111+
onChange?.(structuredClone(builder.instance?.form));
112+
});
113+
};
114+
42115
const createBuilderInstance = async (
116+
BuilderConstructor: BuilderConstructor | undefined,
117+
formSource: FormSource | undefined,
43118
element: HTMLDivElement,
44-
BuilderConstructor:
45-
| BuilderConstructor
46-
| typeof FormioFormBuilder = FormioFormBuilder,
47-
form: FormType = { display: 'form', components: [] },
48119
options: FormBuilderProps['options'] = {},
49120
): Promise<FormioFormBuilder> => {
50-
options = Object.assign({}, options);
51-
form = Object.assign({}, form);
52-
53-
const instance = new BuilderConstructor(element, form, options);
121+
const builder = BuilderConstructor
122+
? new BuilderConstructor(element, formSource, options)
123+
: new FormioFormBuilder(element, formSource, options);
54124

55-
await instance.ready;
56-
return instance;
125+
await builder.ready;
126+
return builder;
57127
};
58128

59-
const DEFAULT_FORM: FormType = { display: 'form' as const, components: [] };
60-
61129
export const FormBuilder = ({
62130
options,
63131
Builder,
64-
initialForm = DEFAULT_FORM,
132+
initialForm,
65133
onBuilderReady,
66-
onChange,
67-
onDeleteComponent,
68-
onEditComponent,
69-
onSaveComponent,
70-
onUpdateComponent,
71-
onAddComponent
134+
...handlers
72135
}: FormBuilderProps) => {
73-
const builder = useRef<FormioFormBuilder | null>(null);
74136
const renderElement = useRef<HTMLDivElement | null>(null);
137+
const [builderInstance, setBuilderInstance] =
138+
useState<FormioFormBuilder | null>(null);
139+
const isMounted = useRef(false);
140+
const currentFormSourceJsonProp = useRef<FormType | null>(null);
75141

76-
// Refs to keep the latest callbacks without reattaching events
77-
const handlersRef = useRef({
78-
onChange,
79-
onDeleteComponent,
80-
onEditComponent,
81-
onSaveComponent,
82-
onUpdateComponent,
83-
onAddComponent
84-
});
85142
useEffect(() => {
86-
handlersRef.current = {
87-
onChange,
88-
onDeleteComponent,
89-
onEditComponent,
90-
onSaveComponent,
91-
onUpdateComponent,
92-
onAddComponent
143+
return () => {
144+
if (builderInstance) {
145+
builderInstance.instance?.destroy(true);
146+
builderInstance.destroy(true);
147+
}
93148
};
94-
}, [onChange, onDeleteComponent, onEditComponent, onSaveComponent, onUpdateComponent, onAddComponent]);
149+
}, [builderInstance]);
95150

96151
useEffect(() => {
97-
let ignore = false;
152+
isMounted.current = true;
153+
return () => {
154+
isMounted.current = false;
155+
};
156+
}, []);
157+
158+
useEffect(() => {
159+
if (
160+
typeof initialForm === 'object' &&
161+
currentFormSourceJsonProp.current &&
162+
Utils._.isEqual(currentFormSourceJsonProp.current, initialForm)
163+
) {
164+
return;
165+
}
98166

99167
const createInstance = async () => {
100168
if (!renderElement.current) {
101-
console.warn('FormBuilder render element not found, cannot render builder.');
169+
console.warn(
170+
'FormBuilder render element not found, cannot render builder.',
171+
);
102172
return;
103173
}
104-
const instance = await createBuilderInstance(
105-
renderElement.current,
174+
175+
currentFormSourceJsonProp.current =
176+
initialForm && typeof initialForm !== 'string'
177+
? structuredClone(initialForm)
178+
: null;
179+
const builder = await createBuilderInstance(
106180
Builder,
107-
structuredClone(initialForm),
181+
currentFormSourceJsonProp.current || initialForm,
182+
renderElement.current,
108183
options,
109184
);
110185

111-
if (!instance) {
112-
console.warn('Failed to create FormBuilder instance');
113-
return;
186+
if (builder) {
187+
if (!isMounted.current) {
188+
builder.instance?.destroy(true);
189+
builder.destroy(true);
190+
}
191+
192+
if (onBuilderReady) {
193+
onBuilderReady(builder);
194+
}
195+
setBuilderInstance((prevInstance) => {
196+
if (prevInstance) {
197+
prevInstance.instance?.destroy(true);
198+
prevInstance.destroy(true);
199+
}
200+
return builder;
201+
});
202+
} else {
203+
console.warn('Failed to create form builder instance');
114204
}
115-
116-
if (ignore) {
117-
instance.instance.destroy(true);
118-
return;
119-
}
120-
121-
// attach handlers here ONCE, immediately after ready
122-
const inst = instance.instance;
123-
const fnOn = (ev: string, cb: (...args: any[]) => void) => inst.on(ev, cb);
124-
125-
const handleSaveComponent = (
126-
component: Component,
127-
original: Component,
128-
parent: Component,
129-
path: string,
130-
index: number,
131-
isNew: boolean,
132-
originalComponentSchema: Component,
133-
) => {
134-
handlersRef.current.onSaveComponent?.(
135-
component,
136-
parent,
137-
index,
138-
originalComponentSchema,
139-
);
140-
handlersRef.current.onChange?.(structuredClone(inst.form));
141-
};
142-
143-
const handleUpdateComponent = (component: Component) => {
144-
handlersRef.current.onUpdateComponent?.(component);
145-
handlersRef.current.onChange?.(structuredClone(inst.form));
146-
};
147-
148-
const handleRemoveComponent = (
149-
component: Component,
150-
parent: Component,
151-
path: string,
152-
index: number,
153-
) => {
154-
handlersRef.current.onDeleteComponent?.(component, parent, path, index);
155-
handlersRef.current.onChange?.(structuredClone(inst.form));
156-
};
157-
158-
const handleEditComponent = (component: Component) => {
159-
handlersRef.current.onEditComponent?.(component);
160-
};
161-
162-
const handleAddComponent = (component: Component , parent: Component, path: string, index: number) => {
163-
handlersRef.current.onAddComponent?.(component, parent, path, index);
164-
handlersRef.current.onChange?.(structuredClone(inst.form));
165-
};
166-
167-
// Attach events
168-
fnOn('saveComponent', handleSaveComponent);
169-
fnOn('updateComponent', handleUpdateComponent);
170-
fnOn('removeComponent', handleRemoveComponent);
171-
fnOn('editComponent', handleEditComponent);
172-
fnOn('addComponent', handleAddComponent);
173-
fnOn('pdfUploaded', () => handlersRef.current.onChange?.(structuredClone(inst.form)));
174-
fnOn('setDisplay', () => handlersRef.current.onChange?.(structuredClone(inst.form)));
175-
176-
// expose instance
177-
if (onBuilderReady) onBuilderReady(instance);
178-
builder.current = instance;
179205
};
180206

181207
createInstance();
208+
}, [Builder, initialForm, onBuilderReady, options]);
209+
210+
useEffect(() => {
211+
if (builderInstance && Object.keys(handlers).length > 0) {
212+
toggleEventHandlers(builderInstance, handlers);
213+
}
182214

183215
return () => {
184-
ignore = true;
185-
// cleanup instance + unsubscribe
186-
if (builder.current) {
187-
builder.current.instance.destroy(true);
216+
if (builderInstance) {
217+
toggleEventHandlers(builderInstance, handlers, false);
188218
}
189219
};
220+
}, [builderInstance, handlers]);
190221

191-
}, []); // should create this instance only one time to avoid problems with listeners inside of webformbuilder.
192-
193-
// set initial form in current builder instance
194-
useEffect(() => {
195-
if (builder.current && initialForm) {
196-
builder.current.instance.setForm(structuredClone(initialForm));
197-
}
198-
}, [initialForm]);
199222
return <div ref={renderElement}></div>;
200-
};
223+
};

0 commit comments

Comments
 (0)