@conform-to/react 0.4.0-pre.0 → 0.4.0-pre.2

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 CHANGED
@@ -10,8 +10,12 @@
10
10
  - [useFieldset](#usefieldset)
11
11
  - [useFieldList](#usefieldlist)
12
12
  - [useControlledInput](#usecontrolledinput)
13
- - [createValidate](#createvalidate)
14
13
  - [conform](#conform)
14
+ - [hasError](#haserror)
15
+ - [isFieldElement](#isfieldelement)
16
+ - [parse](#parse)
17
+ - [setFormError](#setformerror)
18
+ - [shouldValidate](#shouldvalidate)
15
19
 
16
20
  <!-- /aside -->
17
21
 
@@ -21,15 +25,23 @@ By default, the browser calls the [reportValidity()](https://developer.mozilla.o
21
25
 
22
26
  This hook enhances the form validation behaviour in 3 parts:
23
27
 
24
- 1. It lets you hook up custom validation logic into different form events. For example, revalidation will be triggered whenever something changed.
25
- 2. It provides options for you to decide the best timing to start reporting errors. This could be as earliest as the user start typing, or also as late as the user try submitting the form.
28
+ 1. It enhances form validation with custom rules by subscribing to different DOM events and reporting the errors only when it is configured to do so.
29
+ 2. It unifies client and server validation in one place.
26
30
  3. It exposes the state of each field in the form of [data attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*), such as `data-conform-touched`, allowing flexible styling across your form without the need to manipulate the class names.
27
31
 
28
32
  ```tsx
29
33
  import { useForm } from '@conform-to/react';
30
34
 
31
35
  function LoginForm() {
32
- const formProps = useForm({
36
+ const form = useForm({
37
+ /**
38
+ * Validation mode.
39
+ * Support "client-only" or "server-validation".
40
+ *
41
+ * Default to `client-only`.
42
+ */
43
+ mode: 'client-only',
44
+
33
45
  /**
34
46
  * Define when the error should be reported initially.
35
47
  * Support "onSubmit", "onChange", "onBlur".
@@ -38,6 +50,16 @@ function LoginForm() {
38
50
  */
39
51
  initialReport: 'onBlur',
40
52
 
53
+ /**
54
+ * An object representing the initial value of the form.
55
+ */
56
+ defaultValue: undefined;
57
+
58
+ /**
59
+ * An object describing the state from the last submission
60
+ */
61
+ state: undefined;
62
+
41
63
  /**
42
64
  * Enable native validation before hydation.
43
65
  *
@@ -54,43 +76,38 @@ function LoginForm() {
54
76
 
55
77
  /**
56
78
  * A function to be called when the form should be (re)validated.
79
+ * Only sync validation is supported
57
80
  */
58
- validate(form, submitter) {
81
+ onValidate({ form, formData, submission }) {
59
82
  // ...
60
83
  },
61
84
 
62
85
  /**
63
86
  * The submit event handler of the form.
64
87
  */
65
- onSubmit(event) {
88
+ onSubmit(event, { form, formData, submission }) {
66
89
  // ...
67
90
  },
68
91
  });
69
92
 
70
- return (
71
- <form {...formProps}>
72
- <input type="email" name="email" required />
73
- <input type="password" name="password" required />
74
- <button type="submit">Login</button>
75
- </form>
76
- );
93
+ // ...
77
94
  }
78
95
  ```
79
96
 
80
97
  <details>
81
- <summary>What is `formProps`?</summary>
98
+ <summary>What is `form.props`?</summary>
82
99
 
83
100
  It is a group of properties properties required to hook into form events. They can also be set explicitly as shown below:
84
101
 
85
102
  ```tsx
86
103
  function RandomForm() {
87
- const formProps = useForm();
104
+ const form = useForm();
88
105
 
89
106
  return (
90
107
  <form
91
- ref={formProps.ref}
92
- onSubmit={formProps.onSubmit}
93
- noValidate={formProps.noValidate}
108
+ ref={form.props.ref}
109
+ onSubmit={form.props.onSubmit}
110
+ noValidate={form.props.noValidate}
94
111
  >
95
112
  {/* ... */}
96
113
  </form>
@@ -103,17 +120,17 @@ function RandomForm() {
103
120
  <details>
104
121
  <summary>Does it work with custom form component like Remix Form?</summary>
105
122
 
106
- Yes! It will fallback to native form submission if the submit event handler is omitted or the event is not default prevented.
123
+ Yes! It will fallback to native form submission as long as the submit event is not default prevented.
107
124
 
108
125
  ```tsx
109
126
  import { useFrom } from '@conform-to/react';
110
127
  import { Form } from '@remix-run/react';
111
128
 
112
129
  function LoginForm() {
113
- const formProps = useForm();
130
+ const form = useForm();
114
131
 
115
132
  return (
116
- <Form method="post" action="/login" {...formProps}>
133
+ <Form method="post" action="/login" {...form.props}>
117
134
  {/* ... */}
118
135
  </Form>
119
136
  );
@@ -123,9 +140,9 @@ function LoginForm() {
123
140
  </details>
124
141
 
125
142
  <details>
126
- <summary>Is the `validate` function required?</summary>
143
+ <summary>Is the `onValidate` function required?</summary>
127
144
 
128
- The `validate` function is not required if the validation logic can be fully covered by the [native constraints](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Constraint_validation#validation-related_attributes), e.g. **required** / **min** / **pattern** etc.
145
+ The `onValidate` function is not required if the validation logic can be fully covered by the [native constraints](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Constraint_validation#validation-related_attributes), e.g. **required** / **min** / **pattern** etc.
129
146
 
130
147
  ```tsx
131
148
  import { useForm, useFieldset } from '@conform-to/react';
@@ -156,10 +173,10 @@ function LoginForm() {
156
173
 
157
174
  ### useFieldset
158
175
 
159
- This hook can be used to monitor the state of each field and help fields configuration. It lets you:
176
+ This hook can be used to monitor the state of each field and help configuration. It lets you:
160
177
 
161
178
  1. Capturing errors at the form/fieldset level, removing the need to setup invalid handler on each field.
162
- 2. Defining config in one central place. e.g. name, default value and constraint, then distributing it to each field using the [conform](#conform) helpers.
179
+ 2. Defining config in one central place. e.g. name, default value and constraint, then distributing it to each field with the [conform](#conform) helpers.
163
180
 
164
181
  ```tsx
165
182
  import { useForm, useFieldset } from '@conform-to/react';
@@ -493,58 +510,7 @@ function MuiForm() {
493
510
  <MenuItem value="c">Category C</MenuItem>
494
511
  </TextField>
495
512
  </fieldset>
496
- )
497
- }
498
- ```
499
-
500
- ---
501
-
502
- ### createValidate
503
-
504
- This help you configure a validate function to check the validity of each fields and setup custom messages using the Constraint Validation APIs.
505
-
506
- ```tsx
507
- import { useForm, createValidate } from '@conform-to/react';
508
-
509
- export default function SignupForm() {
510
- const formProps = useForm({
511
- validate: createValidate((field, formData) => {
512
- switch (field.name) {
513
- case 'email':
514
- if (field.validity.valueMissing) {
515
- field.setCustomValidity('Email is required');
516
- } else if (field.validity.typeMismatch) {
517
- field.setCustomValidity('Please enter a valid email');
518
- } else {
519
- field.setCustomValidity('');
520
- }
521
- break;
522
- case 'password':
523
- if (field.validity.valueMissing) {
524
- field.setCustomValidity('Password is required');
525
- } else if (field.validity.tooShort) {
526
- field.setCustomValidity(
527
- 'The password should be at least 10 characters long',
528
- );
529
- } else {
530
- field.setCustomValidity('');
531
- }
532
- break;
533
- case 'confirm-password': {
534
- if (field.validity.valueMissing) {
535
- field.setCustomValidity('Confirm Password is required');
536
- } else if (field.value !== formData.get('password')) {
537
- field.setCustomValidity('The password does not match');
538
- } else {
539
- field.setCustomValidity('');
540
- }
541
- break;
542
- }
543
- }
544
- }),
545
- });
546
-
547
- return <form {...formProps}>{/* ... */}</form>;
513
+ );
548
514
  }
549
515
  ```
550
516
 
@@ -615,3 +581,146 @@ function RandomForm() {
615
581
  );
616
582
  }
617
583
  ```
584
+
585
+ ### hasError
586
+
587
+ This helper checks if there is any message defined in error array with the provided name.
588
+
589
+ ```ts
590
+ import { hasError } from '@conform-to/react';
591
+
592
+ /**
593
+ * Assume the error looks like this:
594
+ */
595
+ const error = [['email', 'Email is required']];
596
+
597
+ // This will log `true`
598
+ console.log(hasError(error, 'email'));
599
+
600
+ // This will log `false`
601
+ console.log(hasError(error, 'password'));
602
+ ```
603
+
604
+ ---
605
+
606
+ ### isFieldElement
607
+
608
+ This checks if the provided element is an `input` / `select` / `textarea` or `button` HTML element with type guard. Useful when you need to access the validityState of the fields and modify the validation message manually.
609
+
610
+ ```tsx
611
+ import { isFieldElement } from '@conform-to/react';
612
+
613
+ export default function SignupForm() {
614
+ const form = useForm({
615
+ onValidate({ form }) {
616
+ for (const element of form.elements) {
617
+ if (isFieldElement(element)) {
618
+ switch (field.name) {
619
+ case 'email':
620
+ if (field.validity.valueMissing) {
621
+ field.setCustomValidity('Email is required');
622
+ } else if (field.validity.typeMismatch) {
623
+ field.setCustomValidity('Please enter a valid email');
624
+ } else {
625
+ field.setCustomValidity('');
626
+ }
627
+ break;
628
+ case 'password':
629
+ if (field.validity.valueMissing) {
630
+ field.setCustomValidity('Password is required');
631
+ } else if (field.validity.tooShort) {
632
+ field.setCustomValidity(
633
+ 'The password should be at least 10 characters long',
634
+ );
635
+ } else {
636
+ field.setCustomValidity('');
637
+ }
638
+ break;
639
+ case 'confirm-password': {
640
+ if (field.validity.valueMissing) {
641
+ field.setCustomValidity('Confirm Password is required');
642
+ } else if (field.value !== formData.get('password')) {
643
+ field.setCustomValidity('The password does not match');
644
+ } else {
645
+ field.setCustomValidity('');
646
+ }
647
+ break;
648
+ }
649
+ }
650
+ }
651
+ }
652
+ },
653
+ });
654
+
655
+ // ...
656
+ }
657
+ ```
658
+
659
+ ---
660
+
661
+ ### parse
662
+
663
+ It parses the formData based on the [naming convention](/docs/submission).
664
+
665
+ ```tsx
666
+ import { parse } from '@conform-to/react';
667
+
668
+ const formData = new FormData(formElement);
669
+ const submission = parse(formData);
670
+
671
+ console.log(submission);
672
+ ```
673
+
674
+ ---
675
+
676
+ ### setFormError
677
+
678
+ This helpers updates the form error based on the submission result by looping through all elements in the form and update the error with the [setCustomValidity](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setCustomValidity) API.
679
+
680
+ ```tsx
681
+ import { setFormError } from '@conform-to/react';
682
+
683
+ function ExampleForm() {
684
+ const form = useForm({
685
+ onValidate({ form, submission }) {
686
+ const error = validate(submission);
687
+
688
+ setFormError(form, {
689
+ ...submission,
690
+ error,
691
+ });
692
+ },
693
+ });
694
+
695
+ // ...
696
+ }
697
+ ```
698
+
699
+ ---
700
+
701
+ ### shouldValidate
702
+
703
+ This helper checks if the scope of validation includes a specific field by checking the submission:
704
+
705
+ ```tsx
706
+ import { shouldValidate } from '@conform-to/react';
707
+
708
+ /**
709
+ * The submission type and metadata give us hint on what should be valdiated.
710
+ * If the type is 'validate', only the field with name matching the metadata must be validated.
711
+ *
712
+ * However, if the type is `undefined`, both will log true (Default submission)
713
+ */
714
+ const submission = {
715
+ type: 'validate',
716
+ metadata: 'email',
717
+ value: {},
718
+ error: [],
719
+ };
720
+
721
+ // This will log 'true'
722
+ console.log(shouldValidate(submission, 'email'));
723
+
724
+ // This will log 'false'
725
+ console.log(shouldValidate(submission, 'password'));
726
+ ```
package/hooks.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type FieldConfig, type FieldElement, type FieldValue, type FieldsetConstraint, type FormState, type ListCommand, type Primitive, type Submission } from '@conform-to/dom';
1
+ import { type FieldConfig, type FieldElement, type FieldValue, type FieldsetConstraint, type ListCommand, type Primitive, type Submission } from '@conform-to/dom';
2
2
  import { type InputHTMLAttributes, type FormEvent, type RefObject } from 'react';
3
3
  interface FormContext<Schema extends Record<string, any>> {
4
4
  form: HTMLFormElement;
@@ -6,6 +6,10 @@ interface FormContext<Schema extends Record<string, any>> {
6
6
  submission: Submission<Schema>;
7
7
  }
8
8
  export interface FormConfig<Schema extends Record<string, any>> {
9
+ /**
10
+ * Validation mode. Default to `client-only`.
11
+ */
12
+ mode?: 'client-only' | 'server-validation';
9
13
  /**
10
14
  * Define when the error should be reported initially.
11
15
  * Support "onSubmit", "onChange", "onBlur".
@@ -20,7 +24,7 @@ export interface FormConfig<Schema extends Record<string, any>> {
20
24
  /**
21
25
  * An object describing the state from the last submission
22
26
  */
23
- state?: FormState<Schema>;
27
+ state?: Submission<Schema>;
24
28
  /**
25
29
  * Enable native validation before hydation.
26
30
  *
@@ -36,7 +40,7 @@ export interface FormConfig<Schema extends Record<string, any>> {
36
40
  /**
37
41
  * A function to be called when the form should be (re)validated.
38
42
  */
39
- onValidate?: (context: FormContext<Schema>) => boolean;
43
+ onValidate?: (context: FormContext<Schema>) => Array<[string, string]>;
40
44
  /**
41
45
  * The submit event handler of the form. It will be called
42
46
  * only when the form is considered valid.
@@ -61,7 +65,7 @@ interface Form<Schema extends Record<string, any>> {
61
65
  * Returns properties required to hook into form events.
62
66
  * Applied custom validation and define when error should be reported.
63
67
  *
64
- * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.0/packages/conform-react/README.md#useform
68
+ * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.2/packages/conform-react/README.md#useform
65
69
  */
66
70
  export declare function useForm<Schema extends Record<string, any>>(config?: FormConfig<Schema>): Form<Schema>;
67
71
  /**
@@ -102,7 +106,7 @@ export interface FieldsetConfig<Schema extends Record<string, any>> {
102
106
  /**
103
107
  * Returns all the information about the fieldset.
104
108
  *
105
- * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.0/packages/conform-react/README.md#usefieldset
109
+ * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.2/packages/conform-react/README.md#usefieldset
106
110
  */
107
111
  export declare function useFieldset<Schema extends Record<string, any>>(ref: RefObject<HTMLFormElement | HTMLFieldSetElement>, config?: FieldsetConfig<Schema>): Fieldset<Schema>;
108
112
  export declare function useFieldset<Schema extends Record<string, any>>(ref: RefObject<HTMLFormElement | HTMLFieldSetElement>, config?: FieldConfig<Schema>): Fieldset<Schema>;
@@ -129,7 +133,7 @@ interface ListControl<Schema> {
129
133
  * Returns a list of key and config, with a group of helpers
130
134
  * configuring buttons for list manipulation
131
135
  *
132
- * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.0/packages/conform-react/README.md#usefieldlist
136
+ * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.2/packages/conform-react/README.md#usefieldlist
133
137
  */
134
138
  export declare function useFieldList<Payload = any>(ref: RefObject<HTMLFormElement | HTMLFieldSetElement>, config: FieldConfig<Array<Payload>>): [
135
139
  Array<{
@@ -159,7 +163,7 @@ interface InputControl<Element extends {
159
163
  * This is particular useful when integrating dropdown and datepicker whichs
160
164
  * introduces custom input mode.
161
165
  *
162
- * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.0/packages/conform-react/README.md#usecontrolledinput
166
+ * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.2/packages/conform-react/README.md#usecontrolledinput
163
167
  */
164
168
  export declare function useControlledInput<Element extends {
165
169
  focus: () => void;
package/hooks.js CHANGED
@@ -11,7 +11,7 @@ var helpers = require('./helpers.js');
11
11
  * Returns properties required to hook into form events.
12
12
  * Applied custom validation and define when error should be reported.
13
13
  *
14
- * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.0/packages/conform-react/README.md#useform
14
+ * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.2/packages/conform-react/README.md#useform
15
15
  */
16
16
  function useForm() {
17
17
  var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
@@ -27,15 +27,14 @@ function useForm() {
27
27
  return message !== null && message !== void 0 ? message : '';
28
28
  });
29
29
  var [fieldsetConfig, setFieldsetConfig] = react.useState(() => {
30
- var _config$state$error2, _config$state2, _config$state3, _config$state$value, _config$state4;
30
+ var _config$state$error2, _config$state2, _config$state$value, _config$state3;
31
31
 
32
32
  var error = (_config$state$error2 = (_config$state2 = config.state) === null || _config$state2 === void 0 ? void 0 : _config$state2.error) !== null && _config$state$error2 !== void 0 ? _config$state$error2 : [];
33
- var scope = (_config$state3 = config.state) === null || _config$state3 === void 0 ? void 0 : _config$state3.scope;
34
33
  return {
35
- defaultValue: (_config$state$value = (_config$state4 = config.state) === null || _config$state4 === void 0 ? void 0 : _config$state4.value) !== null && _config$state$value !== void 0 ? _config$state$value : config.defaultValue,
34
+ defaultValue: (_config$state$value = (_config$state3 = config.state) === null || _config$state3 === void 0 ? void 0 : _config$state3.value) !== null && _config$state$value !== void 0 ? _config$state$value : config.defaultValue,
36
35
  initialError: error.filter(_ref2 => {
37
36
  var [name] = _ref2;
38
- return name !== '' && dom.getSubmissionType(name) === null && (!scope || scope.includes(name));
37
+ return name !== '' && dom.getSubmissionType(name) === null;
39
38
  })
40
39
  };
41
40
  });
@@ -53,8 +52,10 @@ function useForm() {
53
52
  return;
54
53
  }
55
54
 
56
- if (!dom.reportValidity(form, config.state)) {
57
- dom.focusFirstInvalidField(form, config.state.scope);
55
+ dom.setFormError(form, config.state);
56
+
57
+ if (!form.reportValidity()) {
58
+ dom.focusFirstInvalidField(form);
58
59
  }
59
60
 
60
61
  dom.requestSubmit(form);
@@ -181,38 +182,62 @@ function useForm() {
181
182
  return;
182
183
  }
183
184
 
184
- var formData = dom.getFormData(form, submitter);
185
- var submission = dom.parse(formData);
186
- var context = {
187
- form,
188
- formData,
189
- submission
190
- }; // Touch all fields only if the submitter is not a command button
191
-
192
- if (!submission.type) {
193
- for (var field of form.elements) {
194
- if (dom.isFieldElement(field)) {
195
- // Mark the field as touched
196
- field.dataset.conformTouched = 'true';
185
+ try {
186
+ var formData = dom.getFormData(form, submitter);
187
+ var submission = dom.parse(formData);
188
+ var _context = {
189
+ form,
190
+ formData,
191
+ submission
192
+ }; // Touch all fields only if the submitter is not a command button
193
+
194
+ if (submission.context === 'submit') {
195
+ for (var field of form.elements) {
196
+ if (dom.isFieldElement(field)) {
197
+ // Mark the field as touched
198
+ field.dataset.conformTouched = 'true';
199
+ }
197
200
  }
198
201
  }
199
- }
200
202
 
201
- if (typeof config.onValidate === 'function' && !config.noValidate && !submitter.formNoValidate) {
202
- try {
203
- if (!config.onValidate(context)) {
204
- dom.focusFirstInvalidField(form);
205
- event.preventDefault();
203
+ var _error;
204
+
205
+ if (typeof config.onValidate === 'function') {
206
+ _error = config.onValidate(_context);
207
+ } else {
208
+ if (config.mode !== 'server-validation') {
209
+ // Clear previous result
210
+ dom.setFormError(form, {
211
+ context: 'submit',
212
+ value: {},
213
+ error: []
214
+ });
206
215
  }
207
- } catch (e) {
208
- console.warn(e);
216
+
217
+ _error = dom.getFormError(form);
218
+ }
219
+
220
+ if (_error.length > 0) {
221
+ submission.error.push(..._error);
209
222
  }
210
- }
211
223
 
212
- if (!event.defaultPrevented) {
213
- var _config$onSubmit;
224
+ if (!config.noValidate && !submitter.formNoValidate && dom.hasError(submission.error) || submission.context === 'validate' && config.mode !== 'server-validation') {
225
+ event.preventDefault();
226
+ } else {
227
+ var _config$onSubmit;
214
228
 
215
- (_config$onSubmit = config.onSubmit) === null || _config$onSubmit === void 0 ? void 0 : _config$onSubmit.call(config, event, context);
229
+ (_config$onSubmit = config.onSubmit) === null || _config$onSubmit === void 0 ? void 0 : _config$onSubmit.call(config, event, _context);
230
+ }
231
+
232
+ if (event.defaultPrevented) {
233
+ dom.setFormError(form, submission);
234
+
235
+ if (!form.reportValidity()) {
236
+ dom.focusFirstInvalidField(form);
237
+ }
238
+ }
239
+ } catch (e) {
240
+ console.warn(e);
216
241
  }
217
242
  }
218
243
 
@@ -275,64 +300,12 @@ function useFieldset(ref, config) {
275
300
  configRef.current = config;
276
301
  });
277
302
  react.useEffect(() => {
278
- /**
279
- * Reset the error state of each field if its validity is changed.
280
- *
281
- * This is a workaround as no official way is provided to notify
282
- * when the validity of the field is changed from `invalid` to `valid`.
283
- */
284
- var resetError = form => {
285
- setError(prev => {
286
- var _configRef$current$na, _configRef$current;
287
-
288
- var next = prev;
289
- var fieldsetName = (_configRef$current$na = (_configRef$current = configRef.current) === null || _configRef$current === void 0 ? void 0 : _configRef$current.name) !== null && _configRef$current$na !== void 0 ? _configRef$current$na : '';
290
-
291
- for (var field of form.elements) {
292
- if (dom.isFieldElement(field) && field.name.startsWith(fieldsetName)) {
293
- var [key, ...paths] = dom.getPaths(fieldsetName.length > 0 ? field.name.slice(fieldsetName.length + 1) : field.name);
294
-
295
- if (typeof key === 'string' && paths.length === 0) {
296
- var _next$key, _next;
297
-
298
- var prevMessage = (_next$key = (_next = next) === null || _next === void 0 ? void 0 : _next[key]) !== null && _next$key !== void 0 ? _next$key : '';
299
- var nextMessage = field.validationMessage;
300
- /**
301
- * Techincally, checking prevMessage not being empty while nextMessage being empty
302
- * is sufficient for our usecase. It checks if the message is changed instead to allow
303
- * the hook to be useful independently.
304
- */
305
-
306
- if (prevMessage !== '' && prevMessage !== nextMessage) {
307
- next = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, next), {}, {
308
- [key]: nextMessage
309
- });
310
- }
311
- }
312
- }
313
- }
314
-
315
- return next;
316
- });
317
- };
318
-
319
- var handleInput = event => {
320
- var form = dom.getFormElement(ref.current);
321
- var field = event.target;
322
-
323
- if (!form || !dom.isFieldElement(field) || field.form !== form) {
324
- return;
325
- }
326
-
327
- resetError(form);
328
- };
329
-
330
303
  var invalidHandler = event => {
331
- var _configRef$current$na2, _configRef$current2;
304
+ var _configRef$current$na, _configRef$current;
332
305
 
333
306
  var form = dom.getFormElement(ref.current);
334
307
  var field = event.target;
335
- var fieldsetName = (_configRef$current$na2 = (_configRef$current2 = configRef.current) === null || _configRef$current2 === void 0 ? void 0 : _configRef$current2.name) !== null && _configRef$current$na2 !== void 0 ? _configRef$current$na2 : '';
308
+ var fieldsetName = (_configRef$current$na = (_configRef$current = configRef.current) === null || _configRef$current === void 0 ? void 0 : _configRef$current.name) !== null && _configRef$current$na !== void 0 ? _configRef$current$na : '';
336
309
 
337
310
  if (!form || !dom.isFieldElement(field) || field.form !== form || !field.name.startsWith(fieldsetName)) {
338
311
  return;
@@ -366,10 +339,39 @@ function useFieldset(ref, config) {
366
339
 
367
340
  if (!form || event.target !== form) {
368
341
  return;
369
- } // This helps resetting error that fullfilled by the submitter
342
+ }
343
+ /**
344
+ * Reset the error state of each field if its validity is changed.
345
+ *
346
+ * This is a workaround as no official way is provided to notify
347
+ * when the validity of the field is changed from `invalid` to `valid`.
348
+ */
349
+
370
350
 
351
+ setError(prev => {
352
+ var _configRef$current$na2, _configRef$current2;
353
+
354
+ var next = prev;
355
+ var fieldsetName = (_configRef$current$na2 = (_configRef$current2 = configRef.current) === null || _configRef$current2 === void 0 ? void 0 : _configRef$current2.name) !== null && _configRef$current$na2 !== void 0 ? _configRef$current$na2 : '';
356
+
357
+ for (var field of form.elements) {
358
+ if (dom.isFieldElement(field) && field.name.startsWith(fieldsetName)) {
359
+ var _next$key, _next;
360
+
361
+ var key = fieldsetName ? field.name.slice(fieldsetName.length + 1) : field.name;
362
+ var prevMessage = (_next$key = (_next = next) === null || _next === void 0 ? void 0 : _next[key]) !== null && _next$key !== void 0 ? _next$key : '';
363
+ var nextMessage = field.validationMessage;
371
364
 
372
- resetError(form);
365
+ if (prevMessage !== '' && nextMessage === '') {
366
+ next = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, next), {}, {
367
+ [key]: ''
368
+ });
369
+ }
370
+ }
371
+ }
372
+
373
+ return next;
374
+ });
373
375
  };
374
376
 
375
377
  var resetHandler = event => {
@@ -388,15 +390,13 @@ function useFieldset(ref, config) {
388
390
  initialError: {}
389
391
  });
390
392
  setError({});
391
- };
393
+ }; // The invalid event does not bubble and so listening on the capturing pharse is needed
392
394
 
393
- document.addEventListener('input', handleInput); // The invalid event does not bubble and so listening on the capturing pharse is needed
394
395
 
395
396
  document.addEventListener('invalid', invalidHandler, true);
396
397
  document.addEventListener('submit', submitHandler);
397
398
  document.addEventListener('reset', resetHandler);
398
399
  return () => {
399
- document.removeEventListener('input', handleInput);
400
400
  document.removeEventListener('invalid', invalidHandler, true);
401
401
  document.removeEventListener('submit', submitHandler);
402
402
  document.removeEventListener('reset', resetHandler);
@@ -437,7 +437,7 @@ function useFieldset(ref, config) {
437
437
  * Returns a list of key and config, with a group of helpers
438
438
  * configuring buttons for list manipulation
439
439
  *
440
- * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.0/packages/conform-react/README.md#usefieldlist
440
+ * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.2/packages/conform-react/README.md#usefieldlist
441
441
  */
442
442
  function useFieldList(ref, config) {
443
443
  var configRef = react.useRef(config);
@@ -581,7 +581,7 @@ function useFieldList(ref, config) {
581
581
  * This is particular useful when integrating dropdown and datepicker whichs
582
582
  * introduces custom input mode.
583
583
  *
584
- * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.0/packages/conform-react/README.md#usecontrolledinput
584
+ * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.2/packages/conform-react/README.md#usecontrolledinput
585
585
  */
586
586
  function useControlledInput(config) {
587
587
  var _config$defaultValue4;
package/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { type FieldsetConstraint, type FormState, type Submission, hasError, isFieldElement, parse, reportValidity, } from '@conform-to/dom';
1
+ export { type FieldsetConstraint, type Submission, getFormError, hasError, isFieldElement, parse, shouldValidate, } from '@conform-to/dom';
2
2
  export * from './hooks';
3
3
  export * as conform from './helpers';
package/index.js CHANGED
@@ -8,6 +8,10 @@ var helpers = require('./helpers.js');
8
8
 
9
9
 
10
10
 
11
+ Object.defineProperty(exports, 'getFormError', {
12
+ enumerable: true,
13
+ get: function () { return dom.getFormError; }
14
+ });
11
15
  Object.defineProperty(exports, 'hasError', {
12
16
  enumerable: true,
13
17
  get: function () { return dom.hasError; }
@@ -20,9 +24,9 @@ Object.defineProperty(exports, 'parse', {
20
24
  enumerable: true,
21
25
  get: function () { return dom.parse; }
22
26
  });
23
- Object.defineProperty(exports, 'reportValidity', {
27
+ Object.defineProperty(exports, 'shouldValidate', {
24
28
  enumerable: true,
25
- get: function () { return dom.reportValidity; }
29
+ get: function () { return dom.shouldValidate; }
26
30
  });
27
31
  exports.useControlledInput = hooks.useControlledInput;
28
32
  exports.useFieldList = hooks.useFieldList;
package/module/hooks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.js';
2
- import { getSubmissionType, reportValidity, focusFirstInvalidField, requestSubmit, isFieldElement, getFormData, parse, getPaths, getName, requestValidate, getFormElement, parseListCommand, updateList } from '@conform-to/dom';
2
+ import { getSubmissionType, setFormError, focusFirstInvalidField, requestSubmit, isFieldElement, getFormData, parse, getFormError, hasError, getPaths, getName, requestValidate, getFormElement, parseListCommand, updateList } from '@conform-to/dom';
3
3
  import { useRef, useState, useEffect } from 'react';
4
4
  import { input } from './helpers.js';
5
5
 
@@ -7,7 +7,7 @@ import { input } from './helpers.js';
7
7
  * Returns properties required to hook into form events.
8
8
  * Applied custom validation and define when error should be reported.
9
9
  *
10
- * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.0/packages/conform-react/README.md#useform
10
+ * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.2/packages/conform-react/README.md#useform
11
11
  */
12
12
  function useForm() {
13
13
  var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
@@ -23,15 +23,14 @@ function useForm() {
23
23
  return message !== null && message !== void 0 ? message : '';
24
24
  });
25
25
  var [fieldsetConfig, setFieldsetConfig] = useState(() => {
26
- var _config$state$error2, _config$state2, _config$state3, _config$state$value, _config$state4;
26
+ var _config$state$error2, _config$state2, _config$state$value, _config$state3;
27
27
 
28
28
  var error = (_config$state$error2 = (_config$state2 = config.state) === null || _config$state2 === void 0 ? void 0 : _config$state2.error) !== null && _config$state$error2 !== void 0 ? _config$state$error2 : [];
29
- var scope = (_config$state3 = config.state) === null || _config$state3 === void 0 ? void 0 : _config$state3.scope;
30
29
  return {
31
- defaultValue: (_config$state$value = (_config$state4 = config.state) === null || _config$state4 === void 0 ? void 0 : _config$state4.value) !== null && _config$state$value !== void 0 ? _config$state$value : config.defaultValue,
30
+ defaultValue: (_config$state$value = (_config$state3 = config.state) === null || _config$state3 === void 0 ? void 0 : _config$state3.value) !== null && _config$state$value !== void 0 ? _config$state$value : config.defaultValue,
32
31
  initialError: error.filter(_ref2 => {
33
32
  var [name] = _ref2;
34
- return name !== '' && getSubmissionType(name) === null && (!scope || scope.includes(name));
33
+ return name !== '' && getSubmissionType(name) === null;
35
34
  })
36
35
  };
37
36
  });
@@ -49,8 +48,10 @@ function useForm() {
49
48
  return;
50
49
  }
51
50
 
52
- if (!reportValidity(form, config.state)) {
53
- focusFirstInvalidField(form, config.state.scope);
51
+ setFormError(form, config.state);
52
+
53
+ if (!form.reportValidity()) {
54
+ focusFirstInvalidField(form);
54
55
  }
55
56
 
56
57
  requestSubmit(form);
@@ -177,38 +178,62 @@ function useForm() {
177
178
  return;
178
179
  }
179
180
 
180
- var formData = getFormData(form, submitter);
181
- var submission = parse(formData);
182
- var context = {
183
- form,
184
- formData,
185
- submission
186
- }; // Touch all fields only if the submitter is not a command button
187
-
188
- if (!submission.type) {
189
- for (var field of form.elements) {
190
- if (isFieldElement(field)) {
191
- // Mark the field as touched
192
- field.dataset.conformTouched = 'true';
181
+ try {
182
+ var formData = getFormData(form, submitter);
183
+ var submission = parse(formData);
184
+ var _context = {
185
+ form,
186
+ formData,
187
+ submission
188
+ }; // Touch all fields only if the submitter is not a command button
189
+
190
+ if (submission.context === 'submit') {
191
+ for (var field of form.elements) {
192
+ if (isFieldElement(field)) {
193
+ // Mark the field as touched
194
+ field.dataset.conformTouched = 'true';
195
+ }
193
196
  }
194
197
  }
195
- }
196
198
 
197
- if (typeof config.onValidate === 'function' && !config.noValidate && !submitter.formNoValidate) {
198
- try {
199
- if (!config.onValidate(context)) {
200
- focusFirstInvalidField(form);
201
- event.preventDefault();
199
+ var _error;
200
+
201
+ if (typeof config.onValidate === 'function') {
202
+ _error = config.onValidate(_context);
203
+ } else {
204
+ if (config.mode !== 'server-validation') {
205
+ // Clear previous result
206
+ setFormError(form, {
207
+ context: 'submit',
208
+ value: {},
209
+ error: []
210
+ });
202
211
  }
203
- } catch (e) {
204
- console.warn(e);
212
+
213
+ _error = getFormError(form);
214
+ }
215
+
216
+ if (_error.length > 0) {
217
+ submission.error.push(..._error);
205
218
  }
206
- }
207
219
 
208
- if (!event.defaultPrevented) {
209
- var _config$onSubmit;
220
+ if (!config.noValidate && !submitter.formNoValidate && hasError(submission.error) || submission.context === 'validate' && config.mode !== 'server-validation') {
221
+ event.preventDefault();
222
+ } else {
223
+ var _config$onSubmit;
210
224
 
211
- (_config$onSubmit = config.onSubmit) === null || _config$onSubmit === void 0 ? void 0 : _config$onSubmit.call(config, event, context);
225
+ (_config$onSubmit = config.onSubmit) === null || _config$onSubmit === void 0 ? void 0 : _config$onSubmit.call(config, event, _context);
226
+ }
227
+
228
+ if (event.defaultPrevented) {
229
+ setFormError(form, submission);
230
+
231
+ if (!form.reportValidity()) {
232
+ focusFirstInvalidField(form);
233
+ }
234
+ }
235
+ } catch (e) {
236
+ console.warn(e);
212
237
  }
213
238
  }
214
239
 
@@ -271,64 +296,12 @@ function useFieldset(ref, config) {
271
296
  configRef.current = config;
272
297
  });
273
298
  useEffect(() => {
274
- /**
275
- * Reset the error state of each field if its validity is changed.
276
- *
277
- * This is a workaround as no official way is provided to notify
278
- * when the validity of the field is changed from `invalid` to `valid`.
279
- */
280
- var resetError = form => {
281
- setError(prev => {
282
- var _configRef$current$na, _configRef$current;
283
-
284
- var next = prev;
285
- var fieldsetName = (_configRef$current$na = (_configRef$current = configRef.current) === null || _configRef$current === void 0 ? void 0 : _configRef$current.name) !== null && _configRef$current$na !== void 0 ? _configRef$current$na : '';
286
-
287
- for (var field of form.elements) {
288
- if (isFieldElement(field) && field.name.startsWith(fieldsetName)) {
289
- var [key, ...paths] = getPaths(fieldsetName.length > 0 ? field.name.slice(fieldsetName.length + 1) : field.name);
290
-
291
- if (typeof key === 'string' && paths.length === 0) {
292
- var _next$key, _next;
293
-
294
- var prevMessage = (_next$key = (_next = next) === null || _next === void 0 ? void 0 : _next[key]) !== null && _next$key !== void 0 ? _next$key : '';
295
- var nextMessage = field.validationMessage;
296
- /**
297
- * Techincally, checking prevMessage not being empty while nextMessage being empty
298
- * is sufficient for our usecase. It checks if the message is changed instead to allow
299
- * the hook to be useful independently.
300
- */
301
-
302
- if (prevMessage !== '' && prevMessage !== nextMessage) {
303
- next = _objectSpread2(_objectSpread2({}, next), {}, {
304
- [key]: nextMessage
305
- });
306
- }
307
- }
308
- }
309
- }
310
-
311
- return next;
312
- });
313
- };
314
-
315
- var handleInput = event => {
316
- var form = getFormElement(ref.current);
317
- var field = event.target;
318
-
319
- if (!form || !isFieldElement(field) || field.form !== form) {
320
- return;
321
- }
322
-
323
- resetError(form);
324
- };
325
-
326
299
  var invalidHandler = event => {
327
- var _configRef$current$na2, _configRef$current2;
300
+ var _configRef$current$na, _configRef$current;
328
301
 
329
302
  var form = getFormElement(ref.current);
330
303
  var field = event.target;
331
- var fieldsetName = (_configRef$current$na2 = (_configRef$current2 = configRef.current) === null || _configRef$current2 === void 0 ? void 0 : _configRef$current2.name) !== null && _configRef$current$na2 !== void 0 ? _configRef$current$na2 : '';
304
+ var fieldsetName = (_configRef$current$na = (_configRef$current = configRef.current) === null || _configRef$current === void 0 ? void 0 : _configRef$current.name) !== null && _configRef$current$na !== void 0 ? _configRef$current$na : '';
332
305
 
333
306
  if (!form || !isFieldElement(field) || field.form !== form || !field.name.startsWith(fieldsetName)) {
334
307
  return;
@@ -362,10 +335,39 @@ function useFieldset(ref, config) {
362
335
 
363
336
  if (!form || event.target !== form) {
364
337
  return;
365
- } // This helps resetting error that fullfilled by the submitter
338
+ }
339
+ /**
340
+ * Reset the error state of each field if its validity is changed.
341
+ *
342
+ * This is a workaround as no official way is provided to notify
343
+ * when the validity of the field is changed from `invalid` to `valid`.
344
+ */
345
+
366
346
 
347
+ setError(prev => {
348
+ var _configRef$current$na2, _configRef$current2;
349
+
350
+ var next = prev;
351
+ var fieldsetName = (_configRef$current$na2 = (_configRef$current2 = configRef.current) === null || _configRef$current2 === void 0 ? void 0 : _configRef$current2.name) !== null && _configRef$current$na2 !== void 0 ? _configRef$current$na2 : '';
352
+
353
+ for (var field of form.elements) {
354
+ if (isFieldElement(field) && field.name.startsWith(fieldsetName)) {
355
+ var _next$key, _next;
356
+
357
+ var key = fieldsetName ? field.name.slice(fieldsetName.length + 1) : field.name;
358
+ var prevMessage = (_next$key = (_next = next) === null || _next === void 0 ? void 0 : _next[key]) !== null && _next$key !== void 0 ? _next$key : '';
359
+ var nextMessage = field.validationMessage;
367
360
 
368
- resetError(form);
361
+ if (prevMessage !== '' && nextMessage === '') {
362
+ next = _objectSpread2(_objectSpread2({}, next), {}, {
363
+ [key]: ''
364
+ });
365
+ }
366
+ }
367
+ }
368
+
369
+ return next;
370
+ });
369
371
  };
370
372
 
371
373
  var resetHandler = event => {
@@ -384,15 +386,13 @@ function useFieldset(ref, config) {
384
386
  initialError: {}
385
387
  });
386
388
  setError({});
387
- };
389
+ }; // The invalid event does not bubble and so listening on the capturing pharse is needed
388
390
 
389
- document.addEventListener('input', handleInput); // The invalid event does not bubble and so listening on the capturing pharse is needed
390
391
 
391
392
  document.addEventListener('invalid', invalidHandler, true);
392
393
  document.addEventListener('submit', submitHandler);
393
394
  document.addEventListener('reset', resetHandler);
394
395
  return () => {
395
- document.removeEventListener('input', handleInput);
396
396
  document.removeEventListener('invalid', invalidHandler, true);
397
397
  document.removeEventListener('submit', submitHandler);
398
398
  document.removeEventListener('reset', resetHandler);
@@ -433,7 +433,7 @@ function useFieldset(ref, config) {
433
433
  * Returns a list of key and config, with a group of helpers
434
434
  * configuring buttons for list manipulation
435
435
  *
436
- * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.0/packages/conform-react/README.md#usefieldlist
436
+ * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.2/packages/conform-react/README.md#usefieldlist
437
437
  */
438
438
  function useFieldList(ref, config) {
439
439
  var configRef = useRef(config);
@@ -577,7 +577,7 @@ function useFieldList(ref, config) {
577
577
  * This is particular useful when integrating dropdown and datepicker whichs
578
578
  * introduces custom input mode.
579
579
  *
580
- * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.0/packages/conform-react/README.md#usecontrolledinput
580
+ * @see https://github.com/edmundhung/conform/tree/v0.4.0-pre.2/packages/conform-react/README.md#usecontrolledinput
581
581
  */
582
582
  function useControlledInput(config) {
583
583
  var _config$defaultValue4;
package/module/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { hasError, isFieldElement, parse, reportValidity } from '@conform-to/dom';
1
+ export { getFormError, hasError, isFieldElement, parse, shouldValidate } from '@conform-to/dom';
2
2
  export { useControlledInput, useFieldList, useFieldset, useForm } from './hooks.js';
3
3
  import * as helpers from './helpers.js';
4
4
  export { helpers as conform };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@conform-to/react",
3
3
  "description": "Conform view adapter for react",
4
4
  "license": "MIT",
5
- "version": "0.4.0-pre.0",
5
+ "version": "0.4.0-pre.2",
6
6
  "main": "index.js",
7
7
  "module": "module/index.js",
8
8
  "repository": {
@@ -19,7 +19,7 @@
19
19
  "url": "https://github.com/edmundhung/conform/issues"
20
20
  },
21
21
  "dependencies": {
22
- "@conform-to/dom": "0.4.0-pre.0"
22
+ "@conform-to/dom": "0.4.0-pre.2"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "react": ">=16.8"