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' ;
33import { Component } from '@formio/core' ;
44import structuredClone from '@ungap/structured-clone' ;
55
6- import { FormType } from './Form' ;
6+ import { FormType , FormSource } from './Form' ;
77
88interface BuilderConstructor {
9- new (
9+ new (
1010 element : HTMLDivElement ,
11- form : FormType ,
11+ formSource : FormSource | undefined ,
1212 options : FormioFormBuilder [ 'options' ] ,
1313 ) : FormioFormBuilder ;
1414}
1515export 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+
42115const 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-
61129export 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