@syncfusion/ej2-react-form-renderer 0.1.1-beta → 34.2.2

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 (173) hide show
  1. package/README.md +86 -110
  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 -48
  149. package/src/ConditionalRuleEngine.d.ts +0 -253
  150. package/src/ConditionalRuleEngine.js +0 -489
  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 -144
  155. package/src/FormRenderer.d.ts +0 -90
  156. package/src/FormRenderer.js +0 -649
  157. package/src/FormValidator.d.ts +0 -479
  158. package/src/FormValidator.js +0 -579
  159. package/src/StaticHtmlRenderer.d.ts +0 -8
  160. package/src/StaticHtmlRenderer.js +0 -21
  161. package/src/json-converter.d.ts +0 -180
  162. package/src/json-converter.js +0 -1329
  163. package/src/parser.d.ts +0 -99
  164. package/src/parser.js +0 -750
  165. package/src/renderFormField.d.ts +0 -20
  166. package/src/renderFormField.js +0 -203
  167. package/src/types/FormSchema.d.ts +0 -205
  168. package/src/types/FormSchema.js +0 -1
  169. package/src/utils.js +0 -146
  170. package/styles/bootstrap5.3-dark.css +0 -26
  171. package/styles/fluent2-dark.css +0 -26
  172. package/styles/form-renderer.base.css +0 -489
  173. package/styles/tailwind3-dark.css +0 -10
