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

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.
Files changed (181) hide show
  1. package/README.md +88 -105
  2. package/diConfig.json +85 -0
  3. package/dist/ej2-react-form-renderer.min.js +10 -0
  4. package/dist/ej2-react-form-renderer.umd.min.js +11 -0
  5. package/dist/ej2-react-form-renderer.umd.min.js.map +1 -0
  6. package/dist/es6/ej2-react-form-renderer.es2015.js +37 -0
  7. package/dist/es6/ej2-react-form-renderer.es2015.js.map +1 -0
  8. package/form-renderer.d.ts +4 -0
  9. package/form-renderer.js +4 -0
  10. package/index.d.ts +2 -4
  11. package/index.js +2 -4
  12. package/license +6 -13
  13. package/package.json +19 -38
  14. package/src/form-renderer/formrenderer.component.d.ts +34 -0
  15. package/src/form-renderer/formrenderer.component.js +49 -0
  16. package/src/form-renderer/index.d.ts +1 -0
  17. package/src/form-renderer/index.js +1 -0
  18. package/src/global.js +1 -0
  19. package/src/index.d.ts +2 -6
  20. package/src/index.js +2 -6
  21. package/styles/bds-lite.css +1 -0
  22. package/styles/bds-lite.scss +1 -0
  23. package/styles/bds.css +1 -0
  24. package/styles/bds.scss +1 -0
  25. package/styles/bootstrap-dark-lite.css +1 -0
  26. package/styles/bootstrap-dark-lite.scss +1 -0
  27. package/styles/bootstrap-dark.css +1 -26
  28. package/styles/bootstrap-dark.scss +1 -0
  29. package/styles/bootstrap-lite.css +1 -0
  30. package/styles/bootstrap-lite.scss +1 -0
  31. package/styles/bootstrap.css +1 -26
  32. package/styles/bootstrap.scss +1 -0
  33. package/styles/bootstrap4-lite.css +1 -0
  34. package/styles/bootstrap4-lite.scss +1 -0
  35. package/styles/bootstrap4.css +1 -26
  36. package/styles/bootstrap4.scss +1 -0
  37. package/styles/bootstrap5-dark-lite.css +1 -0
  38. package/styles/bootstrap5-dark-lite.scss +1 -0
  39. package/styles/bootstrap5-dark.css +1 -26
  40. package/styles/bootstrap5-dark.scss +1 -0
  41. package/styles/bootstrap5-lite.css +1 -0
  42. package/styles/bootstrap5-lite.scss +1 -0
  43. package/styles/bootstrap5.3-lite.css +1 -0
  44. package/styles/bootstrap5.3-lite.scss +1 -0
  45. package/styles/bootstrap5.3.css +1 -26
  46. package/styles/bootstrap5.3.scss +1 -0
  47. package/styles/bootstrap5.css +1 -26
  48. package/styles/bootstrap5.scss +1 -0
  49. package/styles/fabric-dark-lite.css +1 -0
  50. package/styles/fabric-dark-lite.scss +1 -0
  51. package/styles/fabric-dark.css +1 -0
  52. package/styles/fabric-dark.scss +1 -0
  53. package/styles/fabric-lite.css +1 -0
  54. package/styles/fabric-lite.scss +1 -0
  55. package/styles/fabric.css +1 -0
  56. package/styles/fabric.scss +1 -0
  57. package/styles/fluent-dark-lite.css +1 -0
  58. package/styles/fluent-dark-lite.scss +1 -0
  59. package/styles/fluent-dark.css +1 -26
  60. package/styles/fluent-dark.scss +1 -0
  61. package/styles/fluent-lite.css +1 -0
  62. package/styles/fluent-lite.scss +1 -0
  63. package/styles/fluent.css +1 -26
  64. package/styles/fluent.scss +1 -0
  65. package/styles/fluent2-lite.css +1 -0
  66. package/styles/fluent2-lite.scss +1 -0
  67. package/styles/fluent2.css +1 -26
  68. package/styles/fluent2.scss +1 -0
  69. package/styles/form-renderer/bds.css +1 -0
  70. package/styles/form-renderer/bds.scss +1 -0
  71. package/styles/form-renderer/bootstrap-dark.css +1 -0
  72. package/styles/form-renderer/bootstrap-dark.scss +1 -0
  73. package/styles/form-renderer/bootstrap.css +1 -0
  74. package/styles/form-renderer/bootstrap.scss +1 -0
  75. package/styles/form-renderer/bootstrap4.css +1 -0
  76. package/styles/form-renderer/bootstrap4.scss +1 -0
  77. package/styles/form-renderer/bootstrap5-dark.css +1 -0
  78. package/styles/form-renderer/bootstrap5-dark.scss +1 -0
  79. package/styles/form-renderer/bootstrap5.3.css +1 -0
  80. package/styles/form-renderer/bootstrap5.3.scss +1 -0
  81. package/styles/form-renderer/bootstrap5.css +1 -0
  82. package/styles/form-renderer/bootstrap5.scss +1 -0
  83. package/styles/form-renderer/fabric-dark.css +1 -0
  84. package/styles/form-renderer/fabric-dark.scss +1 -0
  85. package/styles/form-renderer/fabric.css +1 -0
  86. package/styles/form-renderer/fabric.scss +1 -0
  87. package/styles/form-renderer/fluent-dark.css +1 -0
  88. package/styles/form-renderer/fluent-dark.scss +1 -0
  89. package/styles/form-renderer/fluent.css +1 -0
  90. package/styles/form-renderer/fluent.scss +1 -0
  91. package/styles/form-renderer/fluent2.css +1 -0
  92. package/styles/form-renderer/fluent2.scss +1 -0
  93. package/styles/form-renderer/highcontrast-light.css +1 -0
  94. package/styles/form-renderer/highcontrast-light.scss +1 -0
  95. package/styles/form-renderer/highcontrast.css +1 -0
  96. package/styles/form-renderer/highcontrast.scss +1 -0
  97. package/styles/form-renderer/material-dark.css +1 -0
  98. package/styles/form-renderer/material-dark.scss +1 -0
  99. package/styles/form-renderer/material.css +1 -0
  100. package/styles/form-renderer/material.scss +1 -0
  101. package/styles/form-renderer/material3-dark.css +1 -0
  102. package/styles/form-renderer/material3-dark.scss +1 -0
  103. package/styles/form-renderer/material3.css +1 -0
  104. package/styles/form-renderer/material3.scss +1 -0
  105. package/styles/form-renderer/tailwind-dark.css +1 -0
  106. package/styles/form-renderer/tailwind-dark.scss +1 -0
  107. package/styles/form-renderer/tailwind.css +1 -0
  108. package/styles/form-renderer/tailwind.scss +1 -0
  109. package/styles/form-renderer/tailwind3.css +1 -0
  110. package/styles/form-renderer/tailwind3.scss +1 -0
  111. package/styles/highcontrast-light-lite.css +1 -0
  112. package/styles/highcontrast-light-lite.scss +1 -0
  113. package/styles/highcontrast-light.css +1 -0
  114. package/styles/highcontrast-light.scss +1 -0
  115. package/styles/highcontrast-lite.css +1 -0
  116. package/styles/highcontrast-lite.scss +1 -0
  117. package/styles/highcontrast.css +1 -0
  118. package/styles/highcontrast.scss +1 -0
  119. package/styles/material-dark-lite.css +1 -0
  120. package/styles/material-dark-lite.scss +1 -0
  121. package/styles/material-dark.css +1 -26
  122. package/styles/material-dark.scss +1 -0
  123. package/styles/material-lite.css +1 -0
  124. package/styles/material-lite.scss +1 -0
  125. package/styles/material.css +1 -26
  126. package/styles/material.scss +1 -0
  127. package/styles/material3-dark-lite.css +1 -0
  128. package/styles/material3-dark-lite.scss +1 -0
  129. package/styles/material3-dark.css +1 -26
  130. package/styles/material3-dark.scss +1 -0
  131. package/styles/material3-lite.css +1 -0
  132. package/styles/material3-lite.scss +1 -0
  133. package/styles/material3.css +1 -26
  134. package/styles/material3.scss +1 -0
  135. package/styles/tailwind-dark-lite.css +1 -0
  136. package/styles/tailwind-dark-lite.scss +1 -0
  137. package/styles/tailwind-dark.css +1 -26
  138. package/styles/tailwind-dark.scss +1 -0
  139. package/styles/tailwind-lite.css +1 -0
  140. package/styles/tailwind-lite.scss +1 -0
  141. package/styles/tailwind.css +1 -26
  142. package/styles/tailwind.scss +1 -0
  143. package/styles/tailwind3-lite.css +1 -0
  144. package/styles/tailwind3-lite.scss +1 -0
  145. package/styles/tailwind3.css +1 -26
  146. package/styles/tailwind3.scss +1 -0
  147. package/src/CheckboxGroup.d.ts +0 -12
  148. package/src/CheckboxGroup.js +0 -49
  149. package/src/ConditionalRuleEngine.d.ts +0 -295
  150. package/src/ConditionalRuleEngine.js +0 -566
  151. package/src/CustomTabComponent.d.ts +0 -13
  152. package/src/CustomTabComponent.js +0 -35
  153. package/src/DataGridComponent.d.ts +0 -28
  154. package/src/DataGridComponent.js +0 -147
  155. package/src/FormRenderer.d.ts +0 -84
  156. package/src/FormRenderer.js +0 -707
  157. package/src/FormValidator.d.ts +0 -479
  158. package/src/FormValidator.js +0 -579
  159. package/src/MinMaxRuleEngine.d.ts +0 -95
  160. package/src/MinMaxRuleEngine.js +0 -361
  161. package/src/StaticHtmlRenderer.d.ts +0 -8
  162. package/src/StaticHtmlRenderer.js +0 -21
  163. package/src/expressions/DependencyTracker.d.ts +0 -30
  164. package/src/expressions/DependencyTracker.js +0 -128
  165. package/src/expressions/ExpressionEngine.d.ts +0 -81
  166. package/src/expressions/ExpressionEngine.js +0 -503
  167. package/src/expressions/ExpressionParser.d.ts +0 -39
  168. package/src/expressions/ExpressionParser.js +0 -270
  169. package/src/json-converter.d.ts +0 -171
  170. package/src/json-converter.js +0 -1209
  171. package/src/parser.d.ts +0 -99
  172. package/src/parser.js +0 -753
  173. package/src/renderFormField.d.ts +0 -21
  174. package/src/renderFormField.js +0 -226
  175. package/src/types/FormSchema.d.ts +0 -211
  176. package/src/types/FormSchema.js +0 -1
  177. package/src/utils.js +0 -408
  178. package/styles/bootstrap5.3-dark.css +0 -26
  179. package/styles/fluent2-dark.css +0 -26
  180. package/styles/form-renderer.base.css +0 -499
  181. package/styles/tailwind3-dark.css +0 -10
