Skip to content

Commit 04d904d

Browse files
committed
feat(formplayer): enable AJV $data validation and computed fields engine
1 parent ea9a2fb commit 04d904d

6 files changed

Lines changed: 397 additions & 212 deletions

File tree

formulus-formplayer/src/App.tsx

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ import { draftService } from './services/DraftService';
6969
import DraftSelector from './components/DraftSelector';
7070
import { loadExtensions } from './services/ExtensionsLoader';
7171
import { 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)
7578
import { 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

Comments
 (0)