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.
Files changed (42) hide show
  1. package/README.md +107 -30
  2. package/dist/cjs/gform-react.development.js +165 -140
  3. package/dist/cjs/gform-react.development.js.map +1 -1
  4. package/dist/cjs/gform-react.production.js +1 -1
  5. package/dist/cjs/gform-react.production.js.map +1 -1
  6. package/dist/esm/GForm.development.js +12 -9
  7. package/dist/esm/GForm.development.js.map +1 -1
  8. package/dist/esm/GForm.production.js +1 -1
  9. package/dist/esm/GForm.production.js.map +1 -1
  10. package/dist/esm/GInput.development.js +22 -21
  11. package/dist/esm/GInput.development.js.map +1 -1
  12. package/dist/esm/GInput.production.js +1 -1
  13. package/dist/esm/GInput.production.js.map +1 -1
  14. package/dist/esm/GValidator.development.js +5 -1
  15. package/dist/esm/GValidator.development.js.map +1 -1
  16. package/dist/esm/GValidator.production.js +1 -1
  17. package/dist/esm/GValidator.production.js.map +1 -1
  18. package/dist/esm/shared.development.js +133 -112
  19. package/dist/esm/shared.development.js.map +1 -1
  20. package/dist/esm/shared.production.js +1 -1
  21. package/dist/esm/shared.production.js.map +1 -1
  22. package/dist/esm/useFormSelector.development.js +3 -2
  23. package/dist/esm/useFormSelector.development.js.map +1 -1
  24. package/dist/esm/useFormSelector.production.js +1 -1
  25. package/dist/index.d.ts +1 -0
  26. package/native/dist/cjs/gform-react.development.js +151 -133
  27. package/native/dist/cjs/gform-react.development.js.map +1 -1
  28. package/native/dist/cjs/gform-react.production.js +1 -1
  29. package/native/dist/cjs/gform-react.production.js.map +1 -1
  30. package/native/dist/esm/RNGForm.development.js +4 -4
  31. package/native/dist/esm/RNGForm.development.js.map +1 -1
  32. package/native/dist/esm/RNGForm.production.js +1 -1
  33. package/native/dist/esm/RNGForm.production.js.map +1 -1
  34. package/native/dist/esm/RNGInput.development.js +23 -21
  35. package/native/dist/esm/RNGInput.development.js.map +1 -1
  36. package/native/dist/esm/RNGInput.production.js +1 -1
  37. package/native/dist/esm/RNGInput.production.js.map +1 -1
  38. package/native/dist/esm/shared.development.js +133 -112
  39. package/native/dist/esm/shared.development.js.map +1 -1
  40. package/native/dist/esm/shared.production.js +1 -1
  41. package/native/dist/esm/shared.production.js.map +1 -1
  42. package/package.json +3 -3
package/README.md CHANGED
@@ -1,42 +1,119 @@
1
- # GForm
2
-
3
- ![gform-react](https://gform-react.onrender.com/gform-logo.png)
4
-
5
- ![npm bundle size](https://img.shields.io/bundlephobia/min/gform-react?label=minified%20size&color=darkergreen)
6
- ![npm bundle size](https://img.shields.io/bundlephobia/minzip/gform-react?label=gzip%20size&color=darkergreen)
7
- ![npm peer dependency version](https://img.shields.io/npm/dependency-version/gform-react/peer/react)
8
- ![npm peer dependency version](https://img.shields.io/npm/dependency-version/gform-react/peer/react-dom)
9
- [![NPM](https://img.shields.io/npm/l/gform-react)](https://unpkg.com/gform-react@latest/LICENSE.md)
10
-
11
- #### build generic forms with validations for react-based applications.
12
- it doesn't matter which UI library you're using,
13
- it only cares about the form and the inputs inside.
14
-
15
- ## Pros
16
- * Lightweight
17
- * Based on native form and constraint validations (can also add custom and async validations)
18
- * Can be used with any UI library that preserves native form controls (input, button, etc. can also be adjusted to non-native controls)
19
- * Tree shakeable
20
- * Accessibility semi-automatic (required inputs and invalid inputs automatically sets aria-required and aria-invalid)
21
- * React Native support
22
- * Supports React 19, Next.js 15
23
-
24
- ## Docs
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
- ## Demo:
28
- https://codesandbox.io/p/sandbox/gifted-elbakyan-fs5g2c
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
- ```shell
102
+
103
+ ```sh
33
104
  npm install gform-react
34
105
  ```
35
106
 
36
107
  yarn:
37
- ```shell
108
+
109
+ ```sh
38
110
  yarn add gform-react
39
111
  ```
40
112
 
41
- #### NOTE:
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 _copyStateFields = (source, destination) => {
24
- for (const key in destination.fields) {
25
- const sourceField = source.fields[key];
26
- const destField = destination.fields[key];
27
- if (!sourceField || sourceField.type !== destField.type) continue;
28
- destination.fields[key] = {
29
- ...destField,
30
- ...sourceField
31
- };
32
- }
33
- };
34
- const _buildFormInitialValues = (rows = []) => {
35
- const fields = {};
36
- const inputs = _findInputs(rows);
37
- inputs.forEach(config => {
38
- if (fields[config.formKey]) {
39
- console.warn(`DEV ONLY - [Duplicate Keys] - field with key '${config.formKey}' already defined.`);
40
- }
41
- const {
42
- required = false,
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
- fields: fields
80
- };
81
- };
82
- const _findInputs = (root, total = []) => {
83
- if (!root) return total;
84
- React.Children.forEach(root, child => {
85
- if (!React.isValidElement(child)) return;
86
- if (child.props) {
87
- const {
88
- formKey,
89
- children
90
- } = child.props;
91
- if (formKey) {
92
- total.push(child.props);
93
- }
94
- if (children) {
95
- _findInputs(children, total);
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 = (validity, exclude = []) => {
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
- this._constraintHandlers.push((input, key) => {
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 && !input.touched;
345
+ const hasInitialValue = !input.dirty && input.value;
376
346
  if (hasInitialValue) {
377
- _checkInputManually(input);
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 exclude = input.type && (input.pattern || hasCustomValidation(input)) ? ['typeMismatch'] : [];
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
- }, exclude);
376
+ });
406
377
  if (!validityKey && input.error) {
407
378
  validityKey = 'customError';
408
379
  }
409
380
  _validateInput(input, validityKey);
410
- return !input.error;
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') console.warn(`DEV ONLY - [Missing Validator] - the input '${input.formKey}' has described the constraint '${validityMap[validityKey]}' however, a correspond validator/custom validation/pattern 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().withPatternMismatchMessage('pattern mismatch'),\n\t...\n}\n\nor either remove the constraint '${validityMap[validityKey]}' from the input props`);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`);
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 result = inputValidator.constraintHandlers[index](input, validityKey);
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 result = inputValidator.handlers[index](input, fields);
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 result = await inputValidator.asyncHandlers[index](input, fields);
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(l => l());
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
- _copyStateFields(stateRef.current, initialState);
519
- }
520
- stateRef.current = initialState;
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('useGFormStore must be used within `GForm` component');
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
- return _buildFormInitialValues(typeof children === 'function' ? children({}) : children);
668
- }, [children]);
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(() => {