@syncfusion/ej2-react-form-renderer 0.1.0-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.
Files changed (48) hide show
  1. package/README.md +148 -0
  2. package/index.d.ts +6 -0
  3. package/index.js +6 -0
  4. package/license +13 -0
  5. package/package.json +49 -0
  6. package/src/CheckboxGroup.d.ts +12 -0
  7. package/src/CheckboxGroup.js +48 -0
  8. package/src/CustomTabComponent.d.ts +13 -0
  9. package/src/CustomTabComponent.js +35 -0
  10. package/src/DataGridComponent.d.ts +29 -0
  11. package/src/DataGridComponent.js +160 -0
  12. package/src/FormRenderer.d.ts +63 -0
  13. package/src/FormRenderer.js +567 -0
  14. package/src/FormValidator.d.ts +479 -0
  15. package/src/FormValidator.js +579 -0
  16. package/src/StaticHtmlRenderer.d.ts +8 -0
  17. package/src/StaticHtmlRenderer.js +21 -0
  18. package/src/index.d.ts +6 -0
  19. package/src/index.js +6 -0
  20. package/src/json-converter.d.ts +170 -0
  21. package/src/json-converter.js +1180 -0
  22. package/src/parser.d.ts +99 -0
  23. package/src/parser.js +749 -0
  24. package/src/renderFormField.d.ts +19 -0
  25. package/src/renderFormField.js +202 -0
  26. package/src/types/FormSchema.d.ts +177 -0
  27. package/src/types/FormSchema.js +1 -0
  28. package/src/utils.js +60 -0
  29. package/styles/bootstrap-dark.css +26 -0
  30. package/styles/bootstrap.css +26 -0
  31. package/styles/bootstrap4.css +26 -0
  32. package/styles/bootstrap5-dark.css +26 -0
  33. package/styles/bootstrap5.3-dark.css +26 -0
  34. package/styles/bootstrap5.3.css +26 -0
  35. package/styles/bootstrap5.css +26 -0
  36. package/styles/fluent-dark.css +26 -0
  37. package/styles/fluent.css +26 -0
  38. package/styles/fluent2-dark.css +26 -0
  39. package/styles/fluent2.css +26 -0
  40. package/styles/form-renderer.base.css +473 -0
  41. package/styles/material-dark.css +26 -0
  42. package/styles/material.css +26 -0
  43. package/styles/material3-dark.css +26 -0
  44. package/styles/material3.css +26 -0
  45. package/styles/tailwind-dark.css +26 -0
  46. package/styles/tailwind.css +26 -0
  47. package/styles/tailwind3-dark.css +10 -0
  48. package/styles/tailwind3.css +26 -0
