@@ -69,7 +69,10 @@ import { draftService } from './services/DraftService';
6969import DraftSelector from './components/DraftSelector' ;
7070import { loadExtensions } from './services/ExtensionsLoader' ;
7171import { getBuiltinExtensions } from './builtinExtensions' ;
72- import { FormEvaluationProvider } from './FormEvaluationContext' ;
72+ import {
73+ FormEvaluationProvider ,
74+ ExtensionFunction ,
75+ } from './FormEvaluationContext' ;
7376
7477// Import development dependencies (Vite will tree-shake these in production)
7578import { webViewMock } from './mocks/webview-mock' ;
@@ -262,15 +265,20 @@ function App() {
262265 const [ extensionRenderers , setExtensionRenderers ] = useState <
263266 JsonFormsRendererRegistryEntry [ ]
264267 > ( [ ] ) ;
265- // Store extension functions for potential future use (e.g., validation context injection)
266- // eslint-disable-next-line @typescript-eslint/no-unused-vars
268+ // Store extension functions for computed fields and evaluation context
267269 const [ extensionFunctions , setExtensionFunctions ] = useState <
268- // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
269- Map < string , Function >
270+ Map < string , ExtensionFunction >
270271 > ( new Map ( ) ) ;
271272 const [ extensionDefinitions , setExtensionDefinitions ] = useState <
272273 Record < string , any >
273274 > ( { } ) ;
275+ // Store computed field configurations keyed by formType
276+ const [ extensionComputedFields , setExtensionComputedFields ] = useState <
277+ Record <
278+ string ,
279+ Record < string , { function : string ; dependencies ?: string [ ] } >
280+ >
281+ > ( { } ) ;
274282
275283 // Reference to the FormulusClient instance and loading state
276284 const formulusClient = useRef < FormulusClient > ( FormulusClient . getInstance ( ) ) ;
@@ -295,7 +303,10 @@ function App() {
295303 try {
296304 const properties = ( formSchema as any ) ?. properties || { } ;
297305 const dynamicEnumFields = Object . entries ( properties )
298- . filter ( ( [ , propSchema ] : [ string , any ] ) => ! ! propSchema ?. [ 'x-dynamicEnum' ] )
306+ . filter (
307+ ( [ , propSchema ] : [ string , any ] ) =>
308+ ! ! propSchema ?. [ 'x-dynamicEnum' ] ,
309+ )
299310 . map ( ( [ key ] ) => key ) ;
300311
301312 console . log ( '[Formplayer] Form init received' , {
@@ -306,7 +317,10 @@ function App() {
306317 dynamicEnumFields,
307318 } ) ;
308319 } catch ( schemaLogError ) {
309- console . warn ( '[Formplayer] Failed to log schema details' , schemaLogError ) ;
320+ console . warn (
321+ '[Formplayer] Failed to log schema details' ,
322+ schemaLogError ,
323+ ) ;
310324 }
311325
312326 // Extract dark mode preference from params
@@ -320,17 +334,27 @@ function App() {
320334 if ( extensions ) {
321335 try {
322336 const extensionResult = await loadExtensions ( extensions ) ;
323-
337+
324338 // Merge loaded functions with built-ins (loaded functions take precedence)
325339 extensionResult . functions . forEach ( ( func , name ) => {
326340 allFunctions . set ( name , func ) ;
327341 } ) ;
328-
342+
329343 setExtensionRenderers ( extensionResult . renderers ) ;
330344 setExtensionFunctions ( allFunctions ) ;
331345 setExtensionDefinitions ( extensionResult . definitions ) ;
346+ setExtensionComputedFields ( extensionResult . computedFields ) ;
332347
333- console . log ( '[Formplayer] Final extension functions:' , Array . from ( allFunctions . keys ( ) ) ) ;
348+ console . log (
349+ '[Formplayer] Final extension functions:' ,
350+ Array . from ( allFunctions . keys ( ) ) ,
351+ ) ;
352+ if ( Object . keys ( extensionResult . computedFields ) . length > 0 ) {
353+ console . log (
354+ '[Formplayer] Computed fields registered:' ,
355+ extensionResult . computedFields ,
356+ ) ;
357+ }
334358
335359 // Log errors but don't fail form initialization
336360 if ( extensionResult . errors . length > 0 ) {
@@ -342,12 +366,14 @@ function App() {
342366 setExtensionRenderers ( [ ] ) ;
343367 setExtensionFunctions ( allFunctions ) ;
344368 setExtensionDefinitions ( { } ) ;
369+ setExtensionComputedFields ( { } ) ;
345370 }
346371 } else {
347372 // No extensions provided, just use built-ins
348373 setExtensionRenderers ( [ ] ) ;
349374 setExtensionFunctions ( allFunctions ) ;
350375 setExtensionDefinitions ( { } ) ;
376+ setExtensionComputedFields ( { } ) ;
351377 console . log ( '[Formplayer] Using only built-in extensions' ) ;
352378 }
353379
@@ -732,21 +758,65 @@ function App() {
732758
733759 const handleDataChange = useCallback (
734760 ( { data } : { data : FormData } ) => {
735- setData ( data ) ;
761+ let finalData = data ;
762+
763+ // Process computed fields if available for this form type
764+ const formType = formInitData ?. formType ;
765+ const computedFieldConfigs = formType
766+ ? extensionComputedFields [ formType ]
767+ : undefined ;
768+
769+ if (
770+ computedFieldConfigs &&
771+ Object . keys ( computedFieldConfigs ) . length > 0
772+ ) {
773+ let hasComputedChanges = false ;
774+ const updatedData : Record < string , any > = { ...data } ;
775+
776+ for ( const [ fieldName , config ] of Object . entries (
777+ computedFieldConfigs ,
778+ ) ) {
779+ const fn = extensionFunctions . get ( config . function ) ;
780+ if ( typeof fn === 'function' ) {
781+ try {
782+ const computedValue = fn ( data ) ;
783+ if ( updatedData [ fieldName ] !== computedValue ) {
784+ updatedData [ fieldName ] = computedValue ;
785+ hasComputedChanges = true ;
786+ }
787+ } catch ( err ) {
788+ console . warn (
789+ `[Formplayer] Error computing field "${ fieldName } ":` ,
790+ err ,
791+ ) ;
792+ }
793+ }
794+ }
795+
796+ if ( hasComputedChanges ) {
797+ finalData = updatedData as FormData ;
798+ }
799+ }
800+
801+ setData ( finalData ) ;
802+
803+ // Expose current form data globally for extension renderers
804+ ( window as any ) . formulusCurrentFormData = finalData ;
736805
737806 // Save draft data whenever form data changes
738807 if ( formInitData ) {
739- draftService . saveDraft ( formInitData . formType , data , formInitData ) ;
808+ draftService . saveDraft ( formInitData . formType , finalData , formInitData ) ;
740809 }
741810 } ,
742- [ formInitData ] ,
811+ [ formInitData , extensionComputedFields , extensionFunctions ] ,
743812 ) ;
744813
745814 // Create AJV instance with extension definitions support
746815 const ajv = useMemo ( ( ) => {
747816 const instance = new Ajv ( {
748817 allErrors : true ,
749818 strict : false , // Allow custom keywords like x-formulus-validation
819+ $data : true , // Enable cross-field validation via $data references (e.g., {"const": {"$data": "1/otherField"}})
750820 } ) ;
751821 addErrors ( instance ) ;
752822 addFormats ( instance ) ;
@@ -836,10 +906,15 @@ function App() {
836906 p : 3 ,
837907 backgroundColor : 'background.paper' ,
838908 } } >
839- < Typography variant = "h6" color = "error" sx = { { mb : 2 , textAlign : 'center' } } >
909+ < Typography
910+ variant = "h6"
911+ color = "error"
912+ sx = { { mb : 2 , textAlign : 'center' } } >
840913 Error Loading Form
841914 </ Typography >
842- < Typography variant = "body2" sx = { { textAlign : 'center' , color : 'text.secondary' } } >
915+ < Typography
916+ variant = "body2"
917+ sx = { { textAlign : 'center' , color : 'text.secondary' } } >
843918 { loadError }
844919 </ Typography >
845920 </ Box >
0 commit comments