@syncfusion/ej2-react-form-renderer 0.1.0-beta → 0.1.2-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,14 +6,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  import React, { useState, useEffect, useRef, useCallback } from 'react';
7
7
  import { Form, FormField } from './FormValidator';
8
8
  import { enableRipple } from '@syncfusion/ej2-base';
9
- import { renderFormField } from './renderFormField';
9
+ import { RenderFormField } from './renderFormField';
10
10
  import { CustomTabComponent } from './CustomTabComponent';
11
- import { mergeSchema, convertComponentsToDataSchema, generateUIPropertiesFromData, generateLayoutFromData, generateDataSchemaFromUI } from './json-converter';
11
+ import { mergeSchema } from './json-converter';
12
12
  import { UniversalJsonParser } from './parser';
13
13
  import { TooltipComponent } from '@syncfusion/ej2-react-popups';
14
14
  import { VALIDATION_REGEX } from './FormValidator';
15
15
  import StaticHtmlRenderer from './StaticHtmlRenderer';
16
- import { splitWidths } from './utils';
16
+ import { formatDateValues, splitWidths } from './utils';
17
+ import { ConditionalRuleEngine } from './ConditionalRuleEngine';
18
+ import { ExpressionEngine } from './expressions/ExpressionEngine';
19
+ import { MinMaxRuleEngine } from './MinMaxRuleEngine';
17
20
  enableRipple(true);
18
21
  /**
19
22
  * Form layout options (only applies when UI schema is NOT provided)
@@ -24,153 +27,60 @@ export const FormLayout = {
24
27
  ThreeColumn: 'threecolumn',
25
28
  FourColumn: 'fourcolumn'
26
29
  };
27
- /**
28
- * Helper: Normalize DataSchema input
29
- * Converts any object to JSON string and parses with UniversalJsonParser
30
- * Handles FormSchema, DataSchema, or raw data - parser infers the format
31
- */
32
- function normalizeDataSchema(input) {
33
- try {
34
- // Convert object to JSON string
35
- const jsonString = JSON.stringify(input);
36
- const parser = new UniversalJsonParser();
37
- const parseResult = parser.parse(jsonString);
38
- if (parseResult.success && parseResult.schema) {
39
- const dataSchema = convertComponentsToDataSchema(parseResult.schema.components);
40
- // console.log(`✅ Parsed object as ${parseResult.metadata.detectedFormat} and converted to DataSchema`);
41
- return dataSchema;
42
- }
43
- else {
44
- console.error('❌ Failed to parse object:', parseResult.errors);
45
- return null;
46
- }
47
- }
48
- catch (error) {
49
- console.error('❌ DataSchema normalization error:', error);
50
- return null;
51
- }
52
- }
53
- /**
54
- * Helper: Parse string input using UniversalJsonParser
55
- * Returns DataSchema if parseable, otherwise null
56
- * Validates that UI schema was not accidentally provided
57
- */
58
- function parseStringToDataSchema(input) {
59
- try {
60
- // First check if user accidentally pasted UI schema in data schema field
61
- const testParse = JSON.parse(input);
62
- if (testParse.layout || (testParse.theme && testParse.properties && testParse.layout)) {
63
- console.error('The provided schema is a UI Schema in the data schema prop. Please provide a valid Data Schema with field definitions and validation rules.');
64
- return null;
65
- }
66
- const parser = new UniversalJsonParser();
67
- const parseResult = parser.parse(input);
68
- if (parseResult.success && parseResult.schema) {
69
- const dataSchema = convertComponentsToDataSchema(parseResult.schema.components);
70
- return dataSchema;
71
- }
72
- else {
73
- console.error('Failed to parse input:', parseResult.errors);
74
- return null;
75
- }
76
- }
77
- catch (error) {
78
- console.error('Parser error:', error);
79
- return null;
80
- }
81
- }
82
- /**
83
- * Helper: Parse string input as UISchema
84
- * Validates JSON structure matches UISchema format
85
- * Detects if data schema was accidentally provided
86
- */
87
- function parseStringToUISchema(input) {
88
- try {
89
- const parsed = JSON.parse(input);
90
- // Check if data schema was pasted in UI schema field
91
- if (parsed.properties && !parsed.layout && !parsed.theme) {
92
- const hasValidationProps = Object.values(parsed.properties).some((prop) => prop.required !== undefined || prop.minLength !== undefined || prop.maxLength !== undefined ||
93
- prop.minValue !== undefined || prop.maxValue !== undefined || prop.pattern !== undefined);
94
- if (hasValidationProps) {
95
- console.error('The provided schema is a Data Schema in the UI schema prop. Please provide a valid UI Schema with theme, properties, and layout definitions.');
96
- return null;
97
- }
98
- }
99
- // Validate UISchema structure
100
- if (!parsed.theme || !parsed.properties || !parsed.layout) {
101
- console.error('Invalid UISchema: missing theme, properties, or layout');
102
- return null;
103
- }
104
- return parsed;
105
- }
106
- catch (error) {
107
- console.error('JSON parse error:', error);
108
- return null;
109
- }
110
- }
111
30
  // ForwardRef to expose public methods
