@syncfusion/ej2-react-form-renderer 0.1.1-beta → 0.1.2-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -10
- package/package.json +2 -2
- package/src/CheckboxGroup.js +2 -1
- package/src/ConditionalRuleEngine.d.ts +43 -1
- package/src/ConditionalRuleEngine.js +77 -0
- package/src/DataGridComponent.js +8 -5
- package/src/FormRenderer.d.ts +1 -7
- package/src/FormRenderer.js +236 -178
- package/src/MinMaxRuleEngine.d.ts +95 -0
- package/src/MinMaxRuleEngine.js +361 -0
- package/src/expressions/DependencyTracker.d.ts +30 -0
- package/src/expressions/DependencyTracker.js +128 -0
- package/src/expressions/ExpressionEngine.d.ts +81 -0
- package/src/expressions/ExpressionEngine.js +503 -0
- package/src/expressions/ExpressionParser.d.ts +39 -0
- package/src/expressions/ExpressionParser.js +270 -0
- package/src/json-converter.d.ts +22 -31
- package/src/json-converter.js +141 -261
- package/src/parser.js +4 -1
- package/src/renderFormField.d.ts +2 -1
- package/src/renderFormField.js +37 -14
- package/src/types/FormSchema.d.ts +7 -1
- package/src/utils.js +262 -0
- package/styles/form-renderer.base.css +10 -0
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ function App() {
|
|
|
78
78
|
|
|
79
79
|
return (
|
|
80
80
|
<FormRenderer
|
|
81
|
-
|
|
81
|
+
schema={schema}
|
|
82
82
|
onSubmit={handleSubmit}
|
|
83
83
|
onChange={handleChange}
|
|
84
84
|
/>
|
|
@@ -105,8 +105,7 @@ export default App;
|
|
|
105
105
|
|
|
106
106
|
| Prop | Type | Description |
|
|
107
107
|
|------|------|-------------|
|
|
108
|
-
| `
|
|
109
|
-
| `uiSchema` | `UISchema \| string` | Optional UI layout schema |
|
|
108
|
+
| `schema` | `Schema \| FormSchema \| string` | Form schema (JSON or object) |
|
|
110
109
|
| `layout` | `'single' \| 'twocolumn' \| 'threecolumn' \| 'fourcolumn'` | Form layout (when no UI schema) |
|
|
111
110
|
| `onSubmit` | `(args: SubmitEventArgs) => void` | Submit handler |
|
|
112
111
|
| `onChange` | `(args: ChangeEventArgs) => void` | Field change handler |
|
|
@@ -125,12 +124,6 @@ export const FormLayout = {
|
|
|
125
124
|
} as const;
|
|
126
125
|
```
|
|
127
126
|
|
|
128
|
-
## Schema Formats
|
|
129
|
-
|
|
130
|
-
The FormRenderer supports multiple schema formats:
|
|
131
|
-
|
|
132
|
-
1. **DataSchema**: JSON Schema-compatible format
|
|
133
|
-
2. **UISchema**: Separate UI layout configuration
|
|
134
127
|
|
|
135
128
|
## Support
|
|
136
129
|
|
|
@@ -145,4 +138,4 @@ Product support is available through the following channels:
|
|
|
145
138
|
|
|
146
139
|
> A [free community license](https://www.syncfusion.com/products/communitylicense) is also available for companies and individuals whose organizations have less than $1 million USD in annual gross revenue and five or fewer developers.
|
|
147
140
|
|
|
148
|
-
© Copyright 2026 Syncfusion®, Inc. All Rights Reserved. The Syncfusion® Essential® Studio license and copyright applies to this distribution.
|
|
141
|
+
© Copyright 2026 Syncfusion®, Inc. All Rights Reserved. The Syncfusion® Essential® Studio license and copyright applies to this distribution.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncfusion/ej2-react-form-renderer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2-beta",
|
|
4
4
|
"description": "Form renderer component for rendering dynamic JSON-driven forms with Syncfusion EJ2 React components",
|
|
5
5
|
"author": "Syncfusion Inc.",
|
|
6
6
|
"license": "SEE LICENSE IN license",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@syncfusion/ej2-react-navigations": "*",
|
|
43
43
|
"@syncfusion/ej2-react-notifications": "*",
|
|
44
44
|
"@syncfusion/ej2-react-popups": "*",
|
|
45
|
-
"@syncfusion/ej2-react-richtexteditor": "
|
|
45
|
+
"@syncfusion/ej2-react-richtexteditor": "*",
|
|
46
46
|
"@syncfusion/ej2-react-splitbuttons": "*"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {}
|
package/src/CheckboxGroup.js
CHANGED
|
@@ -39,7 +39,8 @@ const CheckboxGroup = ({ component, formState, value, onChange, optionRef }) =>
|
|
|
39
39
|
width: '100%'
|
|
40
40
|
};
|
|
41
41
|
const htmlAttrs = resolveAttributes(component.htmlAttributes);
|
|
42
|
-
|
|
42
|
+
const options = Array.isArray(component.options) ? component.options : ['Option 1'];
|
|
43
|
+
return (_jsx("div", { className: `checkbox-group ${component.cssClass || ''}`, style: containerStyle, ...htmlAttrs, role: "group", "aria-label": groupLabel, title: component.tooltip, children: _jsx("div", { className: "checkbox-group-items", style: itemsStyle, children: (options).map((option, idx) => {
|
|
43
44
|
const optionValue = typeof option === 'string' ? option : (option.value ?? option.label ?? option.key);
|
|
44
45
|
const optionLabel = typeof option === 'string' ? option : (option.label ?? option.value ?? String(optionValue));
|
|
45
46
|
return (_jsx(CheckBoxComponent, { id: `${component.id}_${idx}`, label: optionLabel, checked: selectedItems.includes(optionValue), disabled: component.disabled, labelPosition: component.checkboxLabelPosition || 'After', change: (e) => handleCheckboxChange(optionValue, e.checked), cssClass: `${component.cssClass}${component.size === 'Small' ? ' e-small' : component.size === 'Bigger' ? ' e-bigger' : ''}`, htmlAttributes: htmlAttrs, ref: optionRef ? (el) => optionRef(el, idx) : undefined }, `${component.id}-${idx}`));
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
36
|
import React from 'react';
|
|
37
|
-
import type { ConditionType, FormComponentConditions } from '../FormBuilder/types/FormSchema';
|
|
37
|
+
import type { ConditionType, FormComponentConditions, ConditionalDataRule, ChoiceBasedFieldRule } from '../FormBuilder/types/FormSchema';
|
|
38
38
|
/**
|
|
39
39
|
* Form field value type (aligned with FormValidator)
|
|
40
40
|
*/
|
|
@@ -250,4 +250,46 @@ export declare class ConditionalRuleEngine {
|
|
|
250
250
|
* ```
|
|
251
251
|
*/
|
|
252
252
|
static getConditionalValue(conditions: FormComponentConditions | undefined, formValues: Record<string, FormValueType>): any;
|
|
253
|
+
/**
|
|
254
|
+
* Evaluate conditional data availability
|
|
255
|
+
* Returns true if any conditional data rule's condition is met
|
|
256
|
+
*
|
|
257
|
+
* @param conditionalDataRules - Array of ConditionalDataRule objects
|
|
258
|
+
* @param formValues - Current form field values
|
|
259
|
+
* @returns Index of first matching rule, or -1 if none match
|
|
260
|
+
*/
|
|
261
|
+
static evaluateConditionalDataRules(conditionalDataRules: ConditionalDataRule[] | undefined, formValues: Record<string, FormValueType>): number;
|
|
262
|
+
/**
|
|
263
|
+
* Get filtered options for a dependent dropdown based on choice-based field rule
|
|
264
|
+
*
|
|
265
|
+
* If choiceBasedField rule exists and primary field has a value,
|
|
266
|
+
* return only the mapped dependent options for that primary value.
|
|
267
|
+
* Otherwise return all options.
|
|
268
|
+
*
|
|
269
|
+
* @param rule - ChoiceBasedFieldRule configuration
|
|
270
|
+
* @param allDependentOptions - Complete list of dependent field options
|
|
271
|
+
* @param formValues - Current form values
|
|
272
|
+
* @param primaryFieldId - ID of primary field
|
|
273
|
+
* @returns Filtered options array
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```typescript
|
|
277
|
+
* const rule = {
|
|
278
|
+
* primaryFieldId: 'dept-id',
|
|
279
|
+
* choiceMapping: {
|
|
280
|
+
* 'Marketing': ['Markers', 'Sticky Notes'],
|
|
281
|
+
* 'Finance': ['Calculator']
|
|
282
|
+
* }
|
|
283
|
+
* };
|
|
284
|
+
*
|
|
285
|
+
* const filtered = ConditionalRuleEngine.getChoiceBasedOptions(
|
|
286
|
+
* rule,
|
|
287
|
+
* ['Markers', 'Sticky Notes', 'Calculator'],
|
|
288
|
+
* { 'dept-id': 'Marketing' },
|
|
289
|
+
* 'dept-id'
|
|
290
|
+
* );
|
|
291
|
+
* // Returns: ['Markers', 'Sticky Notes']
|
|
292
|
+
* ```
|
|
293
|
+
*/
|
|
294
|
+
static getChoiceBasedOptions(rule: ChoiceBasedFieldRule, allDependentOptions: string[], formValues: Record<string, FormValueType>, primaryFieldId: string): string[];
|
|
253
295
|
}
|
|
@@ -486,4 +486,81 @@ export class ConditionalRuleEngine {
|
|
|
486
486
|
return undefined;
|
|
487
487
|
}
|
|
488
488
|
}
|
|
489
|
+
/**
|
|
490
|
+
* Evaluate conditional data availability
|
|
491
|
+
* Returns true if any conditional data rule's condition is met
|
|
492
|
+
*
|
|
493
|
+
* @param conditionalDataRules - Array of ConditionalDataRule objects
|
|
494
|
+
* @param formValues - Current form field values
|
|
495
|
+
* @returns Index of first matching rule, or -1 if none match
|
|
496
|
+
*/
|
|
497
|
+
static evaluateConditionalDataRules(conditionalDataRules, formValues) {
|
|
498
|
+
if (!conditionalDataRules || !Array.isArray(conditionalDataRules)) {
|
|
499
|
+
return -1;
|
|
500
|
+
}
|
|
501
|
+
for (let i = 0; i < conditionalDataRules.length; i++) {
|
|
502
|
+
const rule = conditionalDataRules[i];
|
|
503
|
+
if (this.evaluateCondition(rule.condition, formValues)) {
|
|
504
|
+
return i; // Return index of first matching rule
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return -1;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Get filtered options for a dependent dropdown based on choice-based field rule
|
|
511
|
+
*
|
|
512
|
+
* If choiceBasedField rule exists and primary field has a value,
|
|
513
|
+
* return only the mapped dependent options for that primary value.
|
|
514
|
+
* Otherwise return all options.
|
|
515
|
+
*
|
|
516
|
+
* @param rule - ChoiceBasedFieldRule configuration
|
|
517
|
+
* @param allDependentOptions - Complete list of dependent field options
|
|
518
|
+
* @param formValues - Current form values
|
|
519
|
+
* @param primaryFieldId - ID of primary field
|
|
520
|
+
* @returns Filtered options array
|
|
521
|
+
*
|
|
522
|
+
* @example
|
|
523
|
+
* ```typescript
|
|
524
|
+
* const rule = {
|
|
525
|
+
* primaryFieldId: 'dept-id',
|
|
526
|
+
* choiceMapping: {
|
|
527
|
+
* 'Marketing': ['Markers', 'Sticky Notes'],
|
|
528
|
+
* 'Finance': ['Calculator']
|
|
529
|
+
* }
|
|
530
|
+
* };
|
|
531
|
+
*
|
|
532
|
+
* const filtered = ConditionalRuleEngine.getChoiceBasedOptions(
|
|
533
|
+
* rule,
|
|
534
|
+
* ['Markers', 'Sticky Notes', 'Calculator'],
|
|
535
|
+
* { 'dept-id': 'Marketing' },
|
|
536
|
+
* 'dept-id'
|
|
537
|
+
* );
|
|
538
|
+
* // Returns: ['Markers', 'Sticky Notes']
|
|
539
|
+
* ```
|
|
540
|
+
*/
|
|
541
|
+
static getChoiceBasedOptions(rule, allDependentOptions, formValues, primaryFieldId) {
|
|
542
|
+
try {
|
|
543
|
+
// Get primary field value from form
|
|
544
|
+
const primaryValue = formValues[primaryFieldId];
|
|
545
|
+
if (primaryValue === null || primaryValue === undefined) {
|
|
546
|
+
// No primary value selected - return all or empty based on rule config
|
|
547
|
+
return rule.showAllWhenNotMapped ? allDependentOptions : [];
|
|
548
|
+
}
|
|
549
|
+
// Ensure primary value is treated as string for comparison
|
|
550
|
+
const primaryValueStr = String(primaryValue);
|
|
551
|
+
// Check if this primary value is mapped in the rule
|
|
552
|
+
if (primaryValueStr in rule.choiceMapping) {
|
|
553
|
+
const mappedOptions = rule.choiceMapping[primaryValueStr];
|
|
554
|
+
// Validate mapped options exist in all options (safety check)
|
|
555
|
+
return mappedOptions.filter(opt => allDependentOptions.includes(opt));
|
|
556
|
+
}
|
|
557
|
+
// Primary value not mapped - return all or empty based on rule config
|
|
558
|
+
return rule.showAllWhenNotMapped ? allDependentOptions : [];
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
console.error('Error in getChoiceBasedOptions:', error);
|
|
562
|
+
// Safe default - return all options
|
|
563
|
+
return allDependentOptions;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
489
566
|
}
|
package/src/DataGridComponent.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { useImperativeHandle, useRef, useState, useCallback, useMemo, useEffect } from 'react';
|
|
3
|
-
import { GridComponent, ColumnsDirective, ColumnDirective, Inject, Toolbar, Edit, Page, Resize, Sort, } from '@syncfusion/ej2-react-grids';
|
|
3
|
+
import { GridComponent, ColumnsDirective, ColumnDirective, Inject, Toolbar, Edit, Page, Filter, Resize, Sort, } from '@syncfusion/ej2-react-grids';
|
|
4
4
|
(() => {
|
|
5
5
|
try {
|
|
6
6
|
const filterPrefix = '[EJ2Grid.Warning]';
|
|
@@ -36,7 +36,7 @@ import { GridComponent, ColumnsDirective, ColumnDirective, Inject, Toolbar, Edit
|
|
|
36
36
|
* - Maps GridColumn.validationRules to ColumnDirective.validationRules
|
|
37
37
|
* - Renders Syncfusion toolbar with CRUD actions based on allowAdding/Editing/Deleting
|
|
38
38
|
*/
|
|
39
|
-
export const DataGridComponent = React.forwardRef(({ id, columns, dataSource = [], height = '200px', width = '100%', locale, cssClass, pageSize = 6, allowPaging = true, allowAdding = true, allowEditing = true, allowDeleting = true, onDataChange }, ref) => {
|
|
39
|
+
export const DataGridComponent = React.forwardRef(({ id, columns, dataSource = [], height = '200px', width = '100%', locale, cssClass, pageSize = 6, allowPaging = true, allowAdding = true, allowEditing = true, allowDeleting = true, allowFiltering = false, onDataChange }, ref) => {
|
|
40
40
|
// Internal state: maintain current grid data
|
|
41
41
|
const [_currentData, setCurrentData] = useState(Array.isArray(dataSource) ? dataSource : []);
|
|
42
42
|
const gridRef = useRef(null);
|
|
@@ -55,11 +55,14 @@ export const DataGridComponent = React.forwardRef(({ id, columns, dataSource = [
|
|
|
55
55
|
}
|
|
56
56
|
setTimeout(() => {
|
|
57
57
|
if (gridRef.current?.refresh) {
|
|
58
|
+
if (dataSource && Array.isArray(dataSource) && dataSource.length > 0) {
|
|
59
|
+
gridRef.current.dataSource = dataSource;
|
|
60
|
+
}
|
|
58
61
|
gridRef.current.refresh();
|
|
59
62
|
}
|
|
60
63
|
}, 10);
|
|
61
64
|
}
|
|
62
|
-
}, [columns, allowAdding, allowDeleting, allowEditing, allowPaging, pageSize]);
|
|
65
|
+
}, [columns, allowAdding, allowDeleting, allowEditing, allowPaging, pageSize, allowFiltering]);
|
|
63
66
|
useEffect(() => {
|
|
64
67
|
const normalized = Array.isArray(dataSource) ? dataSource : [];
|
|
65
68
|
setCurrentData(normalized);
|
|
@@ -134,11 +137,11 @@ export const DataGridComponent = React.forwardRef(({ id, columns, dataSource = [
|
|
|
134
137
|
* Memoize toolbar to prevent unnecessary re-renders
|
|
135
138
|
*/
|
|
136
139
|
const toolbarConfig = useMemo(() => buildToolbar(), [buildToolbar]);
|
|
137
|
-
return (_jsx("div", { className: "datagrid-wrapper", style: { width, height: 'auto' }, children: _jsx("div", { className: "datagrid-grid", children: _jsxs(GridComponent, { ref: gridRef, id: id, dataSource: _currentData, allowPaging: allowPaging, pageSettings: { pageSize }, editSettings: {
|
|
140
|
+
return (_jsx("div", { className: "datagrid-wrapper", style: { width, height: 'auto' }, children: _jsx("div", { className: "datagrid-grid", children: _jsxs(GridComponent, { ref: gridRef, id: id, dataSource: _currentData, allowPaging: allowPaging, filterSettings: { mode: "Immediate" }, pageSettings: { pageSize }, editSettings: {
|
|
138
141
|
allowEditing,
|
|
139
142
|
allowAdding,
|
|
140
143
|
allowDeleting,
|
|
141
144
|
mode: 'Normal',
|
|
142
|
-
}, locale: locale || 'en-US', cssClass: cssClass, toolbar: toolbarConfig, actionComplete: handleActionComplete, height: typeof height === 'string' ? height : `${height}px`, width: typeof width === 'string' ? width : `${width}px`, allowSorting: true, allowFiltering:
|
|
145
|
+
}, locale: locale || 'en-US', cssClass: cssClass, toolbar: toolbarConfig, actionComplete: handleActionComplete, height: typeof height === 'string' ? height : `${height}px`, width: typeof width === 'string' ? width : `${width}px`, allowSorting: true, allowFiltering: allowFiltering, allowSelection: true, children: [_jsx(ColumnsDirective, { children: columns && columns.length > 0 && columns.map((col) => (_jsx(ColumnDirective, { field: col.field, headerText: col.headerText, type: col.type || 'string', isPrimaryKey: col.isPrimaryKey || false, width: col.width, format: col.type === 'date' ? (col.format || 'MM/dd/yyyy') : col.format, editType: col.editType, validationRules: mapValidationRules(col) }, col.field))) }), _jsx(Inject, { services: [Edit, Toolbar, Page, Resize, Sort, Filter] })] }) }) }));
|
|
143
146
|
});
|
|
144
147
|
DataGridComponent.displayName = 'DataGridComponent';
|
package/src/FormRenderer.d.ts
CHANGED
|
@@ -38,13 +38,7 @@ export interface FormRendererProps extends Omit<React.HTMLAttributes<HTMLDivElem
|
|
|
38
38
|
* - FormSchema object (components converted to DataSchema)
|
|
39
39
|
* - DataSchema object (used directly)
|
|
40
40
|
*/
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* UI Schema - can be provided as:
|
|
44
|
-
* - JSON string (validated and parsed)
|
|
45
|
-
* - UISchema object (used directly)
|
|
46
|
-
*/
|
|
47
|
-
uiSchema?: UISchema | string;
|
|
41
|
+
schema?: DataSchema | string | FormSchema | any;
|
|
48
42
|
/**
|
|
49
43
|
* Specifies the locale/language for the form component.
|
|
50
44
|
* Supports locale-specific formatting and translations.
|