@syncfusion/ej2-react-form-renderer 0.1.0-beta → 0.1.1-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/package.json +2 -2
- package/src/CheckboxGroup.js +1 -1
- package/src/ConditionalRuleEngine.d.ts +253 -0
- package/src/ConditionalRuleEngine.js +489 -0
- package/src/DataGridComponent.d.ts +0 -1
- package/src/DataGridComponent.js +3 -19
- package/src/FormRenderer.d.ts +29 -2
- package/src/FormRenderer.js +122 -40
- package/src/json-converter.d.ts +10 -0
- package/src/json-converter.js +181 -32
- package/src/parser.js +18 -17
- package/src/renderFormField.d.ts +2 -1
- package/src/renderFormField.js +44 -43
- package/src/types/FormSchema.d.ts +29 -1
- package/src/utils.js +86 -0
- package/styles/form-renderer.base.css +39 -23
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConditionalRuleEngine - Core rule evaluation engine for conditional behaviors
|
|
3
|
+
*
|
|
4
|
+
* Evaluates declarative condition rules (from QueryBuilder or manual JSON) against
|
|
5
|
+
* form state to determine field visibility, read-only state, disabled state, and
|
|
6
|
+
* which validators should apply.
|
|
7
|
+
*
|
|
8
|
+
* @spec CONDITIONAL-VALIDATION.md
|
|
9
|
+
* @requirement Tasks 3.1-3.12: ConditionalRuleEngine implementation
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Simple rule evaluation (field operator value)
|
|
13
|
+
* - Composite rules (AND/OR combinations with nesting)
|
|
14
|
+
* - Conditional validators (apply validator only when condition met)
|
|
15
|
+
* - Relationship validators (cross-field validation with safe expression eval)
|
|
16
|
+
* - Behavioral flags (visibility, readOnly, disabled)
|
|
17
|
+
* - Error handling with safe defaults
|
|
18
|
+
*
|
|
19
|
+
* Security:
|
|
20
|
+
* - Uses expr-eval for safe expression evaluation (no eval())
|
|
21
|
+
* - Defaults to safe behavior on errors (visible, not disabled, not read-only)
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* const isVisible = ConditionalRuleEngine.isFieldVisible(
|
|
26
|
+
* component.conditions,
|
|
27
|
+
* { country: 'US', age: 25 }
|
|
28
|
+
* );
|
|
29
|
+
*
|
|
30
|
+
* const behaviors = ConditionalRuleEngine.applyConditionalBehaviors(
|
|
31
|
+
* component.conditions,
|
|
32
|
+
* formValues
|
|
33
|
+
* );
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
import { Internationalization } from '@syncfusion/ej2-base';
|
|
37
|
+
/**
|
|
38
|
+
* ConditionalRuleEngine class
|
|
39
|
+
*
|
|
40
|
+
* @spec CONDITIONAL-VALIDATION.md - Core rule evaluation engine
|
|
41
|
+
* @requirement Phase 1: Conditional visibility, readOnly, disabled behaviors
|
|
42
|
+
*/
|
|
43
|
+
export class ConditionalRuleEngine {
|
|
44
|
+
/**
|
|
45
|
+
* Evaluate a conditional rule against form values
|
|
46
|
+
*
|
|
47
|
+
* Handles both simple rules (field operator value) and composite rules (AND/OR).
|
|
48
|
+
* Returns false for malformed rules.
|
|
49
|
+
*
|
|
50
|
+
* @spec CONDITIONAL-VALIDATION.md - evaluateCondition method
|
|
51
|
+
* @requirement Task 3.2: Implement evaluateCondition with simple and composite handling
|
|
52
|
+
*
|
|
53
|
+
* @param rule - ConditionalRule or CompositeConditionalRule
|
|
54
|
+
* @param formValues - Current form field values (key = field name, value = field value)
|
|
55
|
+
* @returns true if condition met, false otherwise
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* const rule = { field: 'age', operator: 'greaterThan', value: 18 };
|
|
60
|
+
* const isAdult = ConditionalRuleEngine.evaluateCondition(rule, { age: 25 });
|
|
61
|
+
* // isAdult = true
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
static evaluateCondition(rule, formValues) {
|
|
65
|
+
try {
|
|
66
|
+
// Handle simple rule (field operator value)
|
|
67
|
+
if ('field' in rule && 'operator' in rule && 'value' in rule) {
|
|
68
|
+
return this.evaluateSimpleRule(rule, formValues);
|
|
69
|
+
}
|
|
70
|
+
// Handle composite rule (AND/OR)
|
|
71
|
+
if ('condition' in rule && 'rules' in rule) {
|
|
72
|
+
return this.evaluateCompositeRule(rule, formValues);
|
|
73
|
+
}
|
|
74
|
+
// Malformed rule - default to false (safe default)
|
|
75
|
+
console.warn('ConditionalRuleEngine: Malformed rule detected', rule);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error('ConditionalRuleEngine: Error evaluating condition', error);
|
|
80
|
+
return false; // Safe default on error
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Evaluate simple field comparison rule
|
|
85
|
+
*
|
|
86
|
+
* Supports operators: equal, notEqual, greaterThan, lessThan, greaterThanOrEqual,
|
|
87
|
+
* lessThanOrEqual, in, notIn, contains, notContains.
|
|
88
|
+
*
|
|
89
|
+
* Handles both camelCase (greaterThan) and lowercase (greaterthan) operator formats
|
|
90
|
+
* for compatibility with QueryBuilder output.
|
|
91
|
+
*
|
|
92
|
+
* @spec CONDITIONAL-VALIDATION.md - evaluateSimpleRule method
|
|
93
|
+
* @requirement Task 3.3: Implement evaluateSimpleRule supporting all operators
|
|
94
|
+
*
|
|
95
|
+
* @param rule - Simple conditional rule with field, operator, value
|
|
96
|
+
* @param formValues - Current form field values
|
|
97
|
+
* @returns true if comparison is true, false otherwise
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const rule = { field: 'email', operator: 'contains', value: '@' };
|
|
102
|
+
* const hasAt = evaluateSimpleRule(rule, { email: 'test@example.com' });
|
|
103
|
+
* // hasAt = true
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
static evaluateSimpleRule(rule, formValues) {
|
|
107
|
+
try {
|
|
108
|
+
const fieldValue = formValues[rule.field];
|
|
109
|
+
const operator = rule.operator.toLowerCase();
|
|
110
|
+
// Allow evaluation for operators that make sense when value is undefined/empty
|
|
111
|
+
if (fieldValue === undefined) {
|
|
112
|
+
const allowedForUndefined = ['equal', 'equals', 'notequal', 'notequals', 'isempty', 'isnotempty'];
|
|
113
|
+
if (!allowedForUndefined.includes(operator)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Check if field value is an array (for multiselect, checkboxGroup)
|
|
118
|
+
const isFieldArray = Array.isArray(fieldValue);
|
|
119
|
+
const intl = new Internationalization();
|
|
120
|
+
switch (operator) {
|
|
121
|
+
case 'equal':
|
|
122
|
+
case 'equals':
|
|
123
|
+
return fieldValue === rule.value;
|
|
124
|
+
case 'notequal':
|
|
125
|
+
case 'notequals':
|
|
126
|
+
return fieldValue !== rule.value;
|
|
127
|
+
case 'startswith': return fieldValue.startsWith(rule.value);
|
|
128
|
+
case 'doesnotstartswith': return !fieldValue.startsWith(rule.value);
|
|
129
|
+
case 'greaterthan':
|
|
130
|
+
case 'lessthan':
|
|
131
|
+
case 'greaterthanorequal':
|
|
132
|
+
case 'lessthanorequal': {
|
|
133
|
+
const ln = Number(fieldValue);
|
|
134
|
+
const rn = Number(rule.value);
|
|
135
|
+
if (!Number.isNaN(ln) && !Number.isNaN(rn)) {
|
|
136
|
+
if (operator === 'greaterthan')
|
|
137
|
+
return ln > rn;
|
|
138
|
+
if (operator === 'lessthan')
|
|
139
|
+
return ln < rn;
|
|
140
|
+
if (operator === 'greaterthanorequal')
|
|
141
|
+
return ln >= rn;
|
|
142
|
+
return ln <= rn;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
case 'dategreaterthan':
|
|
147
|
+
case 'datelessthan': {
|
|
148
|
+
// try date comparison first
|
|
149
|
+
const leftTs = intl.parseDate(fieldValue, { format: 'MM/dd/yyyy' });
|
|
150
|
+
const rightTs = intl.parseDate(rule.value, { format: 'MM/dd/yyyy' });
|
|
151
|
+
if (leftTs !== null && rightTs !== null) {
|
|
152
|
+
if (operator === 'dategreaterthan')
|
|
153
|
+
return leftTs > rightTs;
|
|
154
|
+
if (operator === 'datelessthan')
|
|
155
|
+
return leftTs < rightTs;
|
|
156
|
+
return leftTs <= rightTs;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
case 'timegreaterthan':
|
|
161
|
+
case 'timelessthan': {
|
|
162
|
+
// Parse time strings (format: hh:mm a)
|
|
163
|
+
const leftTime = intl.parseDate(fieldValue, { format: 'hh:mm a', type: 'time' });
|
|
164
|
+
const rightTime = intl.parseDate(rule.value, { format: 'hh:mm a', type: 'time' });
|
|
165
|
+
if (leftTime !== null && rightTime !== null) {
|
|
166
|
+
if (operator === 'timegreaterthan')
|
|
167
|
+
return leftTime > rightTime;
|
|
168
|
+
if (operator === 'timelessthan')
|
|
169
|
+
return leftTime < rightTime;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
case 'datetimegreaterthan':
|
|
174
|
+
case 'datetimelessthan': {
|
|
175
|
+
// Parse datetime strings (format: MM/dd/yyyy hh:mm a)
|
|
176
|
+
const leftDateTime = intl.parseDate(fieldValue, { format: 'MM/dd/yyyy hh:mm a', type: 'dateTime' });
|
|
177
|
+
const rightDateTime = intl.parseDate(rule.value, { format: 'MM/dd/yyyy hh:mm a', type: 'dateTime' });
|
|
178
|
+
if (leftDateTime !== null && rightDateTime !== null) {
|
|
179
|
+
if (operator === 'datetimegreaterthan')
|
|
180
|
+
return leftDateTime > rightDateTime;
|
|
181
|
+
if (operator === 'datetimelessthan')
|
|
182
|
+
return leftDateTime < rightDateTime;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
case 'contains':
|
|
187
|
+
// If field value is array, check if array contains the specific value(s)
|
|
188
|
+
if (isFieldArray) {
|
|
189
|
+
// Parse comma-separated values in rule.value if it's a string
|
|
190
|
+
if (typeof rule.value === 'string' && rule.value.includes(',')) {
|
|
191
|
+
const searchValues = rule.value.split(',').map(v => v.trim());
|
|
192
|
+
// Check if array contains ANY of the search values
|
|
193
|
+
return searchValues.some(searchVal => fieldValue.includes(searchVal));
|
|
194
|
+
}
|
|
195
|
+
return fieldValue.includes(rule.value);
|
|
196
|
+
}
|
|
197
|
+
// Otherwise, check string contains
|
|
198
|
+
return String(fieldValue).includes(String(rule.value));
|
|
199
|
+
case 'notcontains':
|
|
200
|
+
// If field value is array, check if array does NOT contain the specific value(s)
|
|
201
|
+
if (isFieldArray) {
|
|
202
|
+
// Parse comma-separated values in rule.value if it's a string
|
|
203
|
+
if (typeof rule.value === 'string' && rule.value.includes(',')) {
|
|
204
|
+
const searchValues = rule.value.split(',').map(v => v.trim());
|
|
205
|
+
// Check if array contains NONE of the search values
|
|
206
|
+
return !searchValues.some(searchVal => fieldValue.includes(searchVal));
|
|
207
|
+
}
|
|
208
|
+
return !fieldValue.includes(rule.value);
|
|
209
|
+
}
|
|
210
|
+
// Otherwise, check string does not contain
|
|
211
|
+
return !String(fieldValue).includes(String(rule.value));
|
|
212
|
+
case 'isempty':
|
|
213
|
+
// Check if array is empty
|
|
214
|
+
if (isFieldArray) {
|
|
215
|
+
return fieldValue.length === 0;
|
|
216
|
+
}
|
|
217
|
+
// For non-arrays, check if falsy or empty string
|
|
218
|
+
return !fieldValue || String(fieldValue).length === 0;
|
|
219
|
+
case 'isnotempty':
|
|
220
|
+
// Check if array has items
|
|
221
|
+
if (isFieldArray) {
|
|
222
|
+
return fieldValue.length > 0;
|
|
223
|
+
}
|
|
224
|
+
// For non-arrays, check if truthy and not empty string
|
|
225
|
+
return !!fieldValue && String(fieldValue).length > 0;
|
|
226
|
+
default:
|
|
227
|
+
console.warn(`ConditionalRuleEngine: Unknown operator "${rule.operator}"`);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
console.error('ConditionalRuleEngine: Error in evaluateSimpleRule', error);
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Evaluate composite rule with AND/OR logic
|
|
238
|
+
*
|
|
239
|
+
* Recursively evaluates nested rules with AND (all must be true) or OR
|
|
240
|
+
* (at least one must be true) logic.
|
|
241
|
+
*
|
|
242
|
+
* @spec CONDITIONAL-VALIDATION.md - evaluateCompositeRule method
|
|
243
|
+
* @requirement Task 3.4: Implement evaluateCompositeRule for AND/OR logic
|
|
244
|
+
*
|
|
245
|
+
* @param rule - Composite rule with condition ('and'|'or') and rules array
|
|
246
|
+
* @param formValues - Current form field values
|
|
247
|
+
* @returns true if composite condition met, false otherwise
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```typescript
|
|
251
|
+
* const rule = {
|
|
252
|
+
* condition: 'and',
|
|
253
|
+
* rules: [
|
|
254
|
+
* { field: 'age', operator: 'greaterThan', value: 18 },
|
|
255
|
+
* { field: 'country', operator: 'equal', value: 'US' }
|
|
256
|
+
* ]
|
|
257
|
+
* };
|
|
258
|
+
* const isEligible = evaluateCompositeRule(rule, { age: 25, country: 'US' });
|
|
259
|
+
* // isEligible = true
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
static evaluateCompositeRule(rule, formValues) {
|
|
263
|
+
try {
|
|
264
|
+
if (!Array.isArray(rule.rules) || rule.rules.length === 0) {
|
|
265
|
+
console.warn('ConditionalRuleEngine: Composite rule has no rules');
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
const condition = rule.condition.toLowerCase();
|
|
269
|
+
if (condition === 'and') {
|
|
270
|
+
// All rules must be true
|
|
271
|
+
return rule.rules.every((childRule) => this.evaluateCondition(childRule, formValues));
|
|
272
|
+
}
|
|
273
|
+
else if (condition === 'or') {
|
|
274
|
+
// At least one rule must be true
|
|
275
|
+
return rule.rules.some((childRule) => this.evaluateCondition(childRule, formValues));
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
console.warn(`ConditionalRuleEngine: Unknown composite condition "${rule.condition}"`);
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
console.error('ConditionalRuleEngine: Error in evaluateCompositeRule', error);
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Check if field should be visible
|
|
289
|
+
*
|
|
290
|
+
* Evaluates visibleWhen condition. Defaults to true (visible) if no condition
|
|
291
|
+
* is defined or on error.
|
|
292
|
+
*
|
|
293
|
+
* @spec CONDITIONAL-VALIDATION.md - isFieldVisible method
|
|
294
|
+
* @requirement Task 3.8: Implement isFieldVisible (defaults to true if no condition)
|
|
295
|
+
*
|
|
296
|
+
* @param conditions - FormComponentConditions containing visibleWhen rule
|
|
297
|
+
* @param formValues - Current form field values
|
|
298
|
+
* @returns true if field should be visible, false if hidden
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```typescript
|
|
302
|
+
* const conditions = {
|
|
303
|
+
* visibleWhen: { field: 'showEmail', operator: 'equal', value: true }
|
|
304
|
+
* };
|
|
305
|
+
* const visible = isFieldVisible(conditions, { showEmail: true });
|
|
306
|
+
* // visible = true
|
|
307
|
+
* ```
|
|
308
|
+
*/
|
|
309
|
+
static isFieldVisible(conditions, formValues) {
|
|
310
|
+
try {
|
|
311
|
+
if (!conditions?.visibleWhen) {
|
|
312
|
+
return true; // Default: visible if no condition
|
|
313
|
+
}
|
|
314
|
+
return this.evaluateCondition(conditions.visibleWhen, formValues);
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
console.error('ConditionalRuleEngine: Error in isFieldVisible', error);
|
|
318
|
+
return true; // Safe default: visible on error
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Check if field should be read-only
|
|
323
|
+
*
|
|
324
|
+
* Evaluates readOnlyWhen condition. Defaults to false (editable) if no
|
|
325
|
+
* condition is defined or on error.
|
|
326
|
+
*
|
|
327
|
+
* @spec CONDITIONAL-VALIDATION.md - isFieldReadOnly method
|
|
328
|
+
* @requirement Task 3.9: Implement isFieldReadOnly (defaults to false if no condition)
|
|
329
|
+
*
|
|
330
|
+
* @param conditions - FormComponentConditions containing readOnlyWhen rule
|
|
331
|
+
* @param formValues - Current form field values
|
|
332
|
+
* @returns true if field should be read-only, false if editable
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```typescript
|
|
336
|
+
* const conditions = {
|
|
337
|
+
* readOnlyWhen: { field: 'isVerified', operator: 'equal', value: true }
|
|
338
|
+
* };
|
|
339
|
+
* const readOnly = isFieldReadOnly(conditions, { isVerified: true });
|
|
340
|
+
* // readOnly = true
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
static isFieldReadOnly(conditions, formValues) {
|
|
344
|
+
try {
|
|
345
|
+
if (!conditions?.readOnlyWhen) {
|
|
346
|
+
return false; // Default: not read-only if no condition
|
|
347
|
+
}
|
|
348
|
+
return this.evaluateCondition(conditions.readOnlyWhen, formValues);
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
console.error('ConditionalRuleEngine: Error in isFieldReadOnly', error);
|
|
352
|
+
return false; // Safe default: not read-only on error
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Check if field should be disabled
|
|
357
|
+
*
|
|
358
|
+
* Evaluates disabledWhen condition. Defaults to false (enabled) if no
|
|
359
|
+
* condition is defined or on error.
|
|
360
|
+
*
|
|
361
|
+
* @spec CONDITIONAL-VALIDATION.md - isFieldDisabled method
|
|
362
|
+
* @requirement Task 3.10: Implement isFieldDisabled (defaults to false if no condition)
|
|
363
|
+
*
|
|
364
|
+
* @param conditions - FormComponentConditions containing disabledWhen rule
|
|
365
|
+
* @param formValues - Current form field values
|
|
366
|
+
* @returns true if field should be disabled, false if enabled
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* ```typescript
|
|
370
|
+
* const conditions = {
|
|
371
|
+
* disabledWhen: { field: 'orderStatus', operator: 'equal', value: 'completed' }
|
|
372
|
+
* };
|
|
373
|
+
* const disabled = isFieldDisabled(conditions, { orderStatus: 'completed' });
|
|
374
|
+
* // disabled = true
|
|
375
|
+
* ```
|
|
376
|
+
*/
|
|
377
|
+
static isFieldDisabled(conditions, formValues) {
|
|
378
|
+
try {
|
|
379
|
+
if (!conditions?.disabledWhen) {
|
|
380
|
+
return false; // Default: not disabled if no condition
|
|
381
|
+
}
|
|
382
|
+
return this.evaluateCondition(conditions.disabledWhen, formValues);
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
console.error('ConditionalRuleEngine: Error in isFieldDisabled', error);
|
|
386
|
+
return false; // Safe default: not disabled on error
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Check if field should be hidden
|
|
391
|
+
*
|
|
392
|
+
* Evaluates hideWhen condition. Defaults to false (visible) if no condition
|
|
393
|
+
* is defined or on error.
|
|
394
|
+
*
|
|
395
|
+
* @param conditions - FormComponentConditions containing hideWhen rule
|
|
396
|
+
* @param formValues - Current form field values
|
|
397
|
+
* @returns true if field should be hidden, false if visible
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* ```typescript
|
|
401
|
+
* const conditions = {
|
|
402
|
+
* hideWhen: { field: 'hideEmail', operator: 'equal', value: true }
|
|
403
|
+
* };
|
|
404
|
+
* const hidden = isFieldHidden(conditions, { hideEmail: true });
|
|
405
|
+
* // hidden = true
|
|
406
|
+
* ```
|
|
407
|
+
*/
|
|
408
|
+
static isFieldHidden(conditions, formValues) {
|
|
409
|
+
try {
|
|
410
|
+
if (!conditions?.hideWhen) {
|
|
411
|
+
return false; // Default: not hidden if no condition
|
|
412
|
+
}
|
|
413
|
+
return this.evaluateCondition(conditions.hideWhen, formValues);
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
console.error('ConditionalRuleEngine: Error in isFieldHidden', error);
|
|
417
|
+
return false; // Safe default: not hidden on error
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Check if field should be required
|
|
422
|
+
*
|
|
423
|
+
* Evaluates requiredWhen condition. Defaults to false (not required) if no condition
|
|
424
|
+
* is defined or on error.
|
|
425
|
+
*
|
|
426
|
+
* @param conditions - FormComponentConditions containing requiredWhen rule
|
|
427
|
+
* @param formValues - Current form field values
|
|
428
|
+
* @returns true if field should be required, false if optional
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* ```typescript
|
|
432
|
+
* const conditions = {
|
|
433
|
+
* requiredWhen: { field: 'requireDetails', operator: 'equal', value: true }
|
|
434
|
+
* };
|
|
435
|
+
* const required = isFieldRequired(conditions, { requireDetails: true });
|
|
436
|
+
* // required = true
|
|
437
|
+
* ```
|
|
438
|
+
*/
|
|
439
|
+
static isFieldRequired(conditions, formValues) {
|
|
440
|
+
try {
|
|
441
|
+
if (!conditions?.requiredWhen) {
|
|
442
|
+
return false; // Default: not required if no condition
|
|
443
|
+
}
|
|
444
|
+
return this.evaluateCondition(conditions.requiredWhen, formValues);
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
console.error('ConditionalRuleEngine: Error in isFieldRequired', error);
|
|
448
|
+
return false; // Safe default: not required on error
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Get value to set for field if condition is met
|
|
453
|
+
*
|
|
454
|
+
* Evaluates setValueWhen condition and returns the value to assign if true.
|
|
455
|
+
* Returns undefined if no condition is defined, condition not met, or on error.
|
|
456
|
+
*
|
|
457
|
+
* @param conditions - FormComponentConditions containing setValueWhen rule
|
|
458
|
+
* @param formValues - Current form field values
|
|
459
|
+
* @returns The value to set if condition met, undefined otherwise
|
|
460
|
+
*
|
|
461
|
+
* @example
|
|
462
|
+
* ```typescript
|
|
463
|
+
* const conditions = {
|
|
464
|
+
* setValueWhen: {
|
|
465
|
+
* condition: { field: 'country', operator: 'equal', value: 'US' },
|
|
466
|
+
* value: 'United States'
|
|
467
|
+
* }
|
|
468
|
+
* };
|
|
469
|
+
* const value = getConditionalValue(conditions, { country: 'US' });
|
|
470
|
+
* // value = 'United States'
|
|
471
|
+
* ```
|
|
472
|
+
*/
|
|
473
|
+
static getConditionalValue(conditions, formValues) {
|
|
474
|
+
try {
|
|
475
|
+
if (!conditions?.setValueWhen) {
|
|
476
|
+
return undefined;
|
|
477
|
+
}
|
|
478
|
+
const isMet = this.evaluateCondition(conditions.setValueWhen.condition, formValues);
|
|
479
|
+
if (isMet) {
|
|
480
|
+
return conditions.setValueWhen.value;
|
|
481
|
+
}
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
console.error('ConditionalRuleEngine: Error in getConditionalValue', error);
|
|
486
|
+
return undefined;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
@@ -14,7 +14,6 @@ export interface DataGridRef {
|
|
|
14
14
|
*/
|
|
15
15
|
export interface DataGridComponentProps extends GridComponentSchema {
|
|
16
16
|
onDataChange?: (data: Record<string, any>[]) => void;
|
|
17
|
-
isPreviewMode?: boolean;
|
|
18
17
|
}
|
|
19
18
|
/**
|
|
20
19
|
* DataGridComponent: Wrapper around Syncfusion GridComponent
|
package/src/DataGridComponent.js
CHANGED
|
@@ -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%', pageSize = 6, allowPaging = true, allowAdding = true, allowEditing = true, allowDeleting = true, onDataChange
|
|
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) => {
|
|
40
40
|
// Internal state: maintain current grid data
|
|
41
41
|
const [_currentData, setCurrentData] = useState(Array.isArray(dataSource) ? dataSource : []);
|
|
42
42
|
const gridRef = useRef(null);
|
|
@@ -62,23 +62,7 @@ export const DataGridComponent = React.forwardRef(({ id, columns, dataSource = [
|
|
|
62
62
|
}, [columns, allowAdding, allowDeleting, allowEditing, allowPaging, pageSize]);
|
|
63
63
|
useEffect(() => {
|
|
64
64
|
const normalized = Array.isArray(dataSource) ? dataSource : [];
|
|
65
|
-
|
|
66
|
-
setCurrentData(normalized);
|
|
67
|
-
setTimeout(() => {
|
|
68
|
-
try {
|
|
69
|
-
if (gridRef.current.dataSource !== normalized) {
|
|
70
|
-
gridRef.current.dataSource = normalized;
|
|
71
|
-
}
|
|
72
|
-
setTimeout(() => {
|
|
73
|
-
if (gridRef.current?.refresh) {
|
|
74
|
-
gridRef.current.componentRefresh();
|
|
75
|
-
}
|
|
76
|
-
}, 10);
|
|
77
|
-
}
|
|
78
|
-
catch (e) {
|
|
79
|
-
}
|
|
80
|
-
}, 50);
|
|
81
|
-
}
|
|
65
|
+
setCurrentData(normalized);
|
|
82
66
|
}, [dataSource]);
|
|
83
67
|
/**
|
|
84
68
|
* Expose getGridData() via ref - returns current grid data for form submission
|
|
@@ -155,6 +139,6 @@ export const DataGridComponent = React.forwardRef(({ id, columns, dataSource = [
|
|
|
155
139
|
allowAdding,
|
|
156
140
|
allowDeleting,
|
|
157
141
|
mode: 'Normal',
|
|
158
|
-
}, toolbar: toolbarConfig, actionComplete: handleActionComplete, height: typeof height === 'string' ? height : `${height}px`, width: typeof width === 'string' ? width : `${width}px`, allowSorting: true, allowFiltering: false, 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] })] }) }) }));
|
|
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: false, 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] })] }) }) }));
|
|
159
143
|
});
|
|
160
144
|
DataGridComponent.displayName = 'DataGridComponent';
|
package/src/FormRenderer.d.ts
CHANGED
|
@@ -45,17 +45,44 @@ export interface FormRendererProps extends Omit<React.HTMLAttributes<HTMLDivElem
|
|
|
45
45
|
* - UISchema object (used directly)
|
|
46
46
|
*/
|
|
47
47
|
uiSchema?: UISchema | string;
|
|
48
|
+
/**
|
|
49
|
+
* Specifies the locale/language for the form component.
|
|
50
|
+
* Supports locale-specific formatting and translations.
|
|
51
|
+
* @default 'en-US'
|
|
52
|
+
*/
|
|
53
|
+
locale?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Callback invoked when the form is submitted.
|
|
56
|
+
* Receives form data and validation status.
|
|
57
|
+
* @param args - Object containing form data and isValid flag
|
|
58
|
+
*/
|
|
48
59
|
onSubmit?: (args: SubmitEventArgs) => void;
|
|
60
|
+
/**
|
|
61
|
+
* Callback invoked when any form field value changes.
|
|
62
|
+
* Fired on user input for tracked fields.
|
|
63
|
+
* @param args - Object containing changed field name and new value
|
|
64
|
+
*/
|
|
49
65
|
onChange?: (args: ChangeEventArgs) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Callback invoked when a button in the form is clicked.
|
|
68
|
+
* Applies to all button types (button, submit, splitButton).
|
|
69
|
+
* @param args - Object containing button click event details
|
|
70
|
+
*/
|
|
50
71
|
onButtonClick?: (args: ButtonClickEventArgs) => void;
|
|
72
|
+
/**
|
|
73
|
+
* CSS class names to apply to the root form container element.
|
|
74
|
+
* Supports space-separated multiple classes.
|
|
75
|
+
*/
|
|
51
76
|
className?: string;
|
|
52
77
|
/**
|
|
53
|
-
* Form layout configuration (only applies when UI schema is NOT provided)
|
|
78
|
+
* Form layout configuration (only applies when UI schema is NOT provided).
|
|
79
|
+
* Determines how form fields are arranged in columns.
|
|
54
80
|
* @default FormLayout.SingleColumn
|
|
55
81
|
*/
|
|
56
82
|
layout?: FormLayout;
|
|
57
83
|
/**
|
|
58
|
-
* Called when all form elements are rendered and ready
|
|
84
|
+
* Called when all form elements are rendered and ready for interaction.
|
|
85
|
+
* Useful for performing post-render initialization or setup.
|
|
59
86
|
*/
|
|
60
87
|
created?: () => void;
|
|
61
88
|
}
|