gform-react 2.7.2 → 2.8.0
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 +107 -30
- package/dist/cjs/gform-react.development.js +165 -140
- package/dist/cjs/gform-react.development.js.map +1 -1
- package/dist/cjs/gform-react.production.js +1 -1
- package/dist/cjs/gform-react.production.js.map +1 -1
- package/dist/esm/GForm.development.js +12 -9
- package/dist/esm/GForm.development.js.map +1 -1
- package/dist/esm/GForm.production.js +1 -1
- package/dist/esm/GForm.production.js.map +1 -1
- package/dist/esm/GInput.development.js +22 -21
- package/dist/esm/GInput.development.js.map +1 -1
- package/dist/esm/GInput.production.js +1 -1
- package/dist/esm/GInput.production.js.map +1 -1
- package/dist/esm/GValidator.development.js +5 -1
- package/dist/esm/GValidator.development.js.map +1 -1
- package/dist/esm/GValidator.production.js +1 -1
- package/dist/esm/GValidator.production.js.map +1 -1
- package/dist/esm/shared.development.js +133 -112
- package/dist/esm/shared.development.js.map +1 -1
- package/dist/esm/shared.production.js +1 -1
- package/dist/esm/shared.production.js.map +1 -1
- package/dist/esm/useFormSelector.development.js +3 -2
- package/dist/esm/useFormSelector.development.js.map +1 -1
- package/dist/esm/useFormSelector.production.js +1 -1
- package/dist/index.d.ts +1 -0
- package/native/dist/cjs/gform-react.development.js +151 -133
- package/native/dist/cjs/gform-react.development.js.map +1 -1
- package/native/dist/cjs/gform-react.production.js +1 -1
- package/native/dist/cjs/gform-react.production.js.map +1 -1
- package/native/dist/esm/RNGForm.development.js +4 -4
- package/native/dist/esm/RNGForm.development.js.map +1 -1
- package/native/dist/esm/RNGForm.production.js +1 -1
- package/native/dist/esm/RNGForm.production.js.map +1 -1
- package/native/dist/esm/RNGInput.development.js +23 -21
- package/native/dist/esm/RNGInput.development.js.map +1 -1
- package/native/dist/esm/RNGInput.production.js +1 -1
- package/native/dist/esm/RNGInput.production.js.map +1 -1
- package/native/dist/esm/shared.development.js +133 -112
- package/native/dist/esm/shared.development.js.map +1 -1
- package/native/dist/esm/shared.production.js +1 -1
- package/native/dist/esm/shared.production.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,42 +1,119 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://gform-react.onrender.com" title="GForm React – A lightweight React form library built for performance, validation, and clean form logic">
|
|
3
|
+
<img src="https://gform-react.onrender.com/gform-logo.png" alt="gform-react logo" />
|
|
4
|
+
</a>
|
|
5
|
+
<h1>gform-react</h1>
|
|
6
|
+
<p>A lightweight React form library built for <b>performance</b>, <b>validation</b>, and clean <b>form logic</b></p>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Lightweight**
|
|
12
|
+
- **Tree‑shakable** — import only what you use to keep bundles small
|
|
13
|
+
- **Minimal re-renders** — updates only the fields that actually change
|
|
14
|
+
- **Native HTML constraint validation** — full support for `min`, `max`, `pattern`, `minLength`, `maxLength`, `required`, and more
|
|
15
|
+
- **Custom Validations** – add custom validation with any rules
|
|
16
|
+
- **Async Validations** — run asynchronous rules for server-side checks
|
|
17
|
+
- **Supports Yup, Zod, and more** – use any validation library you like
|
|
18
|
+
- **Deeply Nested Forms** — structure forms however you like, across any number of components
|
|
19
|
+
- **Dynamic fields** — add or remove fields at runtime without losing state
|
|
20
|
+
- **Native `<form>` actions** — fully supports browser‑level form submission, including action, method, and HTTP navigation, with no JavaScript required
|
|
21
|
+
- **Next.js Server Actions support** — works seamlessly with Server Actions through standard `<form>` submissions, with no special adapters or client‑side wiring
|
|
22
|
+
- **Accessibility‑friendly** — automatically manages `aria-required` and `aria-invalid`
|
|
23
|
+
- **React Native support** — works seamlessly across web and mobile
|
|
24
|
+
|
|
25
|
+
<br/>
|
|
26
|
+
<div align="center">
|
|
27
|
+
<a href="https://unpkg.com/gform-react@latest/dist/cjs/gform-react.production.js">
|
|
28
|
+
<img src="http://img.badgesize.io/https://unpkg.com/gform-react@latest/dist/cjs/gform-react.production.js?compression=gzip&style=for-the-badge" alt="Minified size">
|
|
29
|
+
</a>
|
|
30
|
+
|
|
31
|
+
<img src="https://img.shields.io/npm/dt/gform-react.svg?style=for-the-badge" alt="React DOM peer dependency">
|
|
32
|
+
<img src="https://img.shields.io/npm/dm/gform-react?style=for-the-badge" alt="npm downloads">
|
|
33
|
+
</div>
|
|
34
|
+
<div align="center">
|
|
35
|
+
<img src="https://img.shields.io/npm/dependency-version/gform-react/peer/react?style=for-the-badge" alt="React peer dependency">
|
|
36
|
+
|
|
37
|
+
<img src="https://img.shields.io/npm/dependency-version/gform-react/peer/react-dom?style=for-the-badge" alt="React DOM peer dependency">
|
|
38
|
+
|
|
39
|
+
<a href="https://unpkg.com/gform-react@latest/LICENSE.md">
|
|
40
|
+
<img src="https://img.shields.io/npm/l/gform-react?style=for-the-badge" alt="MIT License">
|
|
41
|
+
</a>
|
|
42
|
+
</div>
|
|
43
|
+
<br/>
|
|
44
|
+
|
|
45
|
+
## Documentation
|
|
46
|
+
|
|
47
|
+
Full documentation, examples, and API reference:
|
|
48
|
+
|
|
25
49
|
https://gform-react.onrender.com
|
|
26
50
|
|
|
27
|
-
##
|
|
28
|
-
|
|
51
|
+
## QuickStart
|
|
52
|
+
```tsx
|
|
53
|
+
import {GForm, GInput, GValidator, type GValidators} from "gform-react";
|
|
54
|
+
|
|
55
|
+
interface SignInForm {
|
|
56
|
+
username: string;
|
|
57
|
+
password: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const baseValidator = new GValidator().withRequiredMessage('this field is required');
|
|
61
|
+
|
|
62
|
+
const validators: GValidators<SignInForm> = {
|
|
63
|
+
'*': baseValidator, // a default validator for all other fields in the form
|
|
64
|
+
|
|
65
|
+
password: new GValidator(baseValidator)
|
|
66
|
+
.withMinLengthMessage((input) => `${input.formKey} must contain atleast ${input.minLength} chars`),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const App: FC = () => {
|
|
70
|
+
return (
|
|
71
|
+
<GForm<SignInForm> className='some-class'
|
|
72
|
+
validators={validators}
|
|
73
|
+
onSubmit={(formState, e) => { //can be used with native `action` or with Next.js `server actions`
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
console.log(formState);
|
|
76
|
+
}}>
|
|
77
|
+
<GInput formKey='username'
|
|
78
|
+
required
|
|
79
|
+
element={(input, props) => <div>
|
|
80
|
+
<input {...props} placeholder='username'/>
|
|
81
|
+
{input.error && <small className="p-error">{input.errorText}</small>}
|
|
82
|
+
</div>}
|
|
83
|
+
/>
|
|
84
|
+
<GInput formKey='password'
|
|
85
|
+
type='password'
|
|
86
|
+
required
|
|
87
|
+
minLength={5}
|
|
88
|
+
element={(input, props) => <div>
|
|
89
|
+
<input {...props} placeholder='password'/>
|
|
90
|
+
{input.error && <small className="p-error">{input.errorText}</small>}
|
|
91
|
+
</div>}
|
|
92
|
+
/>
|
|
93
|
+
<button>Submit</button>
|
|
94
|
+
</GForm>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
```
|
|
29
98
|
|
|
30
99
|
## Installation
|
|
100
|
+
|
|
31
101
|
npm:
|
|
32
|
-
|
|
102
|
+
|
|
103
|
+
```sh
|
|
33
104
|
npm install gform-react
|
|
34
105
|
```
|
|
35
106
|
|
|
36
107
|
yarn:
|
|
37
|
-
|
|
108
|
+
|
|
109
|
+
```sh
|
|
38
110
|
yarn add gform-react
|
|
39
111
|
```
|
|
40
112
|
|
|
41
|
-
|
|
42
|
-
react >=16.8.0, react-dom >=16.8.0 are peer dependencies
|
|
113
|
+
## Peer dependencies
|
|
114
|
+
react >=16.8.0, react-dom >=16.8.0 are peer dependencies
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT © Tal
|
|
119
|
+
https://www.npmjs.com/package/gform-react
|
|
@@ -20,87 +20,52 @@ const typeValueDict = {
|
|
|
20
20
|
number: 'valueAsNumber'
|
|
21
21
|
};
|
|
22
22
|
const _generateIdUnsafe = () => (+new Date()).toString(36) + (1 - Math.random()).toString(36).substring(2, 16);
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
max,
|
|
44
|
-
maxLength,
|
|
45
|
-
min,
|
|
46
|
-
minLength,
|
|
47
|
-
step,
|
|
48
|
-
pattern,
|
|
49
|
-
type = "text",
|
|
50
|
-
defaultValue,
|
|
51
|
-
value,
|
|
52
|
-
checked,
|
|
53
|
-
defaultChecked,
|
|
54
|
-
formKey,
|
|
55
|
-
debounce,
|
|
56
|
-
validatorKey
|
|
57
|
-
} = config;
|
|
58
|
-
const defaultProps = defaultFieldProps[type] || defaultFieldProps.text;
|
|
59
|
-
const inputValue = value || defaultValue || checked || defaultChecked || defaultProps.value;
|
|
60
|
-
fields[formKey] = {
|
|
61
|
-
formKey,
|
|
62
|
-
type,
|
|
63
|
-
required,
|
|
64
|
-
max,
|
|
65
|
-
maxLength,
|
|
66
|
-
min,
|
|
67
|
-
minLength,
|
|
68
|
-
step,
|
|
69
|
-
pattern,
|
|
70
|
-
value: inputValue,
|
|
71
|
-
validatorKey,
|
|
72
|
-
debounce,
|
|
73
|
-
dirty: false,
|
|
74
|
-
touched: false,
|
|
75
|
-
gid: _generateIdUnsafe()
|
|
76
|
-
};
|
|
77
|
-
});
|
|
23
|
+
const _buildInputInitialValues = input => {
|
|
24
|
+
const {
|
|
25
|
+
required = false,
|
|
26
|
+
max,
|
|
27
|
+
maxLength,
|
|
28
|
+
min,
|
|
29
|
+
minLength,
|
|
30
|
+
step,
|
|
31
|
+
pattern,
|
|
32
|
+
type = "text",
|
|
33
|
+
defaultValue,
|
|
34
|
+
value,
|
|
35
|
+
checked,
|
|
36
|
+
defaultChecked,
|
|
37
|
+
formKey,
|
|
38
|
+
debounce,
|
|
39
|
+
validatorKey
|
|
40
|
+
} = input;
|
|
41
|
+
const defaultProps = defaultFieldProps[type] || defaultFieldProps.text;
|
|
42
|
+
const inputValue = value || defaultValue || checked || defaultChecked || defaultProps.value;
|
|
78
43
|
return {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
44
|
+
formKey,
|
|
45
|
+
type,
|
|
46
|
+
required,
|
|
47
|
+
max,
|
|
48
|
+
maxLength,
|
|
49
|
+
min,
|
|
50
|
+
minLength,
|
|
51
|
+
step,
|
|
52
|
+
pattern,
|
|
53
|
+
value: inputValue,
|
|
54
|
+
validatorKey,
|
|
55
|
+
debounce,
|
|
56
|
+
dirty: false,
|
|
57
|
+
touched: false,
|
|
58
|
+
gid: _generateIdUnsafe(),
|
|
59
|
+
error: false,
|
|
60
|
+
errorText: '',
|
|
61
|
+
dispatchChanges(changes) {},
|
|
62
|
+
checkValidity() {
|
|
63
|
+
return false;
|
|
97
64
|
}
|
|
98
|
-
}
|
|
99
|
-
return total;
|
|
65
|
+
};
|
|
100
66
|
};
|
|
101
|
-
const _findValidityKey =
|
|
67
|
+
const _findValidityKey = validity => {
|
|
102
68
|
for (const key in validity) {
|
|
103
|
-
if (exclude.includes(key)) continue;
|
|
104
69
|
if (key !== 'valid' && validity[key]) {
|
|
105
70
|
return key;
|
|
106
71
|
}
|
|
@@ -350,7 +315,7 @@ class GValidator {
|
|
|
350
315
|
}
|
|
351
316
|
this.track.push(validityKey);
|
|
352
317
|
}
|
|
353
|
-
|
|
318
|
+
const constraintHandler = (input, key) => {
|
|
354
319
|
{
|
|
355
320
|
if (validityKey && validityMap[validityKey] && typeof input[validityMap[validityKey]] === 'undefined') {
|
|
356
321
|
console.warn(`DEV ONLY - [Missing Prop] - the input '${input.formKey}' has registered validator for the violation '${validityKey}' but the input hasn't described the constraint '${validityMap[validityKey]}'.\nadd '${validityMap[validityKey]}' to the input props.\nexample:\n<GInput formKey='${input.formKey}' ${validityMap[validityKey]}={...} />\n\nor either remove '.${handlersMap[validityMap[validityKey]]}(...)' validation`);
|
|
@@ -361,7 +326,11 @@ class GValidator {
|
|
|
361
326
|
return true;
|
|
362
327
|
}
|
|
363
328
|
return false;
|
|
329
|
+
};
|
|
330
|
+
Object.defineProperty(constraintHandler, 'name', {
|
|
331
|
+
value: `constraintHandler_${validityKey}`
|
|
364
332
|
});
|
|
333
|
+
this._constraintHandlers.push(constraintHandler);
|
|
365
334
|
return this;
|
|
366
335
|
}
|
|
367
336
|
}
|
|
@@ -370,30 +339,32 @@ const useFormHandlers = (getState, setState, validators = {}, optimized = false)
|
|
|
370
339
|
const _viHandler = (input, e) => {
|
|
371
340
|
if (!input) return;
|
|
372
341
|
const element = e && e.target;
|
|
342
|
+
input.touched = true;
|
|
373
343
|
if (typeof document !== 'undefined' && (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement)) {
|
|
374
344
|
if (!input.checkValidity) input.checkValidity = () => element.checkValidity();
|
|
375
|
-
const hasInitialValue = !input.dirty && input.value
|
|
345
|
+
const hasInitialValue = !input.dirty && input.value;
|
|
376
346
|
if (hasInitialValue) {
|
|
377
|
-
|
|
347
|
+
const {
|
|
348
|
+
validityKey = 'custom'
|
|
349
|
+
} = _checkInputManually(input);
|
|
350
|
+
element.setCustomValidity(validityKey);
|
|
378
351
|
_dispatchChanges(input, input.formKey);
|
|
379
352
|
return;
|
|
380
353
|
}
|
|
381
354
|
element.setCustomValidity('');
|
|
382
|
-
const
|
|
383
|
-
const validityKey = _findValidityKey(element.validity, exclude);
|
|
355
|
+
const validityKey = _findValidityKey(element.validity);
|
|
384
356
|
_validateInput(input, validityKey, v => element.setCustomValidity(v));
|
|
385
357
|
if (!validityKey && input.error) {
|
|
386
358
|
element.setCustomValidity(input.errorText || 'error');
|
|
387
359
|
}
|
|
388
360
|
_dispatchChanges(input, input.formKey);
|
|
389
361
|
} else {
|
|
390
|
-
input.checkValidity = () => _checkInputManually(input);
|
|
362
|
+
input.checkValidity = () => _checkInputManually(input).isValid;
|
|
391
363
|
input.checkValidity();
|
|
392
364
|
_dispatchChanges(input, input.formKey);
|
|
393
365
|
}
|
|
394
366
|
};
|
|
395
367
|
const _checkInputManually = input => {
|
|
396
|
-
const exclude = input.type && (input.pattern || hasCustomValidation(input)) ? ['typeMismatch'] : [];
|
|
397
368
|
let validityKey = _findValidityKey({
|
|
398
369
|
valueMissing: input.required && !input.value || false,
|
|
399
370
|
typeMismatch: _checkTypeMismatch(input),
|
|
@@ -402,28 +373,34 @@ const useFormHandlers = (getState, setState, validators = {}, optimized = false)
|
|
|
402
373
|
patternMismatch: input.pattern && _checkResult(input.pattern, input.value) || false,
|
|
403
374
|
rangeUnderflow: input.min && Number(input.value) < Number(input.min) || false,
|
|
404
375
|
rangeOverflow: input.max && Number(input.value) > Number(input.max) || false
|
|
405
|
-
}
|
|
376
|
+
});
|
|
406
377
|
if (!validityKey && input.error) {
|
|
407
378
|
validityKey = 'customError';
|
|
408
379
|
}
|
|
409
380
|
_validateInput(input, validityKey);
|
|
410
|
-
return
|
|
381
|
+
return {
|
|
382
|
+
isValid: !input.error,
|
|
383
|
+
validityKey
|
|
384
|
+
};
|
|
411
385
|
};
|
|
412
386
|
const _updateInputHandler = (input, e, unknown) => {
|
|
413
387
|
input.value = _extractValue(e, unknown);
|
|
388
|
+
input.dirty = true;
|
|
414
389
|
_viHandler(input, e);
|
|
415
390
|
};
|
|
416
391
|
const _validateInput = (input, validityKey, setValidity) => {
|
|
417
392
|
const inputValidator = validators[input.validatorKey || input.formKey] || validators['*'];
|
|
418
393
|
{
|
|
419
394
|
if (validityKey && !inputValidator?.hasConstraint(validityKey)) {
|
|
420
|
-
if (validityKey === 'typeMismatch')
|
|
395
|
+
if (validityKey === 'typeMismatch') {
|
|
396
|
+
if (!inputValidator?.handlers.length) console.warn(`DEV ONLY - [Missing Validator] - the input '${input.formKey}' has described the constraint '${validityMap[validityKey]}' however, a correspond validator is missing.\nadd '${handlersMap[validityMap[validityKey]]}' or 'withCustomValidation' or '${handlersMap[validityMap.patternMismatch]}' to the input validator.\nexample:\nconst validators: GValidators = {\n\temail: new GValidator().withTypeMismatchMessage('pattern mismatch'),\n\t...\n}\nif you added on of these validators then the input is still suffering from '${validityKey}' violation.\n\nor either remove the constraint '${validityMap[validityKey]}' from the input props.\n`);else console.warn(`DEV ONLY - [Missing Validator] - the input '${input.formKey}' has described the constraint '${validityMap[validityKey]}' however, a correspond validator is missing or not satisfies the native constraint.\nadd '${handlersMap[validityMap[validityKey]]}' or 'withCustomValidation' to the input validator.\nexample:\nconst validators: GValidators = {\n\temail: new GValidator().withTypeMismatchMessage('pattern mismatch'),\n\t...\n}\n\nif you already have a Custom Validation then the input is still not satisfies the native type pattern.\neither enforce it or remove the constraint '${validityMap[validityKey]}' from the input props`);
|
|
397
|
+
} else console.warn(`DEV ONLY - [Missing Validator] - the input '${input.formKey}' has described the constraint '${validityMap[validityKey]}' however, a correspond validator is missing.\nadd '${handlersMap[validityMap[validityKey]]}' to the input validator.\nexample:\nconst validators: GValidators = {\n\temail: new GValidator().withPatternMismatchMessage('pattern mismatch'),\n\t...\n}\n\nor either remove the constraint '${validityMap[validityKey]}' from the input props`);
|
|
398
|
+
console.warn(`form submition is prevented due to violation(s) of input '${input.formKey}': violation '${validityKey}' caused by '${validityMap[validityKey]}' property (<Ginput ${validityMap[validityKey]}={...} />)`);
|
|
421
399
|
}
|
|
422
400
|
}
|
|
423
401
|
if (inputValidator) {
|
|
424
402
|
__validateInput(input, inputValidator, validityKey, setValidity);
|
|
425
403
|
}
|
|
426
|
-
input.touched = true;
|
|
427
404
|
};
|
|
428
405
|
const _dispatchChanges = (changes, key) => setState(prev => {
|
|
429
406
|
if (key) {
|
|
@@ -446,12 +423,14 @@ const useFormHandlers = (getState, setState, validators = {}, optimized = false)
|
|
|
446
423
|
const __validateInput = (input, inputValidator, validityKey, setValidity) => {
|
|
447
424
|
const fields = getState().fields;
|
|
448
425
|
for (const index in inputValidator.constraintHandlers) {
|
|
449
|
-
const
|
|
426
|
+
const handler = inputValidator.constraintHandlers[index];
|
|
427
|
+
const result = handler(input, validityKey);
|
|
450
428
|
input.error = _checkResult(result, input.value);
|
|
451
429
|
if (input.error) return;
|
|
452
430
|
}
|
|
453
431
|
for (const index in inputValidator.handlers) {
|
|
454
|
-
const
|
|
432
|
+
const handler = inputValidator.handlers[index];
|
|
433
|
+
const result = handler(input, fields);
|
|
455
434
|
input.error = _checkResult(result, input.value);
|
|
456
435
|
if (input.error) return;
|
|
457
436
|
}
|
|
@@ -461,7 +440,8 @@ const useFormHandlers = (getState, setState, validators = {}, optimized = false)
|
|
|
461
440
|
_debounce(input.debounce || 300, `${input.gid}-async`).then(() => {
|
|
462
441
|
const validateAsync = async () => {
|
|
463
442
|
for (const index in inputValidator.asyncHandlers) {
|
|
464
|
-
const
|
|
443
|
+
const handler = inputValidator.asyncHandlers[index];
|
|
444
|
+
const result = await handler(input, fields);
|
|
465
445
|
input.error = _checkResult(result, input.value);
|
|
466
446
|
if (input.error) break;
|
|
467
447
|
}
|
|
@@ -478,10 +458,6 @@ const useFormHandlers = (getState, setState, validators = {}, optimized = false)
|
|
|
478
458
|
});
|
|
479
459
|
}
|
|
480
460
|
};
|
|
481
|
-
const hasCustomValidation = input => {
|
|
482
|
-
const validator = validators[input.validatorKey || input.formKey] || validators['*'];
|
|
483
|
-
return validator && (validator.asyncHandlers.length > 0 || validator.handlers.length > 0);
|
|
484
|
-
};
|
|
485
461
|
return {
|
|
486
462
|
_updateInputHandler,
|
|
487
463
|
_viHandler,
|
|
@@ -496,7 +472,8 @@ const GFormContextProvider = ({
|
|
|
496
472
|
children,
|
|
497
473
|
initialState,
|
|
498
474
|
validators,
|
|
499
|
-
optimized
|
|
475
|
+
optimized,
|
|
476
|
+
formRef
|
|
500
477
|
}) => {
|
|
501
478
|
const stateRef = React.useRef(initialState);
|
|
502
479
|
const listeners = React.useRef(null);
|
|
@@ -507,19 +484,59 @@ const GFormContextProvider = ({
|
|
|
507
484
|
return;
|
|
508
485
|
}
|
|
509
486
|
stateRef.current = nextState;
|
|
510
|
-
listeners.current.forEach(
|
|
487
|
+
listeners.current.forEach(listener => listener());
|
|
511
488
|
}, []);
|
|
512
489
|
const handlers = useFormHandlers(() => stateRef.current, setState, validators, optimized);
|
|
490
|
+
const getInputElement = React.useCallback(formKey => {
|
|
491
|
+
if (formRef && formRef.current) {
|
|
492
|
+
return formRef.current[formKey];
|
|
493
|
+
}
|
|
494
|
+
}, []);
|
|
495
|
+
const registerField = React.useCallback(config => {
|
|
496
|
+
const prev = stateRef.current;
|
|
497
|
+
if (prev.fields[config.formKey]) {
|
|
498
|
+
{
|
|
499
|
+
console.warn(`DEV ONLY - [Duplicate Keys] - field with key '${config.formKey}' already defined.`);
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const inputState = _buildInputInitialValues(config);
|
|
504
|
+
stateRef.current = {
|
|
505
|
+
...prev,
|
|
506
|
+
fields: {
|
|
507
|
+
...prev.fields,
|
|
508
|
+
[config.formKey]: {
|
|
509
|
+
...inputState,
|
|
510
|
+
dispatchChanges: changes => handlers._dispatchChanges(changes, config.formKey)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
}, [handlers]);
|
|
515
|
+
const unregisterField = React.useCallback(formKey => {
|
|
516
|
+
const prev = stateRef.current;
|
|
517
|
+
if (!prev.fields[formKey]) return;
|
|
518
|
+
const {
|
|
519
|
+
[formKey]: _,
|
|
520
|
+
...remainingFields
|
|
521
|
+
} = prev.fields;
|
|
522
|
+
stateRef.current = {
|
|
523
|
+
...prev,
|
|
524
|
+
fields: remainingFields
|
|
525
|
+
};
|
|
526
|
+
listeners.current.forEach(listener => listener());
|
|
527
|
+
}, []);
|
|
513
528
|
const store = React.useMemo(() => {
|
|
514
529
|
if (!listeners.current) {
|
|
515
530
|
listeners.current = new Set();
|
|
531
|
+
stateRef.current = initialState;
|
|
532
|
+
for (const fieldKey in initialState.fields) {
|
|
533
|
+
initialState.fields[fieldKey].dispatchChanges = changes => handlers._dispatchChanges(changes, fieldKey);
|
|
534
|
+
}
|
|
516
535
|
} else {
|
|
517
536
|
listeners.current.clear();
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
for (const fieldKey in initialState.fields) {
|
|
522
|
-
initialState.fields[fieldKey].dispatchChanges = changes => handlers._dispatchChanges(changes, fieldKey);
|
|
537
|
+
for (const fieldKey in stateRef.current.fields) {
|
|
538
|
+
stateRef.current.fields[fieldKey].dispatchChanges = changes => handlers._dispatchChanges(changes, fieldKey);
|
|
539
|
+
}
|
|
523
540
|
}
|
|
524
541
|
return {
|
|
525
542
|
getState: () => stateRef.current,
|
|
@@ -528,7 +545,10 @@ const GFormContextProvider = ({
|
|
|
528
545
|
listeners.current.add(listener);
|
|
529
546
|
return () => listeners.current.delete(listener);
|
|
530
547
|
},
|
|
531
|
-
handlers
|
|
548
|
+
handlers,
|
|
549
|
+
registerField,
|
|
550
|
+
unregisterField,
|
|
551
|
+
getInputElement
|
|
532
552
|
};
|
|
533
553
|
}, [initialState]);
|
|
534
554
|
return React.createElement(GFormContext.Provider, {
|
|
@@ -537,7 +557,7 @@ const GFormContextProvider = ({
|
|
|
537
557
|
};
|
|
538
558
|
const useFormStore = () => {
|
|
539
559
|
const store = React.useContext(GFormContext);
|
|
540
|
-
if (!store.getState) throw new Error('
|
|
560
|
+
if (!store.getState) throw new Error('useFormStore must be used within `GForm` component');
|
|
541
561
|
return store;
|
|
542
562
|
};
|
|
543
563
|
const useFormSelector = selector => {
|
|
@@ -567,9 +587,9 @@ const FormRenderer = React.forwardRef(({
|
|
|
567
587
|
onKeyUp,
|
|
568
588
|
children,
|
|
569
589
|
onInit,
|
|
590
|
+
formRef,
|
|
570
591
|
...rest
|
|
571
592
|
}, ref) => {
|
|
572
|
-
const formRef = React.useRef(null);
|
|
573
593
|
const {
|
|
574
594
|
handlers,
|
|
575
595
|
getState
|
|
@@ -663,15 +683,18 @@ const GForm = React.forwardRef(({
|
|
|
663
683
|
optimized,
|
|
664
684
|
...props
|
|
665
685
|
}, ref) => {
|
|
666
|
-
const initialState = React.useMemo(() => {
|
|
667
|
-
|
|
668
|
-
}, [
|
|
686
|
+
const initialState = React.useMemo(() => ({
|
|
687
|
+
fields: {}
|
|
688
|
+
}), []);
|
|
689
|
+
const formRef = React.useRef(null);
|
|
669
690
|
return React.createElement(GFormContextProvider, {
|
|
670
691
|
initialState: initialState,
|
|
692
|
+
formRef: formRef,
|
|
671
693
|
validators: validators,
|
|
672
694
|
optimized: optimized
|
|
673
695
|
}, React.createElement(FormRenderer, _extends({
|
|
674
|
-
ref: ref
|
|
696
|
+
ref: ref,
|
|
697
|
+
formRef: formRef
|
|
675
698
|
}, props), children));
|
|
676
699
|
});
|
|
677
700
|
|
|
@@ -681,27 +704,42 @@ const makeSelectFields = (keys = []) => createSelector(selectFields, fields => {
|
|
|
681
704
|
return selected.length ? selected : null;
|
|
682
705
|
});
|
|
683
706
|
|
|
684
|
-
const _GInput = React.forwardRef(({
|
|
685
|
-
formKey,
|
|
686
|
-
element,
|
|
687
|
-
title,
|
|
688
|
-
type = 'text',
|
|
689
|
-
fetch,
|
|
690
|
-
fetchDeps,
|
|
691
|
-
optimized,
|
|
692
|
-
debounce = 300,
|
|
693
|
-
defaultChecked,
|
|
694
|
-
defaultValue,
|
|
695
|
-
checked,
|
|
696
|
-
validatorKey,
|
|
697
|
-
value,
|
|
698
|
-
...rest
|
|
699
|
-
}, ref) => {
|
|
700
|
-
const inputState = useFormSelector(state => state.fields[formKey]);
|
|
707
|
+
const _GInput = React.forwardRef((props, ref) => {
|
|
701
708
|
const store = useFormStore();
|
|
709
|
+
const {
|
|
710
|
+
formKey,
|
|
711
|
+
element,
|
|
712
|
+
title,
|
|
713
|
+
type = 'text',
|
|
714
|
+
fetch,
|
|
715
|
+
fetchDeps,
|
|
716
|
+
optimized,
|
|
717
|
+
debounce = 300,
|
|
718
|
+
defaultChecked,
|
|
719
|
+
defaultValue,
|
|
720
|
+
checked,
|
|
721
|
+
validatorKey,
|
|
722
|
+
value,
|
|
723
|
+
...rest
|
|
724
|
+
} = props;
|
|
725
|
+
if (!store.getState().fields[formKey]) {
|
|
726
|
+
store.registerField(props);
|
|
727
|
+
}
|
|
728
|
+
const inputState = useFormSelector(state => state.fields[formKey]);
|
|
729
|
+
const _fetchDeps = useFormSelector(makeSelectFields(fetchDeps));
|
|
730
|
+
React.useEffect(() => {
|
|
731
|
+
if (inputState.value) {
|
|
732
|
+
store.handlers._viHandler(inputState, {
|
|
733
|
+
target: store.getInputElement(formKey)
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
return () => {
|
|
737
|
+
store.unregisterField(formKey);
|
|
738
|
+
};
|
|
739
|
+
}, []);
|
|
702
740
|
const _element = React.useMemo(() => {
|
|
703
741
|
let value, checked;
|
|
704
|
-
if (type === 'checkbox') checked = inputState.value || false;else value = inputState.value || '';
|
|
742
|
+
if (type === 'checkbox') checked = inputState.value || false;else if (type === 'number') value = inputState.value || 0;else value = inputState.value || '';
|
|
705
743
|
const _props = {
|
|
706
744
|
...rest,
|
|
707
745
|
type,
|
|
@@ -734,25 +772,12 @@ const _GInput = React.forwardRef(({
|
|
|
734
772
|
} : (e, unknown) => {
|
|
735
773
|
store.handlers._updateInputHandler(inputState, e, unknown);
|
|
736
774
|
};
|
|
737
|
-
if (!inputState.touched && inputState.dispatchChanges) {
|
|
738
|
-
_props.onFocus = rest.onFocus ? e => {
|
|
739
|
-
rest.onFocus(e);
|
|
740
|
-
inputState.dispatchChanges({
|
|
741
|
-
touched: true
|
|
742
|
-
});
|
|
743
|
-
} : () => {
|
|
744
|
-
inputState.dispatchChanges({
|
|
745
|
-
touched: true
|
|
746
|
-
});
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
775
|
}
|
|
750
776
|
if (element) {
|
|
751
777
|
return element(inputState, _props);
|
|
752
778
|
}
|
|
753
779
|
return React.createElement("input", _props);
|
|
754
780
|
}, [inputState, element]);
|
|
755
|
-
const _fetchDeps = useFormSelector(makeSelectFields(fetchDeps));
|
|
756
781
|
React.useEffect(() => {
|
|
757
782
|
if (fetch) {
|
|
758
783
|
_debounce(debounce, `${inputState.gid}-fetch`).then(() => {
|