@@ -1,649 +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, convertComponentsToDataSchema, generateUIPropertiesFromData, generateLayoutFromData, generateDataSchemaFromUI } 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
- enableRipple(true);
19
- /**
20
- * Form layout options (only applies when UI schema is NOT provided)
21
- */
22
- export const FormLayout = {
23
- SingleColumn: 'single',
24
- TwoColumn: 'twocolumn',
25
- ThreeColumn: 'threecolumn',
26
- FourColumn: 'fourcolumn'
27
- };
28
- /**
29
- * Helper: Normalize DataSchema input
30
- * Converts any object to JSON string and parses with UniversalJsonParser
31
- * Handles FormSchema, DataSchema, or raw data - parser infers the format
32
- */
33
- function normalizeDataSchema(input) {
34
- try {
35
- // Convert object to JSON string
36
- const jsonString = JSON.stringify(input);
37
- const parser = new UniversalJsonParser();
38
- const parseResult = parser.parse(jsonString);
39
- if (parseResult.success && parseResult.schema) {
40
- const dataSchema = convertComponentsToDataSchema(parseResult.schema.components);
41
- // console.log(`✅ Parsed object as ${parseResult.metadata.detectedFormat} and converted to DataSchema`);
42
- return dataSchema;
43
- }
44
- else {
45
- console.error('❌ Failed to parse object:', parseResult.errors);
46
- return null;
47
- }
48
- }
49
- catch (error) {
50
- console.error('❌ DataSchema normalization error:', error);
51
- return null;
52
- }
53
- }
54
- /**
55
- * Helper: Parse string input using UniversalJsonParser
56
- * Returns DataSchema if parseable, otherwise null
57
- * Validates that UI schema was not accidentally provided
58
- */
59
- function parseStringToDataSchema(input) {
60
- try {
61
- // First check if user accidentally pasted UI schema in data schema field
62
- const testParse = JSON.parse(input);
63
- if (testParse.layout || (testParse.theme && testParse.properties && testParse.layout)) {
64
- 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.');
65
- return null;
66
- }
67
- const parser = new UniversalJsonParser();
68
- const parseResult = parser.parse(input);
69
- if (parseResult.success && parseResult.schema) {
70
- const dataSchema = convertComponentsToDataSchema(parseResult.schema.components);
71
- return dataSchema;
72
- }
73
- else {
74
- console.error('Failed to parse input:', parseResult.errors);
75
- return null;
76
- }
77
- }
78
- catch (error) {
79
- console.error('Parser error:', error);
80
- return null;
81
- }
82
- }
83
- /**
84
- * Helper: Parse string input as UISchema
85
- * Validates JSON structure matches UISchema format
86
- * Detects if data schema was accidentally provided
87
- */
88
- function parseStringToUISchema(input) {
89
- try {
90
- const parsed = JSON.parse(input);
91
- // Check if data schema was pasted in UI schema field
92
- if (parsed.properties && !parsed.layout && !parsed.theme) {
93
- const hasValidationProps = Object.values(parsed.properties).some((prop) => prop.required !== undefined || prop.minLength !== undefined || prop.maxLength !== undefined ||
94
- prop.minValue !== undefined || prop.maxValue !== undefined || prop.pattern !== undefined);
95
- if (hasValidationProps) {
96
- 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.');
97
- return null;
98
- }
99
- }
100
- // Validate UISchema structure
101
- if (!parsed.theme || !parsed.properties || !parsed.layout) {
102
- console.error('Invalid UISchema: missing theme, properties, or layout');
103
- return null;
104
- }
105
- return parsed;
106
- }
107
- catch (error) {
108
- console.error('JSON parse error:', error);
109
- return null;
110
- }
111
- }
112
- // ForwardRef to expose public methods
113
- export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, locale, onSubmit, onChange, onButtonClick, className = '', layout = FormLayout.SingleColumn, created, ...restProps }, ref) => {
114
- const processedSchema = React.useMemo(() => {
115
- try {
116
- // Parse and normalize data schema
117
- let parsedDataSchema = dataSchema
118
- ? ((typeof dataSchema === 'string'
119
- ? parseStringToDataSchema(dataSchema)
120
- : normalizeDataSchema(dataSchema)) || undefined)
121
- : undefined;
122
- // Parse and normalize UI schema
123
- let parsedUISchema = uiSchema
124
- ? (typeof uiSchema === 'string'
125
- ? parseStringToUISchema(uiSchema)
126
- : uiSchema) || undefined
127
- : undefined;
128
- // Scenario 1: Both dataSchema and uiSchema provided - full merge
129
- if (parsedDataSchema && parsedUISchema) {
130
- const uiFieldIds = new Set(Object.keys(parsedUISchema.properties));
131
- const dataFieldIds = Object.keys(parsedDataSchema.properties);
132
- const missingFields = dataFieldIds.filter(id => !uiFieldIds.has(id));
133
- if (missingFields.length > 0) {
134
- console.warn(`⚠️ Found ${missingFields.length} fields in data schema missing from UI schema`);
135
- }
136
- const merged = mergeSchema({ dataSchema: parsedDataSchema, uiSchema: parsedUISchema });
137
- return merged;
138
- }
139
- // Scenario 2: Only dataSchema provided - auto-generate UI schema
140
- if (parsedDataSchema && !parsedUISchema) {
141
- const autoUISchema = {
142
- properties: generateUIPropertiesFromData(parsedDataSchema),
143
- layout: generateLayoutFromData(parsedDataSchema)
144
- };
145
- const merged = mergeSchema({ dataSchema: parsedDataSchema, uiSchema: autoUISchema });
146
- return merged;
147
- }
148
- // Scenario 3: Only uiSchema provided - auto-generate data schema
149
- if (!parsedDataSchema && parsedUISchema) {
150
- const autoDataSchema = generateDataSchemaFromUI(parsedUISchema);
151
- const uiFieldIds = new Set(Object.keys(parsedUISchema.properties));
152
- const dataFieldIds = Object.keys(autoDataSchema.properties);
153
- const missingFields = dataFieldIds.filter(id => !uiFieldIds.has(id));
154
- if (missingFields.length > 0) {
155
- console.warn(`Found ${missingFields.length} fields in auto-generated data schema missing from UI schema`);
156
- }
157
- const merged = mergeSchema({ dataSchema: autoDataSchema, uiSchema: parsedUISchema });
158
- return merged;
159
- }
160
- // Scenario 4: Neither provided - return empty form
161
- console.warn('FormRenderer: No schema provided (dataSchema or uiSchema)');
162
- return { version: '0.1.0', theme: 'tailwind3', components: [] };
163
- }
164
- catch (error) {
165
- console.error('FormRenderer schema processing error:', error);
166
- return { version: '0.1.0', theme: 'tailwind3', components: [] };
167
- }
168
- }, [dataSchema, uiSchema]);
169
- const [formState, setFormState] = useState(undefined);
170
- const isInitializedRef = useRef(false);
171
- const formLocale = locale || 'en-US';
172
- // Attach ref to the input element for getComponent
173
- let setInputRef;
174
- // Store input component refs by name/id
175
- const inputRefs = useRef({});
176
- // Track which fields have active conditional values applied (to prevent re-applying while condition stays true)
177
- const conditionalValueAppliedRef = useRef({});
178
- // Check if UI schema has layout components (table, tabs, or panel)
179
- const hasLayoutComponents = React.useMemo(() => {
180
- if (!processedSchema.components)
181
- return false;
182
- const checkForLayoutTypes = (components) => {
183
- for (const comp of components) {
184
- if (['table', 'tabs', 'panel', 'card'].includes(comp.type)) {
185
- return true;
186
- }
187
- if (comp.children && checkForLayoutTypes(comp.children)) {
188
- return true;
189
- }
190
- if (comp.tableCells) {
191
- for (const row of comp.tableCells) {
192
- for (const cell of row) {
193
- if (checkForLayoutTypes(cell))
194
- return true;
195
- }
196
- }
197
- }
198
- if (comp.tabItems) {
199
- for (const tab of comp.tabItems) {
200
- if (tab.content && checkForLayoutTypes(tab.content))
201
- return true;
202
- }
203
- }
204
- }
205
- return false;
206
- };
207
- return checkForLayoutTypes(processedSchema.components);
208
- }, [processedSchema]);
209
- // Determine layout class (only apply if UI schema does NOT have table/tabs/panels)
210
- const layoutClass = React.useMemo(() => {
211
- if (hasLayoutComponents)
212
- return ''; // Ignore layout if UI schema has table/tabs/panels
213
- // Map enum to CSS class names
214
- switch (layout) {
215
- case FormLayout.TwoColumn:
216
- return 'twolayout';
217
- case FormLayout.ThreeColumn:
218
- return 'threelayout';
219
- case FormLayout.FourColumn:
220
- return 'fourlayout';
221
- case FormLayout.SingleColumn:
222
- default:
223
- return ''; // Default single column (no class needed)
224
- }
225
- }, [layout, hasLayoutComponents]);
226
- /**
227
- * Recursively collect all components including nested ones
228
- */
229
- const collectAllComponents = useCallback((components) => {
230
- const allComponents = [];
231
- const traverse = (comps) => {
232
- comps.forEach(comp => {
233
- allComponents.push(comp);
234
- // Handle nested components in panels
235
- if (comp.children && comp.children.length > 0) {
236
- traverse(comp.children);
237
- }
238
- // Handle components in table cells
239
- if (comp.tableCells) {
240
- comp.tableCells.forEach(row => {
241
- row.forEach(cell => {
242
- traverse(cell);
243
- });
244
- });
245
- }
246
- // Handle components in tabs
247
- if (comp.tabItems) {
248
- comp.tabItems.forEach(tab => {
249
- if (tab.content) {
250
- traverse(tab.content);
251
- }
252
- });
253
- }
254
- });
255
- };
256
- traverse(components);
257
- return allComponents;
258
- }, []);
259
- const idToLabel = React.useMemo(() => {
260
- const map = {};
261
- const traverse = (comps) => {
262
- comps.forEach(comp => {
263
- if (comp.label && !['panel', 'table', 'tabs', 'card', 'button', 'message', 'splitButton'].includes(comp.type)) {
264
- map[comp.id] = comp.label;
265
- }
266
- if (comp.children)
267
- traverse(comp.children);
268
- if (comp.tableCells) {
269
- comp.tableCells.forEach(row => {
270
- row.forEach(cell => traverse(cell));
271
- });
272
- }
273
- if (comp.tabItems) {
274
- comp.tabItems.forEach(tab => {
275
- if (tab.content)
276
- traverse(tab.content);
277
- });
278
- }
279
- });
280
- };
281
- traverse(processedSchema.components);
282
- return map;
283
- }, [processedSchema]);
284
- /**
285
- * Build EJ2 FormValidator rules from schema
286
- */
287
- const buildValidationRules = useCallback(() => {
288
- const rules = {};
289
- const formValues = formState?.values || {};
290
- const collectRules = (components) => {
291
- components.forEach((component) => {
292
- if (component) {
293
- const componentRules = {};
294
- const conditionalRequired = ConditionalRuleEngine.isFieldRequired(component.conditions, formValues);
295
- const conditionalDisabled = ConditionalRuleEngine.isFieldDisabled(component.conditions, formValues);
296
- const conditionalReadOnly = ConditionalRuleEngine.isFieldReadOnly(component.conditions, formValues);
297
- const finalRequired = component.required || conditionalRequired;
298
- const finalDisabled = component.disabled || conditionalDisabled;
299
- const finalReadOnly = component.readOnly || conditionalReadOnly;
300
- if (finalRequired && !finalDisabled && !finalReadOnly) {
301
- componentRules.required = [true, component.validationMessage || `${component.label} is required`];
302
- }
303
- if (component.minLength && !finalDisabled && !finalReadOnly) {
304
- componentRules.minLength = [component.minLength, `Minimum ${component.minLength} characters required`];
305
- }
306
- if (component.maxLength && !finalDisabled && !finalReadOnly) {
307
- componentRules.maxLength = [component.maxLength, `Maximum ${component.maxLength} characters allowed`];
308
- }
309
- if (component?.type === 'textbox' && component?.textboxType && !component?.regex && !componentRules.regex && !finalDisabled && !finalReadOnly) {
310
- if (component.textboxType === 'email') {
311
- componentRules.regex = [VALIDATION_REGEX['EMAIL'], component?.validationMessage || 'Invalid format'];
312
- }
313
- else if (component.textboxType === 'url') {
314
- componentRules.regex = [VALIDATION_REGEX['URL'], component?.validationMessage || 'Invalid format'];
315
- }
316
- }
317
- if (component.regex && !finalDisabled && !finalReadOnly) {
318
- componentRules.regex = [component.regex, component.validationMessage || 'Invalid format'];
319
- }
320
- if (Object.keys(componentRules).length > 0) {
321
- rules[component.id] = componentRules;
322
- }
323
- }
324
- // Recursively collect rules from nested components
325
- if (component.children) {
326
- collectRules(component.children);
327
- }
328
- if (component.tableCells) {
329
- component.tableCells.forEach(row => {
330
- row.forEach(cell => collectRules(cell));
331
- });
332
- }
333
- if (component.tabItems) {
334
- component.tabItems.forEach(tab => {
335
- if (tab.content)
336
- collectRules(tab.content);
337
- });
338
- }
339
- });
340
- };
341
- collectRules(processedSchema.components);
342
- return rules;
343
- }, [processedSchema, formState?.values]);
344
- const validationRules = buildValidationRules();
345
- /**
346
- * Build initial form values from component defaultValues
347
- */
348
- const initialValues = React.useMemo(() => {
349
- const values = {};
350
- const allComponents = collectAllComponents(processedSchema.components);
351
- const componentIds = new Set();
352
- allComponents.forEach(comp => {
353
- // Do NOT store dataGrid data inside the form state; grids manage their own internal data.
354
- if (comp.type === 'dataGrid' || comp.type === 'richTextEditor') {
355
- componentIds.add(comp.id);
356
- return;
357
- }
358
- if (comp.defaultValue !== undefined) {
359
- values[comp.id] = comp.defaultValue;
360
- }
361
- else {
362
- // Initialize with type-appropriate defaults
363
- switch (comp.type) {
364
- case 'checkbox':
365
- case 'switch':
366
- values[comp.id] = false;
367
- break;
368
- }
369
- }
370
- componentIds.add(comp.id);
371
- });
372
- if (formState) {
373
- Object.keys(formState.values).filter(key => !componentIds.has(key)).forEach(key => {
374
- delete formState.values[key];
375
- });
376
- }
377
- return values;
378
- }, [processedSchema, collectAllComponents, dataSchema, uiSchema]);
379
- // Update the useEffect that sets default values
380
- useEffect(() => {
381
- if (formState && !isInitializedRef.current) {
382
- isInitializedRef.current = true;
383
- }
384
- if (formState && initialValues) {
385
- Object.keys(formState.values).filter(key => !(key in initialValues));
386
- Object.entries(initialValues).forEach(([id, value]) => {
387
- if (!formState.values[id]) {
388
- formState.values[id] = value;
389
- }
390
- });
391
- }
392
- }, [formState, processedSchema, initialValues]);
393
- /**
394
- * Render a single field with label and error
395
- */
396
- const renderField = (component) => {
397
- const formValues = formState?.values || {};
398
- // Skip layout and button components (handled separately)
399
- if (['panel', 'table', 'tabs', 'card', 'message', 'button', 'splitButton', 'staticHtml'].includes(component.type)) {
400
- return null;
401
- }
402
- const conditionalReadOnly = ConditionalRuleEngine.isFieldReadOnly(component.conditions, formValues);
403
- const conditionalDisabled = ConditionalRuleEngine.isFieldDisabled(component.conditions, formValues);
404
- const conditionalRequired = ConditionalRuleEngine.isFieldRequired(component.conditions, formValues);
405
- const conditionalValue = ConditionalRuleEngine.getConditionalValue(component.conditions, formValues);
406
- const computedReadOnly = component.readOnly || conditionalReadOnly;
407
- const computedDisabled = component.disabled || conditionalDisabled;
408
- const computedRequired = component.required || conditionalRequired;
409
- const labelPos = component.labelPosition || 'top';
410
- const labelWidth = component.labelWidth || 'auto';
411
- const showLabel = !component.hideLabel && component.label && !['checkbox', 'button', 'staticHtml'].includes(component.type);
412
- if ((component.type === 'radio' || component.type === 'checkboxGroup') && Array.isArray(component.options)) {
413
- // For radio, store each option's ref with unique key
414
- setInputRef = (el, idx) => {
415
- if (el && typeof idx === 'number') {
416
- inputRefs.current[`${component.id}_${idx}`] = el;
417
- }
418
- };
419
- }
420
- else {
421
- setInputRef = (el) => {
422
- if (el) {
423
- inputRefs.current[component.id] = el;
424
- }
425
- };
426
- }
427
- const shouldShowError = !!formState?.errors[component.id];
428
- const isRequiredError = shouldShowError && formState?.errors[component.id].includes('required');
429
- const hasError = shouldShowError && !(isRequiredError && !computedRequired);
430
- // Apply conditional value only when condition transitions from false to true (start of new cycle)
431
- const conditionIsActive = conditionalValue !== undefined;
432
- const wasConditionActive = conditionalValueAppliedRef.current[component.id] || false;
433
- if (formState) {
434
- if (conditionIsActive && !wasConditionActive) {
435
- formState.onChange(component.id, { value: conditionalValue });
436
- conditionalValueAppliedRef.current[component.id] = true;
437
- }
438
- else if (!conditionIsActive && wasConditionActive) {
439
- // Condition just became inactive - clear tracking (ready for next cycle)
440
- conditionalValueAppliedRef.current[component.id] = false;
441
- }
442
- // If condition stays active (wasConditionActive && conditionIsActive), do nothing - let user edit
443
- }
444
- 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: {
445
- cursor: 'pointer',
446
- display: 'flex',
447
- alignItems: 'center',
448
- justifyContent: 'center',
449
- flexShrink: 0,
450
- } }) }))] })), _jsxs("div", { className: "field-input", children: [(component.type === 'radio' || component.type === 'checkboxGroup') && Array.isArray(component.options)
451
- ? renderFormField({
452
- component: { ...component, readOnly: computedReadOnly, disabled: computedDisabled, required: computedRequired },
453
- value: formState?.values[component.id],
454
- onChange: (value) => {
455
- formState?.onChange(component.id, { value });
456
- if (onChange) {
457
- onChange({ field: component.label || component.id, value });
458
- }
459
- },
460
- locale: formLocale,
461
- onBlur: () => formState?.onBlur(component.id),
462
- ref: (el, idx) => setInputRef(el, idx),
463
- hasError,
464
- })
465
- : renderFormField({
466
- component: { ...component, readOnly: computedReadOnly, disabled: computedDisabled, required: computedRequired },
467
- value: formState?.values[component.id],
468
- onChange: (value) => {
469
- if (component.type !== 'dataGrid' && component.type !== 'signature' && component.type !== 'richTextEditor') {
470
- value = formatDateValues(component, value);
471
- formState?.onChange(component.id, { value });
472
- if (onChange) {
473
- onChange({ field: component.label || component.id, value });
474
- }
475
- }
476
- },
477
- locale: formLocale,
478
- onBlur: () => { if (component.type !== 'dataGrid' && component.type !== 'richTextEditor')
479
- formState?.onBlur(component.id); },
480
- ref: setInputRef,
481
- hasError,
482
- }), hasError && _jsx("div", { className: "e-error", children: formState.errors[component.id] })] }), component.description && _jsx("div", { className: 'field-description', children: component.description })] }) }) }, component.id));
483
- };
484
- /**
485
- * Render component (including layouts)
486
- */
487
- const renderComponent = (component) => {
488
- const formValues = formState?.values || {};
489
- const isVisible = component.visible === false
490
- ? false
491
- : ConditionalRuleEngine.isFieldVisible(component.conditions, formValues);
492
- const isHidden = ConditionalRuleEngine.isFieldHidden(component.conditions, formValues);
493
- if (!isVisible || isHidden) {
494
- if (formState && formState.values[component.id] !== undefined && formState.values[component.id] !== null) {
495
- formState.onChange(component.id, { value: null });
496
- }
497
- return null;
498
- }
499
- switch (component.type) {
500
- case 'panel':
501
- 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));
502
- case 'table':
503
- const tableRows = component.rows || (component.tableCells ? component.tableCells.length : 2);
504
- const tableCols = component.columns || (component.tableCells && component.tableCells[0] ? component.tableCells[0].length : 2);
505
- const defaultColSpan = Math.floor(12 / tableCols);
506
- const columnWidthsProp = component.columnWidths;
507
- const splittedWidths = columnWidthsProp ? splitWidths(columnWidthsProp.widths) : [];
508
- 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) => {
509
- const cellComponents = component.tableCells?.[rowIndex]?.[colIndex] || [];
510
- const cellSpan = defaultColSpan;
511
- const colWidthValue = splittedWidths[colIndex];
512
- const isSingleColumn = tableCols === 1;
513
- const cellStyle = isSingleColumn ? { width: '100%' } : (colWidthValue ? { width: `${colWidthValue}%` } : {});
514
- const allNullWidths = !columnWidthsProp || (Array.isArray(columnWidthsProp.widths) && columnWidthsProp.widths.every((w) => w === null));
515
- const isFirstRow = rowIndex === 0;
516
- 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}`));
517
- }) }, `row-${rowIndex}`))) }, component.id));
518
- case 'tabs':
519
- return (_jsx("div", { className: `form-tabs-renderer ${component.cssClass || ''}`, children: _jsx(CustomTabComponent, { tabs: component.tabItems?.map(tab => ({
520
- header: tab.header,
521
- content: (_jsx("div", { className: "tab-content-renderer", children: tab.content?.map(comp => renderComponent(comp)) }))
522
- })) || [], className: component.cssClass }) }, component.id));
523
- case 'card':
524
- 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));
525
- case 'staticHtml':
526
- return (_jsx(StaticHtmlRenderer, { id: component.id, content: component.defaultValue || '<p>HTML content</p>', enableHtmlSanitizer: component.enableHtmlSanitizer }, component.id));
527
- case 'message':
528
- return (_jsx("div", { className: "form-group", children: renderFormField({
529
- component,
530
- locale: formLocale,
531
- value: undefined,
532
- onChange: () => { },
533
- }) }, component.id));
534
- case 'button':
535
- const buttonlDisabled = ConditionalRuleEngine.isFieldDisabled(component.conditions, formValues);
536
- const buttonComputed = component.disabled || buttonlDisabled;
537
- const handleButtonClick = (event) => {
538
- if (onButtonClick) {
539
- onButtonClick({
540
- buttonId: component.id,
541
- buttonLabel: component.buttonsGroups ? event.target?.ej2_instances[0].content : component.label || '', event
542
- });
543
- }
544
- };
545
- return (_jsx("div", { className: "form-group", children: renderFormField({
546
- component: { ...component, disabled: buttonComputed },
547
- value: undefined,
548
- onChange: () => { },
549
- locale: formLocale,
550
- onClick: (e) => { handleButtonClick(e); },
551
- ref: (el, idx) => {
552
- if (idx !== undefined) {
553
- inputRefs.current[`${component.id}_${idx}`] = el;
554
- }
555
- else {
556
- inputRefs.current[component.id] = el;
557
- }
558
- }
559
- }) }, component.id));
560
- case 'splitButton':
561
- const splitButtonDisabled = ConditionalRuleEngine.isFieldDisabled(component.conditions, formValues);
562
- const computedDisabled = component.disabled || splitButtonDisabled;
563
- setInputRef = (el) => {
564
- if (el) {
565
- inputRefs.current[component.id] = el;
566
- }
567
- };
568
- const handleSplitButtonEvent = (event) => {
569
- if (onButtonClick) {
570
- onButtonClick({
571
- buttonId: component.id,
572
- buttonLabel: component.label || '',
573
- event
574
- });
575
- }
576
- };
577
- return (_jsx("div", { className: "form-group", children: renderFormField({
578
- component: { ...component, disabled: computedDisabled },
579
- value: undefined,
580
- locale: formLocale,
581
- onChange: () => { },
582
- onClick: (e) => { handleSplitButtonEvent(e); },
583
- ref: setInputRef
584
- }) }, component.id));
585
- default:
586
- return renderField(component);
587
- }
588
- };
589
- // Notify when all form elements and refs are rendered
590
- useEffect(() => {
591
- if (created && formState) {
592
- setTimeout(() => {
593
- const hasInputs = Object.keys(inputRefs.current).length > 0;
594
- if (hasInputs) {
595
- created();
596
- }
597
- }, 0);
598
- }
599
- }, [created, formState]);
600
- // Expose getComponent as a public method
601
- React.useImperativeHandle(ref, () => ({
602
- /**
603
- * Get the instance of the form input by name/id
604
- */
605
- getComponent: (name) => {
606
- return inputRefs.current[name] || null;
607
- },
608
- }), []);
609
- return (_jsx("div", { ...restProps, className: `form-renderer-container ${className}`, children: _jsx(Form, { rules: validationRules, initialValues: initialValues, onFormStateChange: (state) => setFormState(state), onSubmit: (data) => {
610
- const isValid = (formState?.errors ? Object.keys(formState.errors).length === 0 : false);
611
- const transformedData = {};
612
- const formValues = formState?.values || {};
613
- // Exclude non-data components (buttons, messages, and layout-only components)
614
- const excludedTypes = new Set(['button', 'message', 'panel', 'table', 'tabs']);
615
- const allComps = collectAllComponents(processedSchema.components || []);
616
- const excludedIds = new Set(allComps.filter(c => excludedTypes.has(c.type)).map(c => c.id));
617
- const hiddenFieldIds = new Set(allComps.filter(c => {
618
- if (c.visible === false)
619
- return true;
620
- if (c.conditions?.visibleWhen) {
621
- if (!ConditionalRuleEngine.isFieldVisible(c.conditions, formValues))
622
- return true;
623
- }
624
- if (c.conditions?.hideWhen) {
625
- if (ConditionalRuleEngine.isFieldHidden(c.conditions, formValues))
626
- return true;
627
- }
628
- return false;
629
- }).map(c => c.id));
630
- const disabledFieldIds = new Set(allComps.filter(c => {
631
- if (c.disabled)
632
- return true;
633
- if (c.conditions?.disabledWhen) {
634
- return ConditionalRuleEngine.isFieldDisabled(c.conditions, formValues);
635
- }
636
- return false;
637
- }).map(c => c.id));
638
- Object.entries(data).forEach(([id, val]) => {
639
- if (excludedIds.has(id) || hiddenFieldIds.has(id) || disabledFieldIds.has(id))
640
- return;
641
- const label = idToLabel[id] || id;
642
- transformedData[label] = val;
643
- });
644
- if (onSubmit) {
645
- onSubmit({ data: transformedData, isValid });
646
- }
647
- }, validateOnChange: true, className: `form-renderer ${layoutClass}`, children: processedSchema.components.map(component => renderComponent(component)) }) }));
648
- });
649
- export default FormRenderer;