@@ -1,707 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /**
3
- * Form Renderer Component
4
- * Renders JSON-driven forms with React FormValidator
5
- */
6
- import React, { useState, useEffect, useRef, useCallback } from 'react';
7
- import { Form, FormField } from './FormValidator';
8
- import { enableRipple } from '@syncfusion/ej2-base';
9
- import { RenderFormField } from './renderFormField';
10
- import { CustomTabComponent } from './CustomTabComponent';
11
- import { mergeSchema } from './json-converter';
12
- import { UniversalJsonParser } from './parser';
13
- import { TooltipComponent } from '@syncfusion/ej2-react-popups';
14
- import { VALIDATION_REGEX } from './FormValidator';
15
- import StaticHtmlRenderer from './StaticHtmlRenderer';
16
- import { formatDateValues, splitWidths } from './utils';
17
- import { ConditionalRuleEngine } from './ConditionalRuleEngine';
18
- import { ExpressionEngine } from './expressions/ExpressionEngine';
19
- import { MinMaxRuleEngine } from './MinMaxRuleEngine';
20
- enableRipple(true);
21
- /**
22
- * Form layout options (only applies when UI schema is NOT provided)
23
- */
24
- export const FormLayout = {
25
- SingleColumn: 'single',
26
- TwoColumn: 'twocolumn',
27
- ThreeColumn: 'threecolumn',
28
- FourColumn: 'fourcolumn'
29
- };
30
- // ForwardRef to expose public methods
31
- export const FormRenderer = React.forwardRef(({ schema, locale, onSubmit, onChange, onButtonClick, className = '', layout = FormLayout.SingleColumn, created, ...restProps }, ref) => {
32
- const processedSchema = React.useMemo(() => {
33
- try {
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: [] };
45
- }
46
- }
47
- const isUnifiedSchema = parsedInput && typeof parsedInput === 'object' &&
48
- parsedInput.properties && Array.isArray(parsedInput.layout);
49
- if (isUnifiedSchema) {
50
- return mergeSchema(parsedInput);
51
- }
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: [] };
61
- }
62
- }
63
- }
64
- catch (error) {
65
- console.error('FormRenderer schema processing error:', error);
66
- return { version: '1.0.0', components: [] };
67
- }
68
- }, [schema]);
69
- const [formState, setFormState] = useState(undefined);
70
- const isInitializedRef = useRef(false);
71
- const formLocale = locale || 'en-US';
72
- // Attach ref to the input element for getComponent
73
- let setInputRef;
74
- // Store input component refs by name/id
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());
84
- // Check if UI schema has layout components (table, tabs, or panel)
85
- const hasLayoutComponents = React.useMemo(() => {
86
- if (!processedSchema.components)
87
- return false;
88
- const checkForLayoutTypes = (components) => {
89
- for (const comp of components) {
90
- if (['table', 'tabs', 'panel', 'card'].includes(comp.type)) {
91
- return true;
92
- }
93
- if (comp.children && checkForLayoutTypes(comp.children)) {
94
- return true;
95
- }
96
- if (comp.tableCells) {
97
- for (const row of comp.tableCells) {
98
- for (const cell of row) {
99
- if (checkForLayoutTypes(cell))
100
- return true;
101
- }
102
- }
103
- }
104
- if (comp.tabItems) {
105
- for (const tab of comp.tabItems) {
106
- if (tab.content && checkForLayoutTypes(tab.content))
107
- return true;
108
- }
109
- }
110
- }
111
- return false;
112
- };
113
- return checkForLayoutTypes(processedSchema.components);
114
- }, [processedSchema]);
115
- // Determine layout class (only apply if UI schema does NOT have table/tabs/panels)
116
- const layoutClass = React.useMemo(() => {
117
- if (hasLayoutComponents)
118
- return ''; // Ignore layout if UI schema has table/tabs/panels
119
- // Map enum to CSS class names
120
- switch (layout) {
121
- case FormLayout.TwoColumn:
122
- return 'twolayout';
123
- case FormLayout.ThreeColumn:
124
- return 'threelayout';
125
- case FormLayout.FourColumn:
126
- return 'fourlayout';
127
- case FormLayout.SingleColumn:
128
- default:
129
- return ''; // Default single column (no class needed)
130
- }
131
- }, [layout, hasLayoutComponents]);
132
- /**
133
- * Recursively collect all components including nested ones
134
- */
135
- const collectAllComponents = useCallback((components) => {
136
- const allComponents = [];
137
- const traverse = (comps) => {
138
- comps.forEach(comp => {
139
- allComponents.push(comp);
140
- // Handle nested components in panels
141
- if (comp.children && comp.children.length > 0) {
142
- traverse(comp.children);
143
- }
144
- // Handle components in table cells
145
- if (comp.tableCells) {
146
- comp.tableCells.forEach(row => {
147
- row.forEach(cell => {
148
- traverse(cell);
149
- });
150
- });
151
- }
152
- // Handle components in tabs
153
- if (comp.tabItems) {
154
- comp.tabItems.forEach(tab => {
155
- if (tab.content) {
156
- traverse(tab.content);
157
- }
158
- });
159
- }
160
- });
161
- };
162
- traverse(components);
163
- return allComponents;
164
- }, []);
165
- const idToLabel = React.useMemo(() => {
166
- const map = {};
167
- const traverse = (comps) => {
168
- comps.forEach(comp => {
169
- if (comp.label && !['panel', 'table', 'tabs', 'card', 'button', 'message', 'splitButton'].includes(comp.type)) {
170
- map[comp.id] = comp.label;
171
- }
172
- if (comp.children)
173
- traverse(comp.children);
174
- if (comp.tableCells) {
175
- comp.tableCells.forEach(row => {
176
- row.forEach(cell => traverse(cell));
177
- });
178
- }
179
- if (comp.tabItems) {
180
- comp.tabItems.forEach(tab => {
181
- if (tab.content)
182
- traverse(tab.content);
183
- });
184
- }
185
- });
186
- };
187
- traverse(processedSchema.components);
188
- return map;
189
- }, [processedSchema]);
190
- /**
191
- * Build EJ2 FormValidator rules from schema
192
- */
193
- const buildValidationRules = useCallback(() => {
194
- const rules = {};
195
- const formValues = formState?.values || {};
196
- const collectRules = (components) => {
197
- components.forEach((component) => {
198
- if (component) {
199
- const componentRules = {};
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) {
207
- componentRules.required = [true, component.validationMessage || `${component.label} is required`];
208
- }
209
- if (component.minLength && !finalDisabled && !finalReadOnly) {
210
- componentRules.minLength = [component.minLength, `Minimum ${component.minLength} characters required`];
211
- }
212
- if (component.maxLength && !finalDisabled && !finalReadOnly) {
213
- componentRules.maxLength = [component.maxLength, `Maximum ${component.maxLength} characters allowed`];
214
- }
215
- if (component?.type === 'textbox' && component?.textboxType && !component?.regex && !componentRules.regex && !finalDisabled && !finalReadOnly) {
216
- if (component.textboxType === 'email') {
217
- componentRules.regex = [VALIDATION_REGEX['EMAIL'], component?.validationMessage || 'Invalid format'];
218
- }
219
- else if (component.textboxType === 'url') {
220
- componentRules.regex = [VALIDATION_REGEX['URL'], component?.validationMessage || 'Invalid format'];
221
- }
222
- }
223
- if (component.regex && !finalDisabled && !finalReadOnly) {
224
- componentRules.regex = [component.regex, component.validationMessage || 'Invalid format'];
225
- }
226
- if (Object.keys(componentRules).length > 0) {
227
- rules[component.id] = componentRules;
228
- }
229
- }
230
- // Recursively collect rules from nested components
231
- if (component.children) {
232
- collectRules(component.children);
233
- }
234
- if (component.tableCells) {
235
- component.tableCells.forEach(row => {
236
- row.forEach(cell => collectRules(cell));
237
- });
238
- }
239
- if (component.tabItems) {
240
- component.tabItems.forEach(tab => {
241
- if (tab.content)
242
- collectRules(tab.content);
243
- });
244
- }
245
- });
246
- };
247
- collectRules(processedSchema.components);
248
- return rules;
249
- }, [processedSchema, formState?.values]);
250
- const validationRules = buildValidationRules();
251
- /**
252
- * Build initial form values from component defaultValues
253
- */
254
- const initialValues = React.useMemo(() => {
255
- const values = {};
256
- const allComponents = collectAllComponents(processedSchema.components);
257
- const componentIds = new Set();
258
- allComponents.forEach(comp => {
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') {
261
- componentIds.add(comp.id);
262
- return;
263
- }
264
- if (comp.defaultValue !== undefined) {
265
- values[comp.id] = comp.defaultValue;
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
- }
276
- componentIds.add(comp.id);
277
- });
278
- if (formState) {
279
- Object.keys(formState.values).filter(key => !componentIds.has(key)).forEach(key => {
280
- delete formState.values[key];
281
- });
282
- }
283
- return values;
284
- }, [processedSchema, collectAllComponents, schema]);
285
- // Update the useEffect that sets default values
286
- useEffect(() => {
287
- if (formState && !isInitializedRef.current) {
288
- isInitializedRef.current = true;
289
- }
290
- if (formState && initialValues) {
291
- Object.keys(formState.values).filter(key => !(key in initialValues));
292
- Object.entries(initialValues).forEach(([id, value]) => {
293
- if (!formState.values[id]) {
294
- formState.values[id] = value;
295
- }
296
- });
297
- }
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]);
460
- /**
461
- * Render a single field with label and error
462
- */
463
- const renderField = (component) => {
464
- const formValues = formState?.values || {};
465
- // Skip layout and button components (handled separately)
466
- if (['panel', 'table', 'tabs', 'card', 'message', 'button', 'splitButton', 'staticHtml'].includes(component.type)) {
467
- return null;
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
- }
491
- const labelPos = component.labelPosition || 'top';
492
- const labelWidth = component.labelWidth || 'auto';
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)) {
495
- // For radio, store each option's ref with unique key
496
- setInputRef = (el, idx) => {
497
- if (el && typeof idx === 'number') {
498
- inputRefs.current[`${component.id}_${idx}`] = el;
499
- }
500
- };
501
- }
502
- else {
503
- setInputRef = (el) => {
504
- if (el) {
505
- inputRefs.current[component.id] = el;
506
- }
507
- };
508
- }
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: {
527
- cursor: 'pointer',
528
- display: 'flex',
529
- alignItems: 'center',
530
- justifyContent: 'center',
531
- flexShrink: 0,
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) => {
534
- formState?.onChange(component.id, { value });
535
- if (onChange) {
536
- onChange({ field: component.label || component.id, value });
537
- }
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);
553
- formState?.onChange(component.id, { value });
554
- if (onChange) {
555
- onChange({ field: component.label || component.id, value });
556
- }
557
- }
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));
560
- };
561
- /**
562
- * Render component (including layouts)
563
- */
564
- const renderComponent = (component) => {
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
- }
574
- return null;
575
- }
576
- switch (component.type) {
577
- case 'panel':
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));
579
- case 'table':
580
- const tableRows = component.rows || (component.tableCells ? component.tableCells.length : 2);
581
- const tableCols = component.columns || (component.tableCells && component.tableCells[0] ? component.tableCells[0].length : 2);
582
- const defaultColSpan = Math.floor(12 / tableCols);
583
- const columnWidthsProp = component.columnWidths;
584
- const splittedWidths = columnWidthsProp ? splitWidths(columnWidthsProp.widths) : [];
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) => {
586
- const cellComponents = component.tableCells?.[rowIndex]?.[colIndex] || [];
587
- const cellSpan = defaultColSpan;
588
- const colWidthValue = splittedWidths[colIndex];
589
- const isSingleColumn = tableCols === 1;
590
- const cellStyle = isSingleColumn ? { width: '100%' } : (colWidthValue ? { width: `${colWidthValue}%` } : {});
591
- const allNullWidths = !columnWidthsProp || (Array.isArray(columnWidthsProp.widths) && columnWidthsProp.widths.every((w) => w === null));
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}`));
594
- }) }, `row-${rowIndex}`))) }, component.id));
595
- case 'tabs':
596
- return (_jsx("div", { className: `form-tabs-renderer ${component.cssClass || ''}`, children: _jsx(CustomTabComponent, { tabs: component.tabItems?.map(tab => ({
597
- header: tab.header,
598
- content: (_jsx("div", { className: "tab-content-renderer", children: tab.content?.map(comp => renderComponent(comp)) }))
599
- })) || [], className: component.cssClass }) }, component.id));
600
- case 'card':
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));
602
- case 'staticHtml':
603
- return (_jsx(StaticHtmlRenderer, { id: component.id, content: component.defaultValue || '<p>HTML content</p>', enableHtmlSanitizer: component.enableHtmlSanitizer }, component.id));
604
- case 'message':
605
- return (_jsx("div", { className: "form-group", children: _jsx(RenderFormField, { component: component, locale: formLocale, value: undefined, onChange: () => { } }) }, component.id));
606
- case 'button':
607
- const buttonlDisabled = ConditionalRuleEngine.isFieldDisabled(component.conditions, formValues);
608
- const buttonComputed = component.disabled || buttonlDisabled;
609
- const handleButtonClick = (event) => {
610
- if (onButtonClick) {
611
- onButtonClick({
612
- buttonId: component.id,
613
- buttonLabel: component.buttonsGroups ? event.target?.ej2_instances[0].content : component.label || '', event
614
- });
615
- }
616
- };
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) => {
618
- if (idx !== undefined) {
619
- inputRefs.current[`${component.id}_${idx}`] = el;
620
- }
621
- else {
622
- inputRefs.current[component.id] = el;
623
- }
624
- } }) }, component.id));
625
- case 'splitButton':
626
- const splitButtonDisabled = ConditionalRuleEngine.isFieldDisabled(component.conditions, formValues);
627
- const computedDisabled = component.disabled || splitButtonDisabled;
628
- setInputRef = (el) => {
629
- if (el) {
630
- inputRefs.current[component.id] = el;
631
- }
632
- };
633
- const handleSplitButtonEvent = (event) => {
634
- if (onButtonClick) {
635
- onButtonClick({
636
- buttonId: component.id,
637
- buttonLabel: component.label || '',
638
- event
639
- });
640
- }
641
- };
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));
643
- default:
644
- return renderField(component);
645
- }
646
- };
647
- // Notify when all form elements and refs are rendered
648
- useEffect(() => {
649
- if (created && formState) {
650
- setTimeout(() => {
651
- const hasInputs = Object.keys(inputRefs.current).length > 0;
652
- if (hasInputs) {
653
- created();
654
- }
655
- }, 0);
656
- }
657
- }, [created, formState]);
658
- // Expose getComponent as a public method
659
- React.useImperativeHandle(ref, () => ({
660
- /**
661
- * Get the instance of the form input by name/id
662
- */
663
- getComponent: (name) => {
664
- return inputRefs.current[name] || null;
665
- },
666
- }), []);
667
- return (_jsx("div", { ...restProps, className: `form-renderer-container ${className}`, children: _jsx(Form, { rules: validationRules, initialValues: initialValues, onFormStateChange: (state) => setFormState(state), onSubmit: (data) => {
668
- const isValid = (formState?.errors ? Object.keys(formState.errors).length === 0 : false);
669
- const transformedData = {};
670
- const formValues = formState?.values || {};
671
- // Exclude non-data components (buttons, messages, and layout-only components)
672
- const excludedTypes = new Set(['button', 'message', 'panel', 'table', 'tabs']);
673
- const allComps = collectAllComponents(processedSchema.components || []);
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));
696
- Object.entries(data).forEach(([id, val]) => {
697
- if (excludedIds.has(id) || hiddenFieldIds.has(id) || disabledFieldIds.has(id))
698
- return;
699
- const label = idToLabel[id] || id;
700
- transformedData[label] = val;
701
- });
702
- if (onSubmit) {
703
- onSubmit({ data: transformedData, isValid });
704
- }
705
- }, validateOnChange: true, className: `form-renderer ${layoutClass}`, children: processedSchema.components.map(component => renderComponent(component)) }) }));
706
- });
707
- export default FormRenderer;