112
- export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, onSubmit, onChange, onButtonClick, className = '', layout = FormLayout.SingleColumn, created, ...restProps }, ref) => {
31
+ export const FormRenderer = React.forwardRef(({ schema, locale, onSubmit, onChange, onButtonClick, className = '', layout = FormLayout.SingleColumn, created, ...restProps }, ref) => {
113
32
  const processedSchema = React.useMemo(() => {
114
33
  try {
115
- // Parse and normalize data schema
116
- let parsedDataSchema = dataSchema
117
- ? ((typeof dataSchema === 'string'
118
- ? parseStringToDataSchema(dataSchema)
119
- : normalizeDataSchema(dataSchema)) || undefined)
120
- : undefined;
121
- // Parse and normalize UI schema
122
- let parsedUISchema = uiSchema
123
- ? (typeof uiSchema === 'string'
124
- ? parseStringToUISchema(uiSchema)
125
- : uiSchema) || undefined
126
- : undefined;
127
- // Scenario 1: Both dataSchema and uiSchema provided - full merge
128
- if (parsedDataSchema && parsedUISchema) {
129
- const uiFieldIds = new Set(Object.keys(parsedUISchema.properties));
130
- const dataFieldIds = Object.keys(parsedDataSchema.properties);
131
- const missingFields = dataFieldIds.filter(id => !uiFieldIds.has(id));
132
- if (missingFields.length > 0) {
133
- console.warn(`⚠️ Found ${missingFields.length} fields in data schema missing from UI schema`);
34
+ if (!schema) {
35
+ return { version: '1.0.0', components: [] };
36
+ }
37
+ let parsedInput = schema;
38
+ if (typeof schema === 'string') {
39
+ try {
40
+ parsedInput = JSON.parse(schema);
41
+ }
42
+ catch (err) {
43
+ console.error('FormRenderer: Failed to parse schema string', err);
44
+ return { version: '1.0.0', components: [] };
134
45
  }
135
- const merged = mergeSchema({ dataSchema: parsedDataSchema, uiSchema: parsedUISchema });
136
- return merged;
137
46
  }
138
- // Scenario 2: Only dataSchema provided - auto-generate UI schema
139
- if (parsedDataSchema && !parsedUISchema) {
140
- const autoUISchema = {
141
- properties: generateUIPropertiesFromData(parsedDataSchema),
142
- layout: generateLayoutFromData(parsedDataSchema)
143
- };
144
- const merged = mergeSchema({ dataSchema: parsedDataSchema, uiSchema: autoUISchema });
145
- return merged;
47
+ const isUnifiedSchema = parsedInput && typeof parsedInput === 'object' &&
48
+ parsedInput.properties && Array.isArray(parsedInput.layout);
49
+ if (isUnifiedSchema) {
50
+ return mergeSchema(parsedInput);
146
51
  }
147
- // Scenario 3: Only uiSchema provided - auto-generate data schema
148
- if (!parsedDataSchema && parsedUISchema) {
149
- const autoDataSchema = generateDataSchemaFromUI(parsedUISchema);
150
- const uiFieldIds = new Set(Object.keys(parsedUISchema.properties));
151
- const dataFieldIds = Object.keys(autoDataSchema.properties);
152
- const missingFields = dataFieldIds.filter(id => !uiFieldIds.has(id));
153
- if (missingFields.length > 0) {
154
- console.warn(`Found ${missingFields.length} fields in auto-generated data schema missing from UI schema`);
52
+ else {
53
+ const parser = new UniversalJsonParser();
54
+ const parseResult = parser.parse(typeof schema === 'string' ? schema : JSON.stringify(schema));
55
+ if (parseResult.success && parseResult.schema) {
56
+ return parseResult.schema;
57
+ }
58
+ else {
59
+ console.warn('FormRenderer: Failed to parse schema', parseResult.errors);
60
+ return { version: '1.0.0', components: [] };
155
61
  }
156
- const merged = mergeSchema({ dataSchema: autoDataSchema, uiSchema: parsedUISchema });
157
- return merged;
158
62
  }
159
- // Scenario 4: Neither provided - return empty form
160
- console.warn('FormRenderer: No schema provided (dataSchema or uiSchema)');
161
- return { version: '1.0.0-beta', theme: 'tailwind3', components: [] };
162
63
  }
163
64
  catch (error) {
164
65
  console.error('FormRenderer schema processing error:', error);
165
- return { version: '1.0.0-beta', theme: 'tailwind3', components: [] };
66
+ return { version: '1.0.0', components: [] };
166
67
  }
167
- }, [dataSchema, uiSchema]);
68
+ }, [schema]);
168
69
  const [formState, setFormState] = useState(undefined);
169
70
  const isInitializedRef = useRef(false);
71
+ const formLocale = locale || 'en-US';
170
72
  // Attach ref to the input element for getComponent
171
73
  let setInputRef;
172
74
  // Store input component refs by name/id
173
75
  const inputRefs = useRef({});
76
+ // Track which fields have active conditional values applied (to prevent re-applying while condition stays true)
77
+ const conditionalValueAppliedRef = useRef({});
78
+ // Expression Engine initialization
79
+ const expressionEngineRef = useRef(null);
80
+ const expressionDependencyGraphRef = useRef({});
81
+ const expressionEvaluationInProgressRef = useRef(false);
82
+ const previousFormValuesRef = useRef({});
83
+ const computedFieldNamesRef = useRef(new Set());
174
84
  // Check if UI schema has layout components (table, tabs, or panel)
175
85
  const hasLayoutComponents = React.useMemo(() => {
176
86
  if (!processedSchema.components)
@@ -256,7 +166,7 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, onSubmit,
256
166
  const map = {};
257
167
  const traverse = (comps) => {
258
168
  comps.forEach(comp => {
259
- if (comp.label && !['panel', 'table', 'tabs', 'card', 'button', 'message', 'splitbutton'].includes(comp.type)) {
169
+ if (comp.label && !['panel', 'table', 'tabs', 'card', 'button', 'message', 'splitButton'].includes(comp.type)) {
260
170
  map[comp.id] = comp.label;
261
171
  }
262
172
  if (comp.children)
@@ -282,20 +192,27 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, onSubmit,
282
192
  */
283
193
  const buildValidationRules = useCallback(() => {
284
194
  const rules = {};
195
+ const formValues = formState?.values || {};
285
196
  const collectRules = (components) => {
286
197
  components.forEach((component) => {
287
198
  if (component) {
288
199
  const componentRules = {};
289
- if (component.required && !component.disabled && !component.readOnly) {
200
+ const conditionalRequired = ConditionalRuleEngine.isFieldRequired(component.conditions, formValues);
201
+ const conditionalDisabled = ConditionalRuleEngine.isFieldDisabled(component.conditions, formValues);
202
+ const conditionalReadOnly = ConditionalRuleEngine.isFieldReadOnly(component.conditions, formValues);
203
+ const finalRequired = component.required || conditionalRequired;
204
+ const finalDisabled = component.disabled || conditionalDisabled;
205
+ const finalReadOnly = component.readOnly || conditionalReadOnly;
206
+ if (finalRequired && !finalDisabled && !finalReadOnly) {
290
207
  componentRules.required = [true, component.validationMessage || `${component.label} is required`];
291
208
  }
292
- if (component.minLength && !component.disabled && !component.readOnly) {
209
+ if (component.minLength && !finalDisabled && !finalReadOnly) {
293
210
  componentRules.minLength = [component.minLength, `Minimum ${component.minLength} characters required`];
294
211
  }
295
- if (component.maxLength && !component.disabled && !component.readOnly) {
212
+ if (component.maxLength && !finalDisabled && !finalReadOnly) {
296
213
  componentRules.maxLength = [component.maxLength, `Maximum ${component.maxLength} characters allowed`];
297
214
  }
298
- if (component?.type === 'textbox' && component?.textboxType && !component?.regex && !componentRules.regex && !component.disabled && !component.readOnly) {
215
+ if (component?.type === 'textbox' && component?.textboxType && !component?.regex && !componentRules.regex && !finalDisabled && !finalReadOnly) {
299
216
  if (component.textboxType === 'email') {
300
217
  componentRules.regex = [VALIDATION_REGEX['EMAIL'], component?.validationMessage || 'Invalid format'];
301
218
  }
@@ -303,7 +220,7 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, onSubmit,
303
220
  componentRules.regex = [VALIDATION_REGEX['URL'], component?.validationMessage || 'Invalid format'];
304
221
  }
305
222
  }
306
- if (component.regex && !component.disabled && !component.readOnly) {
223
+ if (component.regex && !finalDisabled && !finalReadOnly) {
307
224
  componentRules.regex = [component.regex, component.validationMessage || 'Invalid format'];
308
225
  }
309
226
  if (Object.keys(componentRules).length > 0) {
@@ -329,7 +246,7 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, onSubmit,
329
246
  };
330
247
  collectRules(processedSchema.components);
331
248
  return rules;
332
- }, [processedSchema]);
249
+ }, [processedSchema, formState?.values]);
333
250
  const validationRules = buildValidationRules();
334
251
  /**
335
252
  * Build initial form values from component defaultValues
@@ -339,14 +256,23 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, onSubmit,
339
256
  const allComponents = collectAllComponents(processedSchema.components);
340
257
  const componentIds = new Set();
341
258
  allComponents.forEach(comp => {
342
- // Do NOT store datagrid data inside the form state; grids manage their own internal data.
343
- if (comp.type === 'datagrid' || comp.type === 'richtexteditor') {
259
+ // Do NOT store dataGrid data inside the form state; grids manage their own internal data.
260
+ if (comp.type === 'dataGrid' || comp.type === 'richTextEditor' || comp.type === 'staticHtml') {
344
261
  componentIds.add(comp.id);
345
262
  return;
346
263
  }
347
264
  if (comp.defaultValue !== undefined) {
348
265
  values[comp.id] = comp.defaultValue;
349
266
  }
267
+ else {
268
+ // Initialize with type-appropriate defaults
269
+ switch (comp.type) {
270
+ case 'checkbox':
271
+ case 'switch':
272
+ values[comp.id] = false;
273
+ break;
274
+ }
275
+ }
350
276
  componentIds.add(comp.id);
351
277
  });
352
278
  if (formState) {
@@ -355,7 +281,7 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, onSubmit,
355
281
  });
356
282
  }
357
283
  return values;
358
- }, [processedSchema, collectAllComponents, dataSchema, uiSchema]);
284
+ }, [processedSchema, collectAllComponents, schema]);
359
285
  // Update the useEffect that sets default values
360
286
  useEffect(() => {
361
287
  if (formState && !isInitializedRef.current) {
@@ -370,21 +296,202 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, onSubmit,
370
296
  });
371
297
  }
372
298
  }, [formState, processedSchema, initialValues]);
299
+ /**
300
+ * Initialize Expression Engine and build dependency graph
301
+ * Runs when schema changes
302
+ */
303
+ useEffect(() => {
304
+ if (!processedSchema.components || processedSchema.components.length === 0) {
305
+ return;
306
+ }
307
+ try {
308
+ // Initialize expression engine if not already done
309
+ if (!expressionEngineRef.current) {
310
+ expressionEngineRef.current = new ExpressionEngine();
311
+ }
312
+ const engine = expressionEngineRef.current;
313
+ const allComponents = collectAllComponents(processedSchema.components);
314
+ // Build dependency graph from all expressions
315
+ const graph = engine.buildDependencyGraph(allComponents);
316
+ // Check for circular dependencies
317
+ const cycles = engine.detectCircularDependencies(graph);
318
+ if (cycles.length > 0) {
319
+ //console.warn('⚠️ Circular dependencies detected in expressions:', cycles);
320
+ }
321
+ // Store dependency graph
322
+ expressionDependencyGraphRef.current = graph;
323
+ // Track which fields are computed by expressions
324
+ const computedFields = new Set();
325
+ allComponents.forEach(comp => {
326
+ if (comp.expressionValue) {
327
+ computedFields.add(comp.id);
328
+ }
329
+ });
330
+ computedFieldNamesRef.current = computedFields;
331
+ }
332
+ catch (error) {
333
+ console.error('Error initializing expression engine:', error);
334
+ }
335
+ }, [processedSchema, collectAllComponents]);
336
+ /**
337
+ * Evaluate expressions when form values change
338
+ * Runs expressions in topological order to ensure dependencies are satisfied
339
+ */
340
+ useEffect(() => {
341
+ if (!formState || !expressionEngineRef.current) {
342
+ return;
343
+ }
344
+ // Prevent recursive evaluation loops
345
+ if (expressionEvaluationInProgressRef.current) {
346
+ return;
347
+ }
348
+ try {
349
+ expressionEvaluationInProgressRef.current = true;
350
+ const engine = expressionEngineRef.current;
351
+ const dependencyGraph = expressionDependencyGraphRef.current;
352
+ const formValues = formState.values || {};
353
+ const allComponents = collectAllComponents(processedSchema.components);
354
+ const previousValues = previousFormValuesRef.current;
355
+ const computedFieldNames = computedFieldNamesRef.current;
356
+ // Create a mapping of component id -> component name for context building
357
+ const idToNameMap = {};
358
+ allComponents.forEach(comp => {
359
+ if (comp.name) {
360
+ idToNameMap[comp.id] = comp.name;
361
+ }
362
+ });
363
+ // Convert formValues from id-based keys to name-based keys for expression context
364
+ const contextValues = {};
365
+ Object.entries(formValues).forEach(([componentId, value]) => {
366
+ const fieldName = idToNameMap[componentId];
367
+ if (fieldName) {
368
+ contextValues[fieldName] = value;
369
+ }
370
+ });
371
+ // Capture previous state before checking for changes
372
+ const previousContextValues = { ...previousValues };
373
+ // Update previous values immediately with current context for next cycle
374
+ // This ensures stale values don't affect the next evaluation cycle
375
+ Object.assign(previousValues, contextValues);
376
+ // Find which fields have changed (using captured previous state)
377
+ const changedFields = new Set();
378
+ Object.keys(contextValues).forEach(fieldName => {
379
+ const fieldId = Object.keys(idToNameMap).find(id => idToNameMap[id] === fieldName);
380
+ if (fieldId && contextValues[fieldName] !== previousContextValues[fieldName] && !computedFieldNames.has(fieldId)) {
381
+ changedFields.add(fieldName);
382
+ }
383
+ });
384
+ // If no non-computed fields changed, skip evaluation
385
+ if (changedFields.size === 0) {
386
+ return;
387
+ }
388
+ // Get topological order for evaluation
389
+ const evaluationOrder = engine.getTopologicalOrder(dependencyGraph);
390
+ evaluationOrder.forEach(fieldName => {
391
+ // Find component with this name
392
+ const component = allComponents.find(c => c.name === fieldName);
393
+ if (component && component.expressionValue) {
394
+ // Mark this field as computed
395
+ computedFieldNames.add(component.id);
396
+ try {
397
+ // Only evaluate if field is visible and not read-only
398
+ const isVisible = component.visible !== false
399
+ ? ConditionalRuleEngine.isFieldVisible(component.conditions, formValues)
400
+ : false;
401
+ if (isVisible) {
402
+ const currentValues = {};
403
+ Object.entries(formValues).forEach(([componentId, value]) => {
404
+ const fieldName = idToNameMap[componentId];
405
+ if (fieldName) {
406
+ currentValues[fieldName] = value;
407
+ }
408
+ });
409
+ // Use context values (name-based) for evaluation
410
+ let result = engine.evaluate(component.expressionValue, currentValues);
411
+ result = typeof (result) === 'string' && result.includes('undefined') ? result.replace(/undefined/g, '') : result;
412
+ // Coerce result to appropriate type based on field type
413
+ if (result !== null && result !== undefined && result !== '') {
414
+ switch (component.type) {
415
+ case 'number':
416
+ // Ensure numeric fields return numbers (but preserve empty string)
417
+ const numResult = Number(result);
418
+ result = isNaN(numResult) ? 0 : numResult;
419
+ result = result === 0 ? "" : result;
420
+ break;
421
+ case 'checkbox':
422
+ case 'switch':
423
+ // Ensure boolean fields return booleans
424
+ result = Boolean(result);
425
+ break;
426
+ case 'textbox':
427
+ case 'textarea':
428
+ // Ensure text fields return strings
429
+ result = String(result);
430
+ break;
431
+ case 'dateRange':
432
+ case 'checkboxGroup':
433
+ case "multiselect":
434
+ break;
435
+ default:
436
+ // Other types use result as-is
437
+ result = String(result);
438
+ break;
439
+ }
440
+ }
441
+ // Apply result if different from current value
442
+ if (result !== null && result !== undefined && result !== formValues[component.id]) {
443
+ formState.values[component.id] = result;
444
+ formState.onChange(component.id, { value: result });
445
+ }
446
+ }
447
+ }
448
+ catch (error) {
449
+ console.error(`Error evaluating expression for field "${fieldName}":`, error);
450
+ }
451
+ }
452
+ });
453
+ // Update previous values for next comparison
454
+ Object.assign(previousValues, contextValues);
455
+ }
456
+ finally {
457
+ expressionEvaluationInProgressRef.current = false;
458
+ }
459
+ }, [formState?.values, processedSchema, collectAllComponents]);
373
460
  /**
374
461
  * Render a single field with label and error
375
462
  */
376
463
  const renderField = (component) => {
377
- // Skip invisible components
378
- if (component.visible === false)
379
- return null;
464
+ const formValues = formState?.values || {};
380
465
  // Skip layout and button components (handled separately)
381
- if (['panel', 'table', 'tabs', 'card', 'message', 'button', 'splitbutton', 'statichtml'].includes(component.type)) {
466
+ if (['panel', 'table', 'tabs', 'card', 'message', 'button', 'splitButton', 'staticHtml'].includes(component.type)) {
382
467
  return null;
383
468
  }
469
+ const conditionalReadOnly = ConditionalRuleEngine.isFieldReadOnly(component.conditions, formValues);
470
+ const conditionalDisabled = ConditionalRuleEngine.isFieldDisabled(component.conditions, formValues);
471
+ const conditionalRequired = ConditionalRuleEngine.isFieldRequired(component.conditions, formValues);
472
+ const conditionalValue = ConditionalRuleEngine.getConditionalValue(component.conditions, formValues);
473
+ const computedReadOnly = component.readOnly || conditionalReadOnly;
474
+ const computedDisabled = component.disabled || conditionalDisabled;
475
+ const computedRequired = component.required || conditionalRequired;
476
+ // Calculate min/max constraints from minMaxRange
477
+ let minMaxConstraints = MinMaxRuleEngine.getConstraints(component.minMaxRange, formValues, component.type);
478
+ // For dateTime, also get date and time constraints separately
479
+ if (component.type === 'dateTime') {
480
+ const dateConstraints = MinMaxRuleEngine.getConstraints(component.minMaxRange, formValues, 'date');
481
+ const timeConstraints = MinMaxRuleEngine.getConstraints(component.minMaxTimeRange, formValues, 'time');
482
+ minMaxConstraints = {
483
+ minValue: undefined,
484
+ maxValue: undefined,
485
+ minDate: dateConstraints.minDate,
486
+ maxDate: dateConstraints.maxDate,
487
+ minTime: timeConstraints.minTime,
488
+ maxTime: timeConstraints.maxTime
489
+ };
490
+ }
384
491
  const labelPos = component.labelPosition || 'top';
385
492
  const labelWidth = component.labelWidth || 'auto';
386
- const showLabel = !component.hideLabel && component.label && !['checkbox', 'button', 'statichtml'].includes(component.type);
387
- if ((component.type === 'radio' || component.type === 'checkboxgroup') && Array.isArray(component.options)) {
493
+ const showLabel = !component.hideLabel && component.label && !['checkbox', 'button', 'staticHtml'].includes(component.type);
494
+ if ((component.type === 'radio' || component.type === 'checkboxGroup') && Array.isArray(component.options)) {
388
495
  // For radio, store each option's ref with unique key
389
496
  setInputRef = (el, idx) => {
390
497
  if (el && typeof idx === 'number') {
@@ -399,51 +506,73 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, onSubmit,
399
506
  }
400
507
  };
401
508
  }
402
- const hasError = !!formState?.errors[component.id];
403
- return (_jsx(FormField, { name: component.id, children: _jsx("div", { className: "form-group", children: _jsxs("div", { className: `field-wrapper label-${labelPos.toLowerCase()}`, children: [showLabel && (_jsxs("label", { className: "field-label", htmlFor: component.id, style: { width: labelPos.toLowerCase() === 'left' ? `${labelWidth}%` : 'auto' }, children: [_jsx("span", { children: component.label }), component?.required && _jsx("span", { className: "required-asterisk", children: "*" }), component.tooltip && (_jsx(TooltipComponent, { content: component.tooltip, children: _jsx("span", { className: "e-msg-icon", style: {
509
+ const shouldShowError = !!formState?.errors[component.id];
510
+ const isRequiredError = shouldShowError && formState?.errors[component.id].includes('required');
511
+ const hasError = shouldShowError && !(isRequiredError && !computedRequired);
512
+ // Apply conditional value only when condition transitions from false to true (start of new cycle)
513
+ const conditionIsActive = conditionalValue !== undefined;
514
+ const wasConditionActive = conditionalValueAppliedRef.current[component.id] || false;
515
+ if (formState) {
516
+ if (conditionIsActive && !wasConditionActive) {
517
+ formState.onChange(component.id, { value: conditionalValue });
518
+ conditionalValueAppliedRef.current[component.id] = true;
519
+ }
520
+ else if (!conditionIsActive && wasConditionActive) {
521
+ // Condition just became inactive - clear tracking (ready for next cycle)
522
+ conditionalValueAppliedRef.current[component.id] = false;
523
+ }
524
+ // If condition stays active (wasConditionActive && conditionIsActive), do nothing - let user edit
525
+ }
526
+ return (_jsx(FormField, { name: component.id, children: _jsx("div", { className: "form-group", children: _jsxs("div", { className: `field-wrapper label-${labelPos.toLowerCase()}`, children: [showLabel && (_jsxs("label", { className: "field-label", htmlFor: component.id, style: { width: labelPos.toLowerCase() === 'left' ? `${labelWidth}%` : 'auto' }, children: [_jsx("span", { children: component.label }), computedRequired && _jsx("span", { className: "required-asterisk", children: "*" }), component.tooltip && (_jsx(TooltipComponent, { content: component.tooltip, children: _jsx("span", { className: "e-msg-icon", style: {
404
527
  cursor: 'pointer',
405
528
  display: 'flex',
406
529
  alignItems: 'center',
407
530
  justifyContent: 'center',
408
531
  flexShrink: 0,
409
- } }) }))] })), _jsxs("div", { className: "field-input", children: [(component.type === 'radio' || component.type === 'checkboxgroup') && Array.isArray(component.options)
410
- ? renderFormField({
411
- component,
412
- value: formState?.values[component.id],
413
- onChange: (value) => {
532
+ } }) }))] })), _jsxs("div", { className: "field-input", children: [(component.type === 'radio' || component.type === 'checkboxGroup') && Array.isArray(component.options)
533
+ ? (_jsx(RenderFormField, { component: { ...component, readOnly: computedReadOnly, disabled: computedDisabled, required: computedRequired }, value: formState?.values[component.id], onChange: (value) => {
414
534
  formState?.onChange(component.id, { value });
415
535
  if (onChange) {
416
536
  onChange({ field: component.label || component.id, value });
417
537
  }
418
- },
419
- onBlur: () => formState?.onBlur(component.id),
420
- ref: (el, idx) => setInputRef(el, idx),
421
- hasError,
422
- })
423
- : renderFormField({
424
- component,
425
- value: formState?.values[component.id],
426
- onChange: (value) => {
427
- if (component.type !== 'datagrid' && component.type !== 'signature' && component.type !== 'richtexteditor') {
538
+ }, locale: formLocale, onBlur: () => formState?.onBlur(component.id), ref: (el, idx) => setInputRef(el, idx), hasError: hasError }))
539
+ : (_jsx(RenderFormField, { component: {
540
+ ...component,
541
+ readOnly: computedReadOnly,
542
+ disabled: computedDisabled,
543
+ required: computedRequired,
544
+ ...(minMaxConstraints.minValue !== undefined && { minValue: minMaxConstraints.minValue }),
545
+ ...(minMaxConstraints.maxValue !== undefined && { maxValue: minMaxConstraints.maxValue }),
546
+ ...(minMaxConstraints.minDate !== undefined && { minDate: minMaxConstraints.minDate }),
547
+ ...(minMaxConstraints.maxDate !== undefined && { maxDate: minMaxConstraints.maxDate }),
548
+ ...(minMaxConstraints.minTime !== undefined && { minTime: minMaxConstraints.minTime }),
549
+ ...(minMaxConstraints.maxTime !== undefined && { maxTime: minMaxConstraints.maxTime })
550
+ }, formState: formState, value: formState?.values[component.id], onChange: (value) => {
551
+ if (component.type !== 'dataGrid' && component.type !== 'signature' && component.type !== 'richTextEditor' && component.type !== 'staticHtml') {
552
+ value = formatDateValues(component, value);
428
553
  formState?.onChange(component.id, { value });
429
554
  if (onChange) {
430
555
  onChange({ field: component.label || component.id, value });
431
556
  }
432
557
  }
433
- },
434
- onBlur: () => { if (component.type !== 'datagrid' && component.type !== 'richtexteditor')
435
- formState?.onBlur(component.id); },
436
- ref: setInputRef,
437
- hasError,
438
- }), formState?.errors[component.id] && _jsx("div", { className: "e-error", children: formState.errors[component.id] })] })] }) }) }, component.id));
558
+ }, locale: formLocale, onBlur: () => { if (component.type !== 'dataGrid' && component.type !== 'richTextEditor' && component.type !== 'staticHtml')
559
+ formState?.onBlur(component.id); }, ref: setInputRef, hasError: hasError })), hasError && _jsx("div", { className: "e-error", children: formState.errors[component.id] })] }), component.description && _jsx("div", { className: 'field-description', children: component.description })] }) }) }, component.id));
439
560
  };
440
561
  /**
441
562
  * Render component (including layouts)
442
563
  */
443
564
  const renderComponent = (component) => {
444
- // Skip invisible components
445
- if (component.visible === false)
565
+ const formValues = formState?.values || {};
566
+ const isVisible = component.visible === false
567
+ ? false
568
+ : ConditionalRuleEngine.isFieldVisible(component.conditions, formValues);
569
+ const isHidden = ConditionalRuleEngine.isFieldHidden(component.conditions, formValues);
570
+ if (!isVisible || isHidden) {
571
+ if (formState && formState.values[component.id] !== undefined && formState.values[component.id] !== null) {
572
+ formState.onChange(component.id, { value: null });
573
+ }
446
574
  return null;
575
+ }
447
576
  switch (component.type) {
448
577
  case 'panel':
449
578
  return (_jsxs("fieldset", { className: `form-panel-renderer ${component.cssClass}`, children: [(component.label && !component.hideLabel) && _jsx("legend", { className: `${!component.hideBorders ? 'panel-legend' : ''}`, children: component.label }), _jsx("div", { className: "panel-content", children: component.children?.map(child => renderComponent(child)) })] }, component.id));
@@ -453,13 +582,15 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, onSubmit,
453
582
  const defaultColSpan = Math.floor(12 / tableCols);
454
583
  const columnWidthsProp = component.columnWidths;
455
584
  const splittedWidths = columnWidthsProp ? splitWidths(columnWidthsProp.widths) : [];
456
- return (_jsx("div", { className: `form-table-renderer ${component.hideBorders ? '' : 'table-border'} ${component.cssClass}`, children: Array.from({ length: tableRows }).map((_, rowIndex) => (_jsx("div", { className: "table-row", children: Array.from({ length: tableCols }).map((_, colIndex) => {
585
+ return (_jsx("div", { className: `form-table-renderer ${component.hideBorders ? '' : 'table-border'} ${component.cssClass}`, children: Array.from({ length: tableRows }).map((_, rowIndex) => (_jsx("div", { className: "e-form-table-row", style: component.hideBorders ? { display: 'flex', gap: '1.5rem' } : {}, children: Array.from({ length: tableCols }).map((_, colIndex) => {
457
586
  const cellComponents = component.tableCells?.[rowIndex]?.[colIndex] || [];
458
587
  const cellSpan = defaultColSpan;
459
588
  const colWidthValue = splittedWidths[colIndex];
460
- const cellStyle = colWidthValue ? { width: `${colWidthValue}%` } : {};
589
+ const isSingleColumn = tableCols === 1;
590
+ const cellStyle = isSingleColumn ? { width: '100%' } : (colWidthValue ? { width: `${colWidthValue}%` } : {});
461
591
  const allNullWidths = !columnWidthsProp || (Array.isArray(columnWidthsProp.widths) && columnWidthsProp.widths.every((w) => w === null));
462
- return (_jsx("div", { className: `table-cell ${colWidthValue ? '' : allNullWidths ? `col-${cellSpan}` : ''}`, style: cellStyle, children: cellComponents.map(comp => renderComponent(comp)) }, `cell-${rowIndex}-${colIndex}`));
592
+ const isFirstRow = rowIndex === 0;
593
+ return (_jsx("div", { className: `e-form-table-cell ${colWidthValue ? '' : allNullWidths ? `col-${cellSpan}` : ''}`, style: component.hideBorders ? { ...cellStyle, padding: isFirstRow ? '0' : '12px 0 0 0' } : cellStyle, children: cellComponents.map(comp => renderComponent(comp)) }, `cell-${rowIndex}-${colIndex}`));
463
594
  }) }, `row-${rowIndex}`))) }, component.id));
464
595
  case 'tabs':
465
596
  return (_jsx("div", { className: `form-tabs-renderer ${component.cssClass || ''}`, children: _jsx(CustomTabComponent, { tabs: component.tabItems?.map(tab => ({
@@ -468,15 +599,13 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, onSubmit,
468
599
  })) || [], className: component.cssClass }) }, component.id));
469
600
  case 'card':
470
601
  return (_jsxs("fieldset", { className: `e-card form-card-renderer ${component.cssClass}`, style: { padding: 0 }, children: [_jsx("div", { className: "e-card-header", children: _jsxs("div", { className: "e-card-header-caption", children: [_jsx("div", { className: "e-card-title", children: component.cardTitle || component.label || 'Card Title' }), component.cardSubtitle && (_jsx("div", { className: "e-card-sub-title", children: component.cardSubtitle }))] }) }), _jsx("div", { className: "e-card-content card-content", children: component.children?.map(child => renderComponent(child)) })] }, component.id));
471
- case 'statichtml':
602
+ case 'staticHtml':
472
603
  return (_jsx(StaticHtmlRenderer, { id: component.id, content: component.defaultValue || '<p>HTML content</p>', enableHtmlSanitizer: component.enableHtmlSanitizer }, component.id));
473
604
  case 'message':
474
- return (_jsx("div", { className: "form-group", children: renderFormField({
475
- component,
476
- value: undefined,
477
- onChange: () => { },
478
- }) }, component.id));
605
+ return (_jsx("div", { className: "form-group", children: _jsx(RenderFormField, { component: component, locale: formLocale, value: undefined, onChange: () => { } }) }, component.id));
479
606
  case 'button':
607
+ const buttonlDisabled = ConditionalRuleEngine.isFieldDisabled(component.conditions, formValues);
608
+ const buttonComputed = component.disabled || buttonlDisabled;
480
609
  const handleButtonClick = (event) => {
481
610
  if (onButtonClick) {
482
611
  onButtonClick({
@@ -485,21 +614,17 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, onSubmit,
485
614
  });
486
615
  }
487
616
  };
488
- return (_jsx("div", { className: "form-group", children: renderFormField({
489
- component,
490
- value: undefined,
491
- onChange: () => { },
492
- onClick: (e) => { handleButtonClick(e); },
493
- ref: (el, idx) => {
617
+ return (_jsx("div", { className: "form-group", children: _jsx(RenderFormField, { component: { ...component, disabled: buttonComputed }, value: undefined, onChange: () => { }, locale: formLocale, onClick: (e) => { handleButtonClick(e); }, ref: (el, idx) => {
494
618
  if (idx !== undefined) {
495
619
  inputRefs.current[`${component.id}_${idx}`] = el;
496
620
  }
497
621
  else {
498
622
  inputRefs.current[component.id] = el;
499
623
  }
500
- }
501
- }) }, component.id));
502
- case 'splitbutton':
624
+ } }) }, component.id));
625
+ case 'splitButton':
626
+ const splitButtonDisabled = ConditionalRuleEngine.isFieldDisabled(component.conditions, formValues);
627
+ const computedDisabled = component.disabled || splitButtonDisabled;
503
628
  setInputRef = (el) => {
504
629
  if (el) {
505
630
  inputRefs.current[component.id] = el;
@@ -514,13 +639,7 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, onSubmit,
514
639
  });
515
640
  }
516
641
  };
517
- return (_jsx("div", { className: "form-group", children: renderFormField({
518
- component,
519
- value: undefined,
520
- onChange: () => { },
521
- onClick: (e) => { handleSplitButtonEvent(e); },
522
- ref: setInputRef
523
- }) }, component.id));
642
+ return (_jsx("div", { className: "form-group", children: _jsx(RenderFormField, { component: { ...component, disabled: computedDisabled }, value: undefined, locale: formLocale, onChange: () => { }, onClick: (e) => { handleSplitButtonEvent(e); }, ref: setInputRef }) }, component.id));
524
643
  default:
525
644
  return renderField(component);
526
645
  }
@@ -546,15 +665,36 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, onSubmit,
546
665
  },
547
666
  }), []);
548
667
  return (_jsx("div", { ...restProps, className: `form-renderer-container ${className}`, children: _jsx(Form, { rules: validationRules, initialValues: initialValues, onFormStateChange: (state) => setFormState(state), onSubmit: (data) => {
549
- const isValid = formState?.errors ? Object.keys(formState.errors).length === 0 : false;
668
+ const isValid = (formState?.errors ? Object.keys(formState.errors).length === 0 : false);
550
669
  const transformedData = {};
670
+ const formValues = formState?.values || {};
551
671
  // Exclude non-data components (buttons, messages, and layout-only components)
552
- const excludedTypes = new Set(['button', 'message', 'panel', 'table', 'tabs', 'card', 'splitbutton', 'statichtml']);
672
+ const excludedTypes = new Set(['button', 'message', 'panel', 'table', 'tabs']);
553
673
  const allComps = collectAllComponents(processedSchema.components || []);
554
674
  const excludedIds = new Set(allComps.filter(c => excludedTypes.has(c.type)).map(c => c.id));
675
+ const hiddenFieldIds = new Set(allComps.filter(c => {
676
+ if (c.visible === false)
677
+ return true;
678
+ if (c.conditions?.visibleWhen) {
679
+ if (!ConditionalRuleEngine.isFieldVisible(c.conditions, formValues))
680
+ return true;
681
+ }
682
+ if (c.conditions?.hideWhen) {
683
+ if (ConditionalRuleEngine.isFieldHidden(c.conditions, formValues))
684
+ return true;
685
+ }
686
+ return false;
687
+ }).map(c => c.id));
688
+ const disabledFieldIds = new Set(allComps.filter(c => {
689
+ if (c.disabled)
690
+ return true;
691
+ if (c.conditions?.disabledWhen) {
692
+ return ConditionalRuleEngine.isFieldDisabled(c.conditions, formValues);
693
+ }
694
+ return false;
695
+ }).map(c => c.id));
555
696
  Object.entries(data).forEach(([id, val]) => {
556
- // Skip excluded components so they don't appear as submitted values
557
- if (excludedIds.has(id))
697
+ if (excludedIds.has(id) || hiddenFieldIds.has(id) || disabledFieldIds.has(id))
558
698
  return;
559
699
  const label = idToLabel[id] || id;
560
700
  transformedData[label] = val;