@syncfusion/ej2-react-form-builder 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.
- package/Common/CheckboxGroup.d.ts +13 -0
- package/Common/CheckboxGroup.js +50 -0
- package/Common/ConditionalRuleEngine.d.ts +296 -0
- package/Common/ConditionalRuleEngine.js +566 -0
- package/Common/CustomTabComponent.d.ts +17 -0
- package/Common/CustomTabComponent.js +30 -0
- package/Common/CustomValidationEngine.d.ts +58 -0
- package/Common/CustomValidationEngine.js +146 -0
- package/Common/DataGridComponent.d.ts +20 -0
- package/Common/DataGridComponent.js +273 -0
- package/Common/FormValidator.d.ts +485 -0
- package/Common/FormValidator.js +575 -0
- package/Common/MinMaxRuleEngine.d.ts +105 -0
- package/Common/MinMaxRuleEngine.js +365 -0
- package/Common/StaticHtmlRenderer.d.ts +14 -0
- package/Common/StaticHtmlRenderer.js +24 -0
- package/Common/expressions/DependencyTracker.d.ts +30 -0
- package/Common/expressions/DependencyTracker.js +128 -0
- package/Common/expressions/ExpressionEngine.d.ts +95 -0
- package/Common/expressions/ExpressionEngine.js +506 -0
- package/Common/expressions/ExpressionParser.d.ts +41 -0
- package/Common/expressions/ExpressionParser.js +269 -0
- package/Common/expressions/useGridExpressions.d.ts +12 -0
- package/Common/expressions/useGridExpressions.js +71 -0
- package/Common/globalFormId.d.ts +26 -0
- package/Common/globalFormId.js +45 -0
- package/Common/json-converter.d.ts +444 -0
- package/Common/json-converter.js +1184 -0
- package/Common/parser.d.ts +98 -0
- package/Common/parser.js +753 -0
- package/Common/utils.d.ts +130 -0
- package/Common/utils.js +574 -0
- package/README.md +53 -0
- package/index.d.ts +6 -0
- package/index.js +6 -0
- package/license +13 -0
- package/package.json +54 -0
- package/src/FormBuilder.d.ts +211 -0
- package/src/FormBuilder.js +1299 -0
- package/src/components/Database/ConnectionForm.d.ts +8 -0
- package/src/components/Database/ConnectionForm.js +28 -0
- package/src/components/TopBar.d.ts +10 -0
- package/src/components/TopBar.js +7 -0
- package/src/components/icons/ComponentIcons.d.ts +44 -0
- package/src/components/icons/ComponentIcons.js +40 -0
- package/src/components/layout/FormFieldWrapper.d.ts +41 -0
- package/src/components/layout/FormFieldWrapper.js +112 -0
- package/src/components/layout/LeftPane.d.ts +23 -0
- package/src/components/layout/LeftPane.js +310 -0
- package/src/components/layout/ListBoxPane.d.ts +24 -0
- package/src/components/layout/ListBoxPane.js +94 -0
- package/src/components/layout/MainSection.d.ts +39 -0
- package/src/components/layout/MainSection.js +1766 -0
- package/src/components/layout/Preview.d.ts +12 -0
- package/src/components/layout/Preview.js +337 -0
- package/src/components/layout/RightPane.d.ts +22 -0
- package/src/components/layout/RightPane.js +370 -0
- package/src/components/theme/ThemeSwitcher.d.ts +14 -0
- package/src/components/theme/ThemeSwitcher.js +24 -0
- package/src/components/theme/theme-constant.d.ts +37 -0
- package/src/components/theme/theme-constant.js +23 -0
- package/src/config/component-properties.json +4929 -0
- package/src/config/form-setting.json +56 -0
- package/src/config/locale.d.ts +449 -0
- package/src/config/locale.js +449 -0
- package/src/context/FormSchemaContext.d.ts +32 -0
- package/src/context/FormSchemaContext.js +155 -0
- package/src/context/LocalizationContext.d.ts +27 -0
- package/src/context/LocalizationContext.js +43 -0
- package/src/context/ThemeContext.d.ts +23 -0
- package/src/context/ThemeContext.js +161 -0
- package/src/context/contexts.d.ts +6 -0
- package/src/context/contexts.js +6 -0
- package/src/context/defaultFormSchema.d.ts +6 -0
- package/src/context/defaultFormSchema.js +20 -0
- package/src/defaultPalette.d.ts +9 -0
- package/src/defaultPalette.js +45 -0
- package/src/defaultTemplate.d.ts +3 -0
- package/src/defaultTemplate.js +185 -0
- package/src/import/schemaImportUtils.d.ts +24 -0
- package/src/import/schemaImportUtils.js +121 -0
- package/src/index.d.ts +16 -0
- package/src/index.js +6 -0
- package/src/types/ActionButtons.d.ts +44 -0
- package/src/types/ActionButtons.js +27 -0
- package/src/types/ComponentProperties.d.ts +33 -0
- package/src/types/ComponentProperties.js +2 -0
- package/src/types/FormSchema.d.ts +585 -0
- package/src/types/FormSchema.js +147 -0
- package/src/types/connection.d.ts +19 -0
- package/src/types/connection.js +9 -0
- package/src/utils/AttributesEditor.d.ts +13 -0
- package/src/utils/AttributesEditor.js +117 -0
- package/src/utils/ButtonGroupEditor.d.ts +15 -0
- package/src/utils/ButtonGroupEditor.js +141 -0
- package/src/utils/ChoiceBasedFieldEditor.d.ts +16 -0
- package/src/utils/ChoiceBasedFieldEditor.js +87 -0
- package/src/utils/ClipboardHelper.d.ts +26 -0
- package/src/utils/ClipboardHelper.js +83 -0
- package/src/utils/ColumnBuilder.d.ts +11 -0
- package/src/utils/ColumnBuilder.js +153 -0
- package/src/utils/ColumnSpanEditor.d.ts +8 -0
- package/src/utils/ColumnSpanEditor.js +98 -0
- package/src/utils/ConditionDataEditor.d.ts +29 -0
- package/src/utils/ConditionDataEditor.js +177 -0
- package/src/utils/ConditionalEditorUtils.d.ts +27 -0
- package/src/utils/ConditionalEditorUtils.js +310 -0
- package/src/utils/ConditionsEditor.d.ts +20 -0
- package/src/utils/ConditionsEditor.js +571 -0
- package/src/utils/CustomAccordion.d.ts +13 -0
- package/src/utils/CustomAccordion.js +38 -0
- package/src/utils/CustomValidationEditor.d.ts +11 -0
- package/src/utils/CustomValidationEditor.js +208 -0
- package/src/utils/DataSourceCollector.d.ts +3 -0
- package/src/utils/DataSourceCollector.js +252 -0
- package/src/utils/ExpressionEditor.d.ts +13 -0
- package/src/utils/ExpressionEditor.js +99 -0
- package/src/utils/FormSettingsPanel.d.ts +9 -0
- package/src/utils/FormSettingsPanel.js +60 -0
- package/src/utils/LookupConfig.d.ts +11 -0
- package/src/utils/LookupConfig.js +159 -0
- package/src/utils/MinMaxValueEditor.d.ts +25 -0
- package/src/utils/MinMaxValueEditor.js +188 -0
- package/src/utils/OptionsEditor.d.ts +8 -0
- package/src/utils/OptionsEditor.js +79 -0
- package/src/utils/PropertyRenderer.d.ts +22 -0
- package/src/utils/PropertyRenderer.js +210 -0
- package/src/utils/ValueRenderer.d.ts +2 -0
- package/src/utils/ValueRenderer.js +91 -0
- package/src/utils/ej2DbAdaptor.d.ts +6 -0
- package/src/utils/ej2DbAdaptor.js +38 -0
- package/src/utils/mappingHeuristics.d.ts +10 -0
- package/src/utils/mappingHeuristics.js +33 -0
- package/src/utils/renderComponent.d.ts +18 -0
- package/src/utils/renderComponent.js +300 -0
- package/styles/tailwind3.css +67 -0
- package/styles/themes/action-buttons.css +110 -0
- package/styles/themes/datagrid.css +380 -0
- package/styles/themes/drag-reorder.css +248 -0
- package/styles/themes/import-dialog.css +292 -0
- package/styles/themes/index.css +658 -0
- package/styles/themes/left-pane.css +253 -0
- package/styles/themes/options-editor.css +63 -0
- package/styles/themes/preview.css +702 -0
- package/styles/themes/property-dialog.css +371 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { forwardRef, useEffect, useCallback, useRef, useImperativeHandle, createContext } from 'react';
|
|
4
|
+
import { L10n } from '@syncfusion/ej2-base';
|
|
5
|
+
import { VALIDATION_REGEX } from './utils';
|
|
6
|
+
const FormContext = createContext(null);
|
|
7
|
+
const FormProvider = FormContext.Provider;
|
|
8
|
+
/**
|
|
9
|
+
* Provides a form component with built-in validation functionality. Manages form state tracking,
|
|
10
|
+
* field validation, and submission handling.
|
|
11
|
+
*
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { Form, FormField, FormState } from '@syncfusion/react-inputs';
|
|
14
|
+
*
|
|
15
|
+
* const [formState, setFormState] = useState<FormState>();
|
|
16
|
+
*
|
|
17
|
+
* <Form
|
|
18
|
+
* rules={{ username: { required: [true, 'Username is required'] } }}
|
|
19
|
+
* onSubmit={data => console.log(data)}
|
|
20
|
+
* onFormStateChange={setFormState} >
|
|
21
|
+
* <FormField name="username">
|
|
22
|
+
* <input
|
|
23
|
+
* name="username"
|
|
24
|
+
* value={(formState?.values.username || '') as string}
|
|
25
|
+
* onChange={(e) => formState?.onChange('username', { value: e.target.value })}
|
|
26
|
+
* onBlur={() => formState?.onBlur('username')}
|
|
27
|
+
* onFocus={() => formState?.onFocus('username')}
|
|
28
|
+
* />
|
|
29
|
+
* {formState?.errors?.username && (<div className="error">{formState.errors.username}</div>)}
|
|
30
|
+
* </FormField>
|
|
31
|
+
* <button type="submit">Submit</button>
|
|
32
|
+
* </Form>
|
|
33
|
+
* ```
|
|
34
|
+
* @private
|
|
35
|
+
*/
|
|
36
|
+
export const Form = forwardRef((props, ref) => {
|
|
37
|
+
const { rules, onSubmit, onReset, children, onFormStateChange, initialValues = {}, validateOnChange = false, className = '', ...otherProps } = props;
|
|
38
|
+
const formRef = useRef(null);
|
|
39
|
+
const stateRef = useRef({
|
|
40
|
+
values: { ...initialValues },
|
|
41
|
+
errors: {},
|
|
42
|
+
touched: {},
|
|
43
|
+
visited: {},
|
|
44
|
+
modified: {},
|
|
45
|
+
submitted: false,
|
|
46
|
+
validated: {}
|
|
47
|
+
});
|
|
48
|
+
const notifyStateChange = useCallback(() => {
|
|
49
|
+
if (onFormStateChange) {
|
|
50
|
+
formStateRef.current = getFormState();
|
|
51
|
+
onFormStateChange(formStateRef.current);
|
|
52
|
+
}
|
|
53
|
+
}, [onFormStateChange]);
|
|
54
|
+
const setFieldValue = useCallback((field, value) => {
|
|
55
|
+
stateRef.current = {
|
|
56
|
+
...stateRef.current,
|
|
57
|
+
values: {
|
|
58
|
+
...stateRef.current.values,
|
|
59
|
+
[field]: value
|
|
60
|
+
},
|
|
61
|
+
modified: {
|
|
62
|
+
...stateRef.current.modified,
|
|
63
|
+
[field]: true
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}, []);
|
|
67
|
+
const setFieldTouched = useCallback((field) => {
|
|
68
|
+
stateRef.current = {
|
|
69
|
+
...stateRef.current,
|
|
70
|
+
touched: {
|
|
71
|
+
...stateRef.current.touched,
|
|
72
|
+
[field]: true
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}, []);
|
|
76
|
+
const setFieldVisited = useCallback((field) => {
|
|
77
|
+
stateRef.current = {
|
|
78
|
+
...stateRef.current,
|
|
79
|
+
visited: {
|
|
80
|
+
...stateRef.current.visited,
|
|
81
|
+
[field]: true
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}, []);
|
|
85
|
+
const setFieldError = useCallback((field, error) => {
|
|
86
|
+
const errors = { ...stateRef.current.errors };
|
|
87
|
+
if (error) {
|
|
88
|
+
errors[field] = error;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
delete errors[field];
|
|
92
|
+
}
|
|
93
|
+
stateRef.current = {
|
|
94
|
+
...stateRef.current,
|
|
95
|
+
errors
|
|
96
|
+
};
|
|
97
|
+
}, []);
|
|
98
|
+
const setSubmitted = useCallback((value) => {
|
|
99
|
+
stateRef.current = {
|
|
100
|
+
...stateRef.current,
|
|
101
|
+
submitted: value
|
|
102
|
+
};
|
|
103
|
+
}, []);
|
|
104
|
+
const resetForm = useCallback((values) => {
|
|
105
|
+
stateRef.current = {
|
|
106
|
+
values,
|
|
107
|
+
errors: {},
|
|
108
|
+
touched: {},
|
|
109
|
+
visited: {},
|
|
110
|
+
modified: {},
|
|
111
|
+
submitted: false,
|
|
112
|
+
validated: {}
|
|
113
|
+
};
|
|
114
|
+
notifyStateChange();
|
|
115
|
+
}, [notifyStateChange]);
|
|
116
|
+
const touchAllFields = useCallback(() => {
|
|
117
|
+
const allTouched = {};
|
|
118
|
+
Object.keys(stateRef.current.values).forEach((field) => {
|
|
119
|
+
allTouched[field] = true;
|
|
120
|
+
});
|
|
121
|
+
stateRef.current = {
|
|
122
|
+
...stateRef.current,
|
|
123
|
+
touched: allTouched
|
|
124
|
+
};
|
|
125
|
+
}, []);
|
|
126
|
+
const visitAllFields = useCallback(() => {
|
|
127
|
+
const allVisited = {};
|
|
128
|
+
Object.keys(stateRef.current.values).forEach((field) => {
|
|
129
|
+
allVisited[field] = true;
|
|
130
|
+
});
|
|
131
|
+
stateRef.current = {
|
|
132
|
+
...stateRef.current,
|
|
133
|
+
visited: allVisited
|
|
134
|
+
};
|
|
135
|
+
}, []);
|
|
136
|
+
const setBulkErrors = useCallback((errors) => {
|
|
137
|
+
const newErrors = { ...stateRef.current.errors };
|
|
138
|
+
Object.entries(errors).forEach(([field, error]) => {
|
|
139
|
+
if (error) {
|
|
140
|
+
newErrors[field] = error;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
delete newErrors[field];
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
stateRef.current = {
|
|
147
|
+
...stateRef.current,
|
|
148
|
+
errors: newErrors
|
|
149
|
+
};
|
|
150
|
+
notifyStateChange();
|
|
151
|
+
}, [notifyStateChange]);
|
|
152
|
+
const rulesRef = useRef(rules);
|
|
153
|
+
const fieldLevelRulesRef = useRef({});
|
|
154
|
+
const formStateRef = useRef(null);
|
|
155
|
+
const l10nRef = useRef(null);
|
|
156
|
+
const defaultErrorMessages = {
|
|
157
|
+
required: 'This field is required.',
|
|
158
|
+
email: 'Please enter a valid email address.',
|
|
159
|
+
url: 'Please enter a valid URL.',
|
|
160
|
+
date: 'Please enter a valid date.',
|
|
161
|
+
dateIso: 'Please enter a valid date (ISO).',
|
|
162
|
+
creditCard: 'Please enter valid card number.',
|
|
163
|
+
number: 'Please enter a valid number.',
|
|
164
|
+
digits: 'Please enter only digits.',
|
|
165
|
+
maxLength: 'Please enter no more than {0} characters.',
|
|
166
|
+
minLength: 'Please enter at least {0} characters.',
|
|
167
|
+
rangeLength: 'Please enter a value between {0} and {1} characters long.',
|
|
168
|
+
range: 'Please enter a value between {0} and {1}.',
|
|
169
|
+
max: 'Please enter a value less than or equal to {0}.',
|
|
170
|
+
min: 'Please enter a value greater than or equal to {0}.',
|
|
171
|
+
regex: 'Please enter a correct value.',
|
|
172
|
+
tel: 'Please enter a valid phone number.',
|
|
173
|
+
equalTo: 'Please enter the same value again.'
|
|
174
|
+
};
|
|
175
|
+
const registeredFields = useRef({});
|
|
176
|
+
const registerField = (fieldName, rules) => {
|
|
177
|
+
registeredFields.current[fieldName] = true;
|
|
178
|
+
if (rules) {
|
|
179
|
+
fieldLevelRulesRef.current[fieldName] = rules;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const getMergedRulesForField = (fieldName) => {
|
|
183
|
+
const formLevelRules = rulesRef.current[fieldName];
|
|
184
|
+
const fieldLevelRules = fieldLevelRulesRef.current[fieldName];
|
|
185
|
+
if (!formLevelRules && !fieldLevelRules) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
...formLevelRules,
|
|
190
|
+
...fieldLevelRules
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
const getAllFieldNames = () => {
|
|
194
|
+
const names = new Set([
|
|
195
|
+
...Object.keys(rulesRef.current || {}),
|
|
196
|
+
...Object.keys(fieldLevelRulesRef.current || {})
|
|
197
|
+
]);
|
|
198
|
+
return Array.from(names);
|
|
199
|
+
};
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
l10nRef.current = new L10n('formValidator', defaultErrorMessages);
|
|
202
|
+
return () => {
|
|
203
|
+
l10nRef.current = null;
|
|
204
|
+
};
|
|
205
|
+
}, []);
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
notifyStateChange();
|
|
208
|
+
validateInitialValues();
|
|
209
|
+
return () => {
|
|
210
|
+
l10nRef.current = null;
|
|
211
|
+
rulesRef.current = {};
|
|
212
|
+
fieldLevelRulesRef.current = {};
|
|
213
|
+
registeredFields.current = {};
|
|
214
|
+
if (formRef.current) {
|
|
215
|
+
formRef.current = null;
|
|
216
|
+
}
|
|
217
|
+
formStateRef.current = null;
|
|
218
|
+
};
|
|
219
|
+
}, []);
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
rulesRef.current = rules;
|
|
222
|
+
}, [rules]);
|
|
223
|
+
const validateInitialValues = () => {
|
|
224
|
+
if (Object.keys(initialValues).length > 0) {
|
|
225
|
+
const errors = {};
|
|
226
|
+
for (const fieldName in initialValues) {
|
|
227
|
+
if (Object.prototype.hasOwnProperty.call(initialValues, fieldName) &&
|
|
228
|
+
Object.prototype.hasOwnProperty.call(rulesRef.current, fieldName)) {
|
|
229
|
+
const error = validateFieldValue(fieldName, initialValues[fieldName]);
|
|
230
|
+
if (error) {
|
|
231
|
+
errors[fieldName] = error;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
errors[fieldName] = null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (Object.keys(errors).length > 0) {
|
|
239
|
+
setBulkErrors(errors);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const formatErrorMessage = (ruleName, params) => {
|
|
244
|
+
let formattedMessage = l10nRef.current?.getConstant(ruleName);
|
|
245
|
+
if (Array.isArray(params)) {
|
|
246
|
+
params.forEach((value, index) => {
|
|
247
|
+
const placeholder = `{${index}}`;
|
|
248
|
+
if (formattedMessage.includes(placeholder)) {
|
|
249
|
+
formattedMessage = formattedMessage.replace(placeholder, String(value));
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
formattedMessage = formattedMessage.replace('{0}', String(params));
|
|
255
|
+
}
|
|
256
|
+
return formattedMessage;
|
|
257
|
+
};
|
|
258
|
+
const validateCreditCard = (value) => {
|
|
259
|
+
if (!VALIDATION_REGEX.CREDIT_CARD.test(value)) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
const cardNumber = value.replace(/[\s-]/g, '');
|
|
263
|
+
let sum = 0;
|
|
264
|
+
let shouldDouble = false;
|
|
265
|
+
for (let i = cardNumber.length - 1; i >= 0; i--) {
|
|
266
|
+
let digit = parseInt(cardNumber.charAt(i), 10);
|
|
267
|
+
if (shouldDouble) {
|
|
268
|
+
digit *= 2;
|
|
269
|
+
if (digit > 9) {
|
|
270
|
+
digit -= 9;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
sum += digit;
|
|
274
|
+
shouldDouble = !shouldDouble;
|
|
275
|
+
}
|
|
276
|
+
return (sum % 10) === 0;
|
|
277
|
+
};
|
|
278
|
+
const validateFieldValue = (fieldName, value) => {
|
|
279
|
+
const fieldRules = getMergedRulesForField(fieldName);
|
|
280
|
+
if (!fieldRules || !registeredFields.current[fieldName]) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
const isValueEmpty = value === undefined || value === null || value.toString().trim() === '';
|
|
284
|
+
const isRequired = fieldRules.required != null && fieldRules.required[0] !== false;
|
|
285
|
+
if (isValueEmpty && !isRequired) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
for (const ruleName in fieldRules) {
|
|
289
|
+
if (Object.prototype.hasOwnProperty.call(fieldRules, ruleName)) {
|
|
290
|
+
const ruleValue = fieldRules[ruleName];
|
|
291
|
+
if (!ruleValue) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
let isValid = true;
|
|
295
|
+
let param = null;
|
|
296
|
+
let errorMessage;
|
|
297
|
+
if (ruleName === 'customValidator' && typeof ruleValue === 'function') {
|
|
298
|
+
const customError = ruleValue(value);
|
|
299
|
+
if (customError) {
|
|
300
|
+
return customError;
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (Array.isArray(ruleValue)) {
|
|
305
|
+
param = ruleValue[0];
|
|
306
|
+
errorMessage = ruleValue[1];
|
|
307
|
+
if (ruleName === 'required' && param === false) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (ruleName !== 'required' && (value === '' || value === null || value === undefined)) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
switch (ruleName) {
|
|
315
|
+
case 'required':
|
|
316
|
+
isValid = !isValueEmpty;
|
|
317
|
+
break;
|
|
318
|
+
case 'email':
|
|
319
|
+
isValid = VALIDATION_REGEX.EMAIL.test(value);
|
|
320
|
+
break;
|
|
321
|
+
case 'url':
|
|
322
|
+
isValid = VALIDATION_REGEX.URL.test(value);
|
|
323
|
+
break;
|
|
324
|
+
case 'date':
|
|
325
|
+
isValid = !isNaN(Date.parse(value));
|
|
326
|
+
break;
|
|
327
|
+
case 'dateIso':
|
|
328
|
+
isValid = VALIDATION_REGEX.DATE_ISO.test(value);
|
|
329
|
+
break;
|
|
330
|
+
case 'number':
|
|
331
|
+
isValid = !isNaN(Number(value)) && String(value).indexOf(' ') === -1;
|
|
332
|
+
break;
|
|
333
|
+
case 'digits':
|
|
334
|
+
isValid = VALIDATION_REGEX.DIGITS.test(value);
|
|
335
|
+
break;
|
|
336
|
+
case 'creditCard':
|
|
337
|
+
isValid = validateCreditCard(value);
|
|
338
|
+
break;
|
|
339
|
+
case 'minLength':
|
|
340
|
+
isValid = String(value).length >= Number(param);
|
|
341
|
+
break;
|
|
342
|
+
case 'maxLength':
|
|
343
|
+
isValid = String(value).length <= Number(param);
|
|
344
|
+
break;
|
|
345
|
+
case 'rangeLength':
|
|
346
|
+
if (Array.isArray(param)) {
|
|
347
|
+
isValid = String(value).length >= param[0] && String(value).length <= param[1];
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
case 'min':
|
|
351
|
+
isValid = Number(value) >= Number(param);
|
|
352
|
+
break;
|
|
353
|
+
case 'max':
|
|
354
|
+
isValid = Number(value) <= Number(param);
|
|
355
|
+
break;
|
|
356
|
+
case 'range':
|
|
357
|
+
if (Array.isArray(param)) {
|
|
358
|
+
isValid = Number(value) >= param[0] && Number(value) <= param[1];
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
361
|
+
case 'regex':
|
|
362
|
+
if (param instanceof RegExp) {
|
|
363
|
+
isValid = param.test(value);
|
|
364
|
+
}
|
|
365
|
+
else if (typeof param === 'string') {
|
|
366
|
+
// eslint-disable-next-line security/detect-non-literal-regexp
|
|
367
|
+
isValid = new RegExp(param).test(value);
|
|
368
|
+
}
|
|
369
|
+
break;
|
|
370
|
+
case 'tel':
|
|
371
|
+
isValid = VALIDATION_REGEX.PHONE.test(value);
|
|
372
|
+
break;
|
|
373
|
+
case 'equalTo':
|
|
374
|
+
if (typeof param === 'string') {
|
|
375
|
+
isValid = value === stateRef.current.values[param];
|
|
376
|
+
}
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
if (!isValid) {
|
|
380
|
+
if (errorMessage) {
|
|
381
|
+
return errorMessage;
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
return formatErrorMessage(ruleName, param);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return null;
|
|
390
|
+
};
|
|
391
|
+
const validateForm = () => {
|
|
392
|
+
const errors = {};
|
|
393
|
+
const fields = getAllFieldNames();
|
|
394
|
+
for (const field of fields) {
|
|
395
|
+
const error = validateFieldValue(field, stateRef.current.values[field]);
|
|
396
|
+
if (!stateRef.current.values[field]) {
|
|
397
|
+
setFieldValue(field, stateRef.current.values[field]);
|
|
398
|
+
}
|
|
399
|
+
if (error) {
|
|
400
|
+
errors[field] = error;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return errors;
|
|
404
|
+
};
|
|
405
|
+
const validate = () => {
|
|
406
|
+
const formErrors = validateForm();
|
|
407
|
+
for (const field in formErrors) {
|
|
408
|
+
if (Object.prototype.hasOwnProperty.call(formErrors, field)) {
|
|
409
|
+
setFieldError(field, formErrors[field]);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const fields = getAllFieldNames();
|
|
413
|
+
for (const field of fields) {
|
|
414
|
+
if (!formErrors[field]) {
|
|
415
|
+
setFieldError(field, null);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (Object.keys(formErrors).length === 0) {
|
|
419
|
+
stateRef.current = {
|
|
420
|
+
...stateRef.current,
|
|
421
|
+
errors: {},
|
|
422
|
+
touched: {},
|
|
423
|
+
visited: {},
|
|
424
|
+
modified: {},
|
|
425
|
+
submitted: false,
|
|
426
|
+
validated: {}
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
notifyStateChange();
|
|
430
|
+
return Object.keys(formErrors).length === 0;
|
|
431
|
+
};
|
|
432
|
+
const handleSubmit = (event) => {
|
|
433
|
+
event?.preventDefault();
|
|
434
|
+
const isValid = validate();
|
|
435
|
+
touchAllFields();
|
|
436
|
+
visitAllFields();
|
|
437
|
+
setSubmitted(true);
|
|
438
|
+
notifyStateChange();
|
|
439
|
+
if (isValid && !event.nativeEvent.submitter.parentNode.classList.contains('e-toolbar-item')) {
|
|
440
|
+
onSubmit?.(stateRef.current.values);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
const handleChange = (name, { value }) => {
|
|
444
|
+
setFieldValue(name, value);
|
|
445
|
+
if (validateOnChange) {
|
|
446
|
+
const error = validateFieldValue(name, value);
|
|
447
|
+
setFieldError(name, error);
|
|
448
|
+
}
|
|
449
|
+
notifyStateChange();
|
|
450
|
+
};
|
|
451
|
+
const handleBlur = (fieldName) => {
|
|
452
|
+
setFieldTouched(fieldName);
|
|
453
|
+
const error = validateFieldValue(fieldName, stateRef.current.values[fieldName]);
|
|
454
|
+
setFieldError(fieldName, error);
|
|
455
|
+
notifyStateChange();
|
|
456
|
+
};
|
|
457
|
+
const handleFormReset = (args) => {
|
|
458
|
+
resetForm({ ...initialValues });
|
|
459
|
+
onReset?.(args);
|
|
460
|
+
};
|
|
461
|
+
const reset = () => {
|
|
462
|
+
handleFormReset();
|
|
463
|
+
};
|
|
464
|
+
const getFormState = () => {
|
|
465
|
+
const state = stateRef.current;
|
|
466
|
+
const fields = getAllFieldNames();
|
|
467
|
+
return {
|
|
468
|
+
values: state.values,
|
|
469
|
+
errors: state.errors,
|
|
470
|
+
submitted: state.submitted,
|
|
471
|
+
touched: state.touched,
|
|
472
|
+
visited: state.visited,
|
|
473
|
+
modified: state.modified,
|
|
474
|
+
valid: fields.reduce((acc, fieldName) => {
|
|
475
|
+
acc[fieldName] = !state.errors[fieldName];
|
|
476
|
+
return acc;
|
|
477
|
+
}, {}),
|
|
478
|
+
allowSubmit: Object.keys(state.errors).length === 0,
|
|
479
|
+
onChange: handleChange,
|
|
480
|
+
onBlur: handleBlur,
|
|
481
|
+
onFocus: handleFocus,
|
|
482
|
+
onFormReset: handleFormReset,
|
|
483
|
+
onSubmit: handleSubmit,
|
|
484
|
+
fieldNames: Object.keys(registeredFields.current).reduce((acc, fieldName) => {
|
|
485
|
+
acc[fieldName] = fieldName;
|
|
486
|
+
return acc;
|
|
487
|
+
}, {})
|
|
488
|
+
};
|
|
489
|
+
};
|
|
490
|
+
const validateField = (fieldName) => {
|
|
491
|
+
const error = validateFieldValue(fieldName, stateRef.current.values[fieldName]);
|
|
492
|
+
setFieldError(fieldName, error);
|
|
493
|
+
notifyStateChange();
|
|
494
|
+
return !error;
|
|
495
|
+
};
|
|
496
|
+
const publicAPI = React.useMemo(() => ({
|
|
497
|
+
rules,
|
|
498
|
+
initialValues,
|
|
499
|
+
validateOnChange
|
|
500
|
+
}), [rules, initialValues, validateOnChange]);
|
|
501
|
+
useImperativeHandle(ref, () => ({
|
|
502
|
+
...publicAPI,
|
|
503
|
+
validate,
|
|
504
|
+
reset,
|
|
505
|
+
validateField,
|
|
506
|
+
element: formRef.current
|
|
507
|
+
}), [publicAPI]);
|
|
508
|
+
const formClassName = React.useMemo(() => {
|
|
509
|
+
return [
|
|
510
|
+
'sf-control sf-form-validator',
|
|
511
|
+
className
|
|
512
|
+
].filter(Boolean).join(' ');
|
|
513
|
+
}, [className]);
|
|
514
|
+
const handleFocus = (fieldName) => {
|
|
515
|
+
setFieldVisited(fieldName);
|
|
516
|
+
notifyStateChange();
|
|
517
|
+
};
|
|
518
|
+
const formContextValue = {
|
|
519
|
+
registerField: registerField
|
|
520
|
+
};
|
|
521
|
+
return (_jsx(FormProvider, { value: formContextValue, children: _jsx("form", { ref: formRef, className: formClassName, onSubmit: handleSubmit, onReset: handleFormReset, noValidate: true, ...otherProps, children: children }) }));
|
|
522
|
+
});
|
|
523
|
+
Form.displayName = 'Form';
|
|
524
|
+
/**
|
|
525
|
+
* Specifies a component that connects form inputs with validation rules. The FormField component provides
|
|
526
|
+
* an easy way to integrate form controls with the Form validation system, handling state management and
|
|
527
|
+
* validation automatically.
|
|
528
|
+
*
|
|
529
|
+
* ```typescript
|
|
530
|
+
* const [formState, setFormState] = useState<FormState>();
|
|
531
|
+
*
|
|
532
|
+
* <Form
|
|
533
|
+
* rules={{
|
|
534
|
+
* username: { required: [true, 'Username is required'] }
|
|
535
|
+
* }}
|
|
536
|
+
* onSubmit={data => console.log(data)}
|
|
537
|
+
* onFormStateChange={setFormState}
|
|
538
|
+
* >
|
|
539
|
+
* <FormField name="username">
|
|
540
|
+
* <input
|
|
541
|
+
* name="username"
|
|
542
|
+
* value={formState?.values.username || ''}
|
|
543
|
+
* onChange={(e) => formState?.onChange('username', { value: e.target.value })}
|
|
544
|
+
* onBlur={() => formState?.onBlur('username')}
|
|
545
|
+
* onFocus={() => formState?.onFocus('username')}
|
|
546
|
+
* />
|
|
547
|
+
* {formState?.touched?.username && formState?.errors?.username && (
|
|
548
|
+
* <div className="error">{formState.errors.username}</div>
|
|
549
|
+
* )}
|
|
550
|
+
* </FormField>
|
|
551
|
+
* <button type="submit">Submit</button>
|
|
552
|
+
* </Form>
|
|
553
|
+
* ```
|
|
554
|
+
*
|
|
555
|
+
* @param {IFormFieldProps} props - Specifies the form field configuration properties
|
|
556
|
+
* @returns {React.ReactNode} - Returns the children with access to form validation context
|
|
557
|
+
* @private
|
|
558
|
+
*/
|
|
559
|
+
export const FormField = (props) => {
|
|
560
|
+
const { name, children, rules } = props;
|
|
561
|
+
if (!name) {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
565
|
+
const formContext = React.useContext(FormContext);
|
|
566
|
+
if (!formContext) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
570
|
+
React.useEffect(() => {
|
|
571
|
+
formContext?.registerField?.(name, rules);
|
|
572
|
+
}, [name, rules, formContext]);
|
|
573
|
+
return _jsx(_Fragment, { children: children });
|
|
574
|
+
};
|
|
575
|
+
FormField.displayName = 'FormField';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @private
|
|
3
|
+
*/
|
|
4
|
+
export interface MinMaxRangeItem {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
minValue?: number | string | {
|
|
7
|
+
field: string;
|
|
8
|
+
};
|
|
9
|
+
maxValue?: number | string | {
|
|
10
|
+
field: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* @private
|
|
15
|
+
*/
|
|
16
|
+
export interface MinMaxConstraints {
|
|
17
|
+
minValue?: number | string;
|
|
18
|
+
maxValue?: number | string;
|
|
19
|
+
minDate?: string;
|
|
20
|
+
maxDate?: string;
|
|
21
|
+
minTime?: string;
|
|
22
|
+
maxTime?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Extract min/max constraints from minMaxRange array
|
|
26
|
+
* @private
|
|
27
|
+
*/
|
|
28
|
+
export declare class MinMaxRuleEngine {
|
|
29
|
+
/**
|
|
30
|
+
* Extract min/max constraints from minMaxRange array
|
|
31
|
+
* Handles three cases:
|
|
32
|
+
* 1. Direct values (enabled: false)
|
|
33
|
+
* 2. Single field reference (enabled: true, 1 range)
|
|
34
|
+
* 3. Multiple ranges (enabled: true, 2+ ranges) - takes UTMOST
|
|
35
|
+
* @private
|
|
36
|
+
* @param minMaxRange - Array of range configurations
|
|
37
|
+
* @param formValues - Current form state values (id → value)
|
|
38
|
+
* @param componentType - 'number' | 'date' | 'dateRange'
|
|
39
|
+
* @returns Constraints object with appropriate keys
|
|
40
|
+
*/
|
|
41
|
+
static getConstraints(minMaxRange: MinMaxRangeItem[] | undefined, formValues: Record<string, any>, componentType?: string): MinMaxConstraints;
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a single value - either direct or field reference
|
|
44
|
+
*
|
|
45
|
+
* @param value - Direct value or field reference object
|
|
46
|
+
* @param formValues - Form state values for field lookup
|
|
47
|
+
* @returns Resolved value or undefined
|
|
48
|
+
*/
|
|
49
|
+
private static resolveValue;
|
|
50
|
+
/**
|
|
51
|
+
* Get the most permissive (widest) minimum value
|
|
52
|
+
* For numbers: MIN of all mins
|
|
53
|
+
* For dates: earliest date (MIN string comparison on ISO format)
|
|
54
|
+
* For times: earliest time (MIN string comparison on 24-hour format)
|
|
55
|
+
*
|
|
56
|
+
* @param ranges - Array of {min, max} values
|
|
57
|
+
* @param componentType - Type of component
|
|
58
|
+
* @returns Most permissive minimum value
|
|
59
|
+
*/
|
|
60
|
+
private static getPermissiveMin;
|
|
61
|
+
/**
|
|
62
|
+
* Get the most permissive (widest) maximum value
|
|
63
|
+
* For numbers: MAX of all maxs
|
|
64
|
+
* For dates: latest date (MAX string comparison on ISO format)
|
|
65
|
+
* For times: latest time (MAX string comparison on 24-hour format)
|
|
66
|
+
*
|
|
67
|
+
* @param ranges - Array of {min, max} values
|
|
68
|
+
* @param componentType - Type of component
|
|
69
|
+
* @returns Most permissive maximum value
|
|
70
|
+
*/
|
|
71
|
+
private static getPermissiveMax;
|
|
72
|
+
/**
|
|
73
|
+
* Convert MM/DD/YYYY date format to ISO (YYYY-MM-DD) for comparison
|
|
74
|
+
* Handles both formats gracefully
|
|
75
|
+
*
|
|
76
|
+
* @param dateString - Date in MM/DD/YYYY or ISO format
|
|
77
|
+
* @returns ISO date string (YYYY-MM-DD)
|
|
78
|
+
*/
|
|
79
|
+
private static convertToISO;
|
|
80
|
+
/**
|
|
81
|
+
* Convert 12-hour time format to 24-hour format for comparison
|
|
82
|
+
* Handles both 12-hour (hh:mm a) and 24-hour (HH:mm) formats
|
|
83
|
+
*
|
|
84
|
+
* @param timeString - Time in 12-hour (hh:mm a/p) or 24-hour (HH:mm) format
|
|
85
|
+
* @returns 24-hour format time string (HH:mm)
|
|
86
|
+
*/
|
|
87
|
+
private static convertTimeTo24Hour;
|
|
88
|
+
/**
|
|
89
|
+
* Get empty constraints object based on component type
|
|
90
|
+
*/
|
|
91
|
+
private static getEmptyConstraints;
|
|
92
|
+
/**
|
|
93
|
+
* Validate if a value passes all min/max constraints (optional)
|
|
94
|
+
* Can be used for client-side validation before submission
|
|
95
|
+
*
|
|
96
|
+
* @param value - Value to validate
|
|
97
|
+
* @param constraints - Constraints to check against
|
|
98
|
+
* @param componentType - Type of component
|
|
99
|
+
* @returns Validation result with error message if invalid
|
|
100
|
+
*/
|
|
101
|
+
static validateValue(value: any, constraints: MinMaxConstraints, componentType: string): {
|
|
102
|
+
isValid: boolean;
|
|
103
|
+
error?: string;
|
|
104
|
+
};
|
|
105
|
+
}
|