@@ -0,0 +1,567 @@
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 { splitWidths } from './utils';
17
+ enableRipple(true);
18
+ /**
19
+ * Form layout options (only applies when UI schema is NOT provided)
20
+ */
21
+ export const FormLayout = {
22
+ SingleColumn: 'single',
23
+ TwoColumn: 'twocolumn',
24
+ ThreeColumn: 'threecolumn',
25
+ FourColumn: 'fourcolumn'
26
+ };
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
+ // ForwardRef to expose public methods
112
+ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, onSubmit, onChange, onButtonClick, className = '', layout = FormLayout.SingleColumn, created, ...restProps }, ref) => {
113
+ const processedSchema = React.useMemo(() => {
114
+ 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`);
134
+ }
135
+ const merged = mergeSchema({ dataSchema: parsedDataSchema, uiSchema: parsedUISchema });
136
+ return merged;
137
+ }
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;
146
+ }
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`);
155
+ }
156
+ const merged = mergeSchema({ dataSchema: autoDataSchema, uiSchema: parsedUISchema });
157
+ return merged;
158
+ }
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
+ }
163
+ catch (error) {
164
+ console.error('FormRenderer schema processing error:', error);
165
+ return { version: '1.0.0-beta', theme: 'tailwind3', components: [] };
166
+ }
167
+ }, [dataSchema, uiSchema]);
168
+ const [formState, setFormState] = useState(undefined);
169
+ const isInitializedRef = useRef(false);
170
+ // Attach ref to the input element for getComponent
171
+ let setInputRef;
172
+ // Store input component refs by name/id
173
+ const inputRefs = useRef({});
174
+ // Check if UI schema has layout components (table, tabs, or panel)
175
+ const hasLayoutComponents = React.useMemo(() => {
176
+ if (!processedSchema.components)
177
+ return false;
178
+ const checkForLayoutTypes = (components) => {
179
+ for (const comp of components) {
180
+ if (['table', 'tabs', 'panel', 'card'].includes(comp.type)) {
181
+ return true;
182
+ }
183
+ if (comp.children && checkForLayoutTypes(comp.children)) {
184
+ return true;
185
+ }
186
+ if (comp.tableCells) {
187
+ for (const row of comp.tableCells) {
188
+ for (const cell of row) {
189
+ if (checkForLayoutTypes(cell))
190
+ return true;
191
+ }
192
+ }
193
+ }
194
+ if (comp.tabItems) {
195
+ for (const tab of comp.tabItems) {
196
+ if (tab.content && checkForLayoutTypes(tab.content))
197
+ return true;
198
+ }
199
+ }
200
+ }
201
+ return false;
202
+ };
203
+ return checkForLayoutTypes(processedSchema.components);
204
+ }, [processedSchema]);
205
+ // Determine layout class (only apply if UI schema does NOT have table/tabs/panels)
206
+ const layoutClass = React.useMemo(() => {
207
+ if (hasLayoutComponents)
208
+ return ''; // Ignore layout if UI schema has table/tabs/panels
209
+ // Map enum to CSS class names
210
+ switch (layout) {
211
+ case FormLayout.TwoColumn:
212
+ return 'twolayout';
213
+ case FormLayout.ThreeColumn:
214
+ return 'threelayout';
215
+ case FormLayout.FourColumn:
216
+ return 'fourlayout';
217
+ case FormLayout.SingleColumn:
218
+ default:
219
+ return ''; // Default single column (no class needed)
220
+ }
221
+ }, [layout, hasLayoutComponents]);
222
+ /**
223
+ * Recursively collect all components including nested ones
224
+ */
225
+ const collectAllComponents = useCallback((components) => {
226
+ const allComponents = [];
227
+ const traverse = (comps) => {
228
+ comps.forEach(comp => {
229
+ allComponents.push(comp);
230
+ // Handle nested components in panels
231
+ if (comp.children && comp.children.length > 0) {
232
+ traverse(comp.children);
233
+ }
234
+ // Handle components in table cells
235
+ if (comp.tableCells) {
236
+ comp.tableCells.forEach(row => {
237
+ row.forEach(cell => {
238
+ traverse(cell);
239
+ });
240
+ });
241
+ }
242
+ // Handle components in tabs
243
+ if (comp.tabItems) {
244
+ comp.tabItems.forEach(tab => {
245
+ if (tab.content) {
246
+ traverse(tab.content);
247
+ }
248
+ });
249
+ }
250
+ });
251
+ };
252
+ traverse(components);
253
+ return allComponents;
254
+ }, []);
255
+ const idToLabel = React.useMemo(() => {
256
+ const map = {};
257
+ const traverse = (comps) => {
258
+ comps.forEach(comp => {
259
+ if (comp.label && !['panel', 'table', 'tabs', 'card', 'button', 'message', 'splitbutton'].includes(comp.type)) {
260
+ map[comp.id] = comp.label;
261
+ }
262
+ if (comp.children)
263
+ traverse(comp.children);
264
+ if (comp.tableCells) {
265
+ comp.tableCells.forEach(row => {
266
+ row.forEach(cell => traverse(cell));
267
+ });
268
+ }
269
+ if (comp.tabItems) {
270
+ comp.tabItems.forEach(tab => {
271
+ if (tab.content)
272
+ traverse(tab.content);
273
+ });
274
+ }
275
+ });
276
+ };
277
+ traverse(processedSchema.components);
278
+ return map;
279
+ }, [processedSchema]);
280
+ /**
281
+ * Build EJ2 FormValidator rules from schema
282
+ */
283
+ const buildValidationRules = useCallback(() => {
284
+ const rules = {};
285
+ const collectRules = (components) => {
286
+ components.forEach((component) => {
287
+ if (component) {
288
+ const componentRules = {};
289
+ if (component.required && !component.disabled && !component.readOnly) {
290
+ componentRules.required = [true, component.validationMessage || `${component.label} is required`];
291
+ }
292
+ if (component.minLength && !component.disabled && !component.readOnly) {
293
+ componentRules.minLength = [component.minLength, `Minimum ${component.minLength} characters required`];
294
+ }
295
+ if (component.maxLength && !component.disabled && !component.readOnly) {
296
+ componentRules.maxLength = [component.maxLength, `Maximum ${component.maxLength} characters allowed`];
297
+ }
298
+ if (component?.type === 'textbox' && component?.textboxType && !component?.regex && !componentRules.regex && !component.disabled && !component.readOnly) {
299
+ if (component.textboxType === 'email') {
300
+ componentRules.regex = [VALIDATION_REGEX['EMAIL'], component?.validationMessage || 'Invalid format'];
301
+ }
302
+ else if (component.textboxType === 'url') {
303
+ componentRules.regex = [VALIDATION_REGEX['URL'], component?.validationMessage || 'Invalid format'];
304
+ }
305
+ }
306
+ if (component.regex && !component.disabled && !component.readOnly) {
307
+ componentRules.regex = [component.regex, component.validationMessage || 'Invalid format'];
308
+ }
309
+ if (Object.keys(componentRules).length > 0) {
310
+ rules[component.id] = componentRules;
311
+ }
312
+ }
313
+ // Recursively collect rules from nested components
314
+ if (component.children) {
315
+ collectRules(component.children);
316
+ }
317
+ if (component.tableCells) {
318
+ component.tableCells.forEach(row => {
319
+ row.forEach(cell => collectRules(cell));
320
+ });
321
+ }
322
+ if (component.tabItems) {
323
+ component.tabItems.forEach(tab => {
324
+ if (tab.content)
325
+ collectRules(tab.content);
326
+ });
327
+ }
328
+ });
329
+ };
330
+ collectRules(processedSchema.components);
331
+ return rules;
332
+ }, [processedSchema]);
333
+ const validationRules = buildValidationRules();
334
+ /**
335
+ * Build initial form values from component defaultValues
336
+ */
337
+ const initialValues = React.useMemo(() => {
338
+ const values = {};
339
+ const allComponents = collectAllComponents(processedSchema.components);
340
+ const componentIds = new Set();
341
+ 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') {
344
+ componentIds.add(comp.id);
345
+ return;
346
+ }
347
+ if (comp.defaultValue !== undefined) {
348
+ values[comp.id] = comp.defaultValue;
349
+ }
350
+ componentIds.add(comp.id);
351
+ });
352
+ if (formState) {
353
+ Object.keys(formState.values).filter(key => !componentIds.has(key)).forEach(key => {
354
+ delete formState.values[key];
355
+ });
356
+ }
357
+ return values;
358
+ }, [processedSchema, collectAllComponents, dataSchema, uiSchema]);
359
+ // Update the useEffect that sets default values
360
+ useEffect(() => {
361
+ if (formState && !isInitializedRef.current) {
362
+ isInitializedRef.current = true;
363
+ }
364
+ if (formState && initialValues) {
365
+ Object.keys(formState.values).filter(key => !(key in initialValues));
366
+ Object.entries(initialValues).forEach(([id, value]) => {
367
+ if (!formState.values[id]) {
368
+ formState.values[id] = value;
369
+ }
370
+ });
371
+ }
372
+ }, [formState, processedSchema, initialValues]);
373
+ /**
374
+ * Render a single field with label and error
375
+ */
376
+ const renderField = (component) => {
377
+ // Skip invisible components
378
+ if (component.visible === false)
379
+ return null;
380
+ // Skip layout and button components (handled separately)
381
+ if (['panel', 'table', 'tabs', 'card', 'message', 'button', 'splitbutton', 'statichtml'].includes(component.type)) {
382
+ return null;
383
+ }
384
+ const labelPos = component.labelPosition || 'top';
385
+ 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)) {
388
+ // For radio, store each option's ref with unique key
389
+ setInputRef = (el, idx) => {
390
+ if (el && typeof idx === 'number') {
391
+ inputRefs.current[`${component.id}_${idx}`] = el;
392
+ }
393
+ };
394
+ }
395
+ else {
396
+ setInputRef = (el) => {
397
+ if (el) {
398
+ inputRefs.current[component.id] = el;
399
+ }
400
+ };
401
+ }
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: {
404
+ cursor: 'pointer',
405
+ display: 'flex',
406
+ alignItems: 'center',
407
+ justifyContent: 'center',
408
+ 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) => {
414
+ formState?.onChange(component.id, { value });
415
+ if (onChange) {
416
+ onChange({ field: component.label || component.id, value });
417
+ }
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') {
428
+ formState?.onChange(component.id, { value });
429
+ if (onChange) {
430
+ onChange({ field: component.label || component.id, value });
431
+ }
432
+ }
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));
439
+ };
440
+ /**
441
+ * Render component (including layouts)
442
+ */
443
+ const renderComponent = (component) => {
444
+ // Skip invisible components
445
+ if (component.visible === false)
446
+ return null;
447
+ switch (component.type) {
448
+ case 'panel':
449
+ 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));
450
+ case 'table':
451
+ const tableRows = component.rows || (component.tableCells ? component.tableCells.length : 2);
452
+ const tableCols = component.columns || (component.tableCells && component.tableCells[0] ? component.tableCells[0].length : 2);
453
+ const defaultColSpan = Math.floor(12 / tableCols);
454
+ const columnWidthsProp = component.columnWidths;
455
+ 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) => {
457
+ const cellComponents = component.tableCells?.[rowIndex]?.[colIndex] || [];
458
+ const cellSpan = defaultColSpan;
459
+ const colWidthValue = splittedWidths[colIndex];
460
+ const cellStyle = colWidthValue ? { width: `${colWidthValue}%` } : {};
461
+ 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}`));
463
+ }) }, `row-${rowIndex}`))) }, component.id));
464
+ case 'tabs':
465
+ return (_jsx("div", { className: `form-tabs-renderer ${component.cssClass || ''}`, children: _jsx(CustomTabComponent, { tabs: component.tabItems?.map(tab => ({
466
+ header: tab.header,
467
+ content: (_jsx("div", { className: "tab-content-renderer", children: tab.content?.map(comp => renderComponent(comp)) }))
468
+ })) || [], className: component.cssClass }) }, component.id));
469
+ case 'card':
470
+ 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':
472
+ return (_jsx(StaticHtmlRenderer, { id: component.id, content: component.defaultValue || '<p>HTML content</p>', enableHtmlSanitizer: component.enableHtmlSanitizer }, component.id));
473
+ case 'message':
474
+ return (_jsx("div", { className: "form-group", children: renderFormField({
475
+ component,
476
+ value: undefined,
477
+ onChange: () => { },
478
+ }) }, component.id));
479
+ case 'button':
480
+ const handleButtonClick = (event) => {
481
+ if (onButtonClick) {
482
+ onButtonClick({
483
+ buttonId: component.id,
484
+ buttonLabel: component.buttonsGroups ? event.target?.ej2_instances[0].content : component.label || '', event
485
+ });
486
+ }
487
+ };
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) => {
494
+ if (idx !== undefined) {
495
+ inputRefs.current[`${component.id}_${idx}`] = el;
496
+ }
497
+ else {
498
+ inputRefs.current[component.id] = el;
499
+ }
500
+ }
501
+ }) }, component.id));
502
+ case 'splitbutton':
503
+ setInputRef = (el) => {
504
+ if (el) {
505
+ inputRefs.current[component.id] = el;
506
+ }
507
+ };
508
+ const handleSplitButtonEvent = (event) => {
509
+ if (onButtonClick) {
510
+ onButtonClick({
511
+ buttonId: component.id,
512
+ buttonLabel: component.label || '',
513
+ event
514
+ });
515
+ }
516
+ };
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));
524
+ default:
525
+ return renderField(component);
526
+ }
527
+ };
528
+ // Notify when all form elements and refs are rendered
529
+ useEffect(() => {
530
+ if (created && formState) {
531
+ setTimeout(() => {
532
+ const hasInputs = Object.keys(inputRefs.current).length > 0;
533
+ if (hasInputs) {
534
+ created();
535
+ }
536
+ }, 0);
537
+ }
538
+ }, [created, formState]);
539
+ // Expose getComponent as a public method
540
+ React.useImperativeHandle(ref, () => ({
541
+ /**
542
+ * Get the instance of the form input by name/id
543
+ */
544
+ getComponent: (name) => {
545
+ return inputRefs.current[name] || null;
546
+ },
547
+ }), []);
548
+ 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;
550
+ const transformedData = {};
551
+ // Exclude non-data components (buttons, messages, and layout-only components)
552
+ const excludedTypes = new Set(['button', 'message', 'panel', 'table', 'tabs', 'card', 'splitbutton', 'statichtml']);
553
+ const allComps = collectAllComponents(processedSchema.components || []);
554
+ const excludedIds = new Set(allComps.filter(c => excludedTypes.has(c.type)).map(c => c.id));
555
+ Object.entries(data).forEach(([id, val]) => {
556
+ // Skip excluded components so they don't appear as submitted values
557
+ if (excludedIds.has(id))
558
+ return;
559
+ const label = idToLabel[id] || id;
560
+ transformedData[label] = val;
561
+ });
562
+ if (onSubmit) {
563
+ onSubmit({ data: transformedData, isValid });
564
+ }
565
+ }, validateOnChange: true, className: `form-renderer ${layoutClass}`, children: processedSchema.components.map(component => renderComponent(component)) }) }));
566
+ });
567
+ export default FormRenderer;