@conform-to/react 0.7.3 → 0.8.0-pre.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 CHANGED
@@ -156,7 +156,7 @@ function LoginForm() {
156
156
 
157
157
  ### useFieldset
158
158
 
159
- This hook enables you to work with [nested object](/docs/configuration.md#nested-object) by monitoring the state of each nested field and prepraing the config required.
159
+ This hook enables you to work with [nested object](/docs/complex-structures.md#nested-object) by monitoring the state of each nested field and prepraing the config required.
160
160
 
161
161
  ```tsx
162
162
  import { useForm, useFieldset } from '@conform-to/react';
@@ -238,7 +238,7 @@ function ExampleForm() {
238
238
 
239
239
  ### useFieldList
240
240
 
241
- This hook enables you to work with [array](/docs/configuration.md#array) and support the [list](#list) intent button builder to modify a list. It can also be used with [useFieldset](#usefieldset) for [nested list](/docs/configuration.md#nested-list) at the same time.
241
+ This hook enables you to work with [array](/docs/complex-structures.md#array) and support the [list](#list) intent button builder to modify a list. It can also be used with [useFieldset](#usefieldset) for [nested list](/docs/complex-structures.md#nested-list) at the same time.
242
242
 
243
243
  ```tsx
244
244
  import { useForm, useFieldList, list } from '@conform-to/react';
@@ -373,7 +373,6 @@ function Example() {
373
373
  <input
374
374
  {...conform.input(title, {
375
375
  type: 'text',
376
- ariaAttributes: true,
377
376
  })}
378
377
  />
379
378
  </form>
@@ -385,7 +384,7 @@ function Example() {
385
384
 
386
385
  ### parse
387
386
 
388
- It parses the formData based on the [naming convention](/docs/configuration.md#naming-convention) with the validation result from the resolver.
387
+ It parses the formData based on the [naming convention](/docs/complex-structures.md#naming-convention) with the validation result from the resolver.
389
388
 
390
389
  ```tsx
391
390
  import { parse } from '@conform-to/react';
@@ -393,16 +392,16 @@ import { parse } from '@conform-to/react';
393
392
  const formData = new FormData();
394
393
  const submission = parse(formData, {
395
394
  resolve({ email, password }) {
396
- const error: Record<string, string> = {};
395
+ const error: Record<string, string[]> = {};
397
396
 
398
397
  if (typeof email !== 'string') {
399
- error.email = 'Email is required';
398
+ error.email = ['Email is required'];
400
399
  } else if (!/^[^@]+@[^@]+$/.test(email)) {
401
- error.email = 'Email is invalid';
400
+ error.email = ['Email is invalid'];
402
401
  }
403
402
 
404
403
  if (typeof password !== 'string') {
405
- error.password = 'Password is required';
404
+ error.password = ['Password is required'];
406
405
  }
407
406
 
408
407
  if (error.email || error.password) {
package/helpers.d.ts CHANGED
@@ -1,16 +1,18 @@
1
1
  import { INTENT, VALIDATION_UNDEFINED, VALIDATION_SKIPPED } from '@conform-to/dom';
2
2
  import type { FieldConfig, Primitive } from './hooks.js';
3
3
  import type { CSSProperties, HTMLInputTypeAttribute } from 'react';
4
- interface FormControlProps {
4
+ interface FormElementProps {
5
5
  id?: string;
6
6
  name: string;
7
7
  form?: string;
8
+ 'aria-describedby'?: string;
9
+ 'aria-invalid'?: boolean;
10
+ }
11
+ interface FormControlProps extends FormElementProps {
8
12
  required?: boolean;
9
13
  autoFocus?: boolean;
10
14
  tabIndex?: number;
11
15
  style?: CSSProperties;
12
- 'aria-describedby'?: string;
13
- 'aria-invalid'?: boolean;
14
16
  'aria-hidden'?: boolean;
15
17
  }
16
18
  interface InputProps<Schema> extends FormControlProps {
@@ -36,14 +38,15 @@ interface TextareaProps extends FormControlProps {
36
38
  defaultValue?: string;
37
39
  }
38
40
  type BaseOptions = {
39
- ariaAttributes?: false;
40
- hidden?: boolean;
41
- } | {
42
- ariaAttributes: true;
41
+ ariaAttributes?: true;
43
42
  description?: boolean;
43
+ } | {
44
+ ariaAttributes: false;
45
+ };
46
+ type ControlOptions = BaseOptions & {
44
47
  hidden?: boolean;
45
48
  };
46
- type InputOptions = BaseOptions & ({
49
+ type InputOptions = ControlOptions & ({
47
50
  type: 'checkbox' | 'radio';
48
51
  value?: string;
49
52
  } | {
@@ -59,6 +62,7 @@ export declare function input<Schema extends Primitive | unknown>(config: FieldC
59
62
  export declare function input<Schema extends File | File[]>(config: FieldConfig<Schema>, options: InputOptions & {
60
63
  type: 'file';
61
64
  }): InputProps<Schema>;
62
- export declare function select<Schema extends Primitive | Primitive[] | undefined | unknown>(config: FieldConfig<Schema>, options?: BaseOptions): SelectProps;
63
- export declare function textarea<Schema extends Primitive | undefined | unknown>(config: FieldConfig<Schema>, options?: BaseOptions): TextareaProps;
65
+ export declare function select<Schema extends Primitive | Primitive[] | undefined | unknown>(config: FieldConfig<Schema>, options?: ControlOptions): SelectProps;
66
+ export declare function textarea<Schema extends Primitive | undefined | unknown>(config: FieldConfig<Schema>, options?: ControlOptions): TextareaProps;
67
+ export declare function fieldset<Schema extends Record<string, unknown> | undefined | unknown>(config: FieldConfig<Schema>, options?: BaseOptions): FormControlProps;
64
68
  export { INTENT, VALIDATION_UNDEFINED, VALIDATION_SKIPPED };
package/helpers.js CHANGED
@@ -5,26 +5,10 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var _rollupPluginBabelHelpers = require('./_virtual/_rollupPluginBabelHelpers.js');
6
6
  var dom = require('@conform-to/dom');
7
7
 
8
- function getFormControlProps(config, options) {
9
- var props = {
10
- id: config.id,
11
- name: config.name,
12
- form: config.form,
13
- required: config.required,
14
- autoFocus: config.initialError && Object.entries(config.initialError).length > 0 ? true : undefined
15
- };
16
- if (options !== null && options !== void 0 && options.ariaAttributes) {
17
- var _config$error;
18
- if (config.descriptionId && options !== null && options !== void 0 && options.description) {
19
- props['aria-describedby'] = config.descriptionId;
20
- }
21
- if (config.errorId && (_config$error = config.error) !== null && _config$error !== void 0 && _config$error.length) {
22
- props['aria-invalid'] = true;
23
- props['aria-describedby'] = config.descriptionId && options !== null && options !== void 0 && options.description ? "".concat(config.errorId, " ").concat(config.descriptionId) : config.errorId;
24
- }
25
- }
26
- return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, props), options !== null && options !== void 0 && options.hidden ? hiddenProps : {});
27
- }
8
+ /**
9
+ * Cleanup `undefined` from the dervied props
10
+ * To minimize conflicts when merging with user defined props
11
+ */
28
12
  function cleanup(props) {
29
13
  for (var key in props) {
30
14
  if (props[key] === undefined) {
@@ -33,6 +17,32 @@ function cleanup(props) {
33
17
  }
34
18
  return props;
35
19
  }
20
+ function getFormElementProps(config) {
21
+ var _options$ariaAttribut, _config$error, _config$error2;
22
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
23
+ var hasAriaAttributes = (_options$ariaAttribut = options.ariaAttributes) !== null && _options$ariaAttribut !== void 0 ? _options$ariaAttribut : true;
24
+ return cleanup({
25
+ id: config.id,
26
+ name: config.name,
27
+ form: config.form,
28
+ 'aria-invalid': hasAriaAttributes && config.errorId && (_config$error = config.error) !== null && _config$error !== void 0 && _config$error.length ? true : undefined,
29
+ 'aria-describedby': hasAriaAttributes ? [config.errorId && (_config$error2 = config.error) !== null && _config$error2 !== void 0 && _config$error2.length ? config.errorId : undefined, config.descriptionId && options.ariaAttributes !== false && options.description ? config.descriptionId : undefined].reduce((result, id) => {
30
+ if (!result) {
31
+ return id;
32
+ }
33
+ if (!id) {
34
+ return result;
35
+ }
36
+ return "".concat(result, " ").concat(id);
37
+ }) : undefined
38
+ });
39
+ }
40
+ function getFormControlProps(config, options) {
41
+ return cleanup(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormElementProps(config, options)), {}, {
42
+ required: config.required,
43
+ autoFocus: config.initialError && Object.entries(config.initialError).length > 0 ? true : undefined
44
+ }, options !== null && options !== void 0 && options.hidden ? hiddenProps : undefined));
45
+ }
36
46
  var hiddenProps = {
37
47
  /**
38
48
  * Style to make the input element visually hidden
@@ -74,19 +84,20 @@ function input(config) {
74
84
  return cleanup(props);
75
85
  }
76
86
  function select(config, options) {
77
- var props = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(config, options)), {}, {
87
+ return cleanup(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(config, options)), {}, {
78
88
  defaultValue: config.defaultValue,
79
89
  multiple: config.multiple
80
- });
81
- return cleanup(props);
90
+ }));
82
91
  }
83
92
  function textarea(config, options) {
84
- var props = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(config, options)), {}, {
93
+ return cleanup(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(config, options)), {}, {
85
94
  defaultValue: config.defaultValue,
86
95
  minLength: config.minLength,
87
96
  maxLength: config.maxLength
88
- });
89
- return cleanup(props);
97
+ }));
98
+ }
99
+ function fieldset(config, options) {
100
+ return getFormElementProps(config, options);
90
101
  }
91
102
 
92
103
  Object.defineProperty(exports, 'INTENT', {
@@ -101,6 +112,7 @@ Object.defineProperty(exports, 'VALIDATION_UNDEFINED', {
101
112
  enumerable: true,
102
113
  get: function () { return dom.VALIDATION_UNDEFINED; }
103
114
  });
115
+ exports.fieldset = fieldset;
104
116
  exports.hiddenProps = hiddenProps;
105
117
  exports.input = input;
106
118
  exports.select = select;
package/helpers.mjs CHANGED
@@ -1,26 +1,10 @@
1
1
  import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.mjs';
2
2
  export { INTENT, VALIDATION_SKIPPED, VALIDATION_UNDEFINED } from '@conform-to/dom';
3
3
 
4
- function getFormControlProps(config, options) {
5
- var props = {
6
- id: config.id,
7
- name: config.name,
8
- form: config.form,
9
- required: config.required,
10
- autoFocus: config.initialError && Object.entries(config.initialError).length > 0 ? true : undefined
11
- };
12
- if (options !== null && options !== void 0 && options.ariaAttributes) {
13
- var _config$error;
14
- if (config.descriptionId && options !== null && options !== void 0 && options.description) {
15
- props['aria-describedby'] = config.descriptionId;
16
- }
17
- if (config.errorId && (_config$error = config.error) !== null && _config$error !== void 0 && _config$error.length) {
18
- props['aria-invalid'] = true;
19
- props['aria-describedby'] = config.descriptionId && options !== null && options !== void 0 && options.description ? "".concat(config.errorId, " ").concat(config.descriptionId) : config.errorId;
20
- }
21
- }
22
- return _objectSpread2(_objectSpread2({}, props), options !== null && options !== void 0 && options.hidden ? hiddenProps : {});
23
- }
4
+ /**
5
+ * Cleanup `undefined` from the dervied props
6
+ * To minimize conflicts when merging with user defined props
7
+ */
24
8
  function cleanup(props) {
25
9
  for (var key in props) {
26
10
  if (props[key] === undefined) {
@@ -29,6 +13,32 @@ function cleanup(props) {
29
13
  }
30
14
  return props;
31
15
  }
16
+ function getFormElementProps(config) {
17
+ var _options$ariaAttribut, _config$error, _config$error2;
18
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
19
+ var hasAriaAttributes = (_options$ariaAttribut = options.ariaAttributes) !== null && _options$ariaAttribut !== void 0 ? _options$ariaAttribut : true;
20
+ return cleanup({
21
+ id: config.id,
22
+ name: config.name,
23
+ form: config.form,
24
+ 'aria-invalid': hasAriaAttributes && config.errorId && (_config$error = config.error) !== null && _config$error !== void 0 && _config$error.length ? true : undefined,
25
+ 'aria-describedby': hasAriaAttributes ? [config.errorId && (_config$error2 = config.error) !== null && _config$error2 !== void 0 && _config$error2.length ? config.errorId : undefined, config.descriptionId && options.ariaAttributes !== false && options.description ? config.descriptionId : undefined].reduce((result, id) => {
26
+ if (!result) {
27
+ return id;
28
+ }
29
+ if (!id) {
30
+ return result;
31
+ }
32
+ return "".concat(result, " ").concat(id);
33
+ }) : undefined
34
+ });
35
+ }
36
+ function getFormControlProps(config, options) {
37
+ return cleanup(_objectSpread2(_objectSpread2({}, getFormElementProps(config, options)), {}, {
38
+ required: config.required,
39
+ autoFocus: config.initialError && Object.entries(config.initialError).length > 0 ? true : undefined
40
+ }, options !== null && options !== void 0 && options.hidden ? hiddenProps : undefined));
41
+ }
32
42
  var hiddenProps = {
33
43
  /**
34
44
  * Style to make the input element visually hidden
@@ -70,19 +80,20 @@ function input(config) {
70
80
  return cleanup(props);
71
81
  }
72
82
  function select(config, options) {
73
- var props = _objectSpread2(_objectSpread2({}, getFormControlProps(config, options)), {}, {
83
+ return cleanup(_objectSpread2(_objectSpread2({}, getFormControlProps(config, options)), {}, {
74
84
  defaultValue: config.defaultValue,
75
85
  multiple: config.multiple
76
- });
77
- return cleanup(props);
86
+ }));
78
87
  }
79
88
  function textarea(config, options) {
80
- var props = _objectSpread2(_objectSpread2({}, getFormControlProps(config, options)), {}, {
89
+ return cleanup(_objectSpread2(_objectSpread2({}, getFormControlProps(config, options)), {}, {
81
90
  defaultValue: config.defaultValue,
82
91
  minLength: config.minLength,
83
92
  maxLength: config.maxLength
84
- });
85
- return cleanup(props);
93
+ }));
94
+ }
95
+ function fieldset(config, options) {
96
+ return getFormElementProps(config, options);
86
97
  }
87
98
 
88
- export { hiddenProps, input, select, textarea };
99
+ export { fieldset, hiddenProps, input, select, textarea };
package/hooks.d.ts CHANGED
@@ -5,7 +5,7 @@ export interface FieldConfig<Schema> extends FieldConstraint<Schema> {
5
5
  id?: string;
6
6
  name: string;
7
7
  defaultValue?: FieldValue<Schema>;
8
- initialError?: Record<string, string | string[]>;
8
+ initialError?: Record<string, string[]>;
9
9
  form?: string;
10
10
  descriptionId?: string;
11
11
  errorId?: string;
@@ -26,6 +26,11 @@ type SubmissionResult = {
26
26
  payload: Submission['payload'] | null;
27
27
  error: Submission['error'];
28
28
  };
29
+ interface ReportOptions {
30
+ formError?: string[];
31
+ resetForm?: boolean;
32
+ }
33
+ export declare function report(submission: Submission, options?: ReportOptions): SubmissionResult;
29
34
  export interface FormConfig<Output extends Record<string, any>, Input extends Record<string, any> = Output> {
30
35
  /**
31
36
  * If the form id is provided, Id for label,
@@ -107,7 +112,7 @@ interface FormProps {
107
112
  interface Form {
108
113
  id?: string;
109
114
  errorId?: string;
110
- error: string;
115
+ error: string | undefined;
111
116
  errors: string[];
112
117
  ref: RefObject<HTMLFormElement>;
113
118
  props: FormProps;
@@ -137,7 +142,7 @@ export interface FieldsetConfig<Schema extends Record<string, any> | undefined>
137
142
  /**
138
143
  * An object describing the initial error of each field
139
144
  */
140
- initialError?: Record<string, string | string[]>;
145
+ initialError?: Record<string, string[]>;
141
146
  /**
142
147
  * An object describing the constraint of each field
143
148
  */
@@ -171,7 +176,6 @@ interface InputControl {
171
176
  focus: () => void;
172
177
  blur: () => void;
173
178
  }
174
- export declare function useEventListeners(type: string, ref: RefObject<FieldElement>): void;
175
179
  /**
176
180
  * Returns a ref object and a set of helpers that dispatch corresponding dom event.
177
181
  *
@@ -196,11 +200,6 @@ export declare function validateConstraint(options: {
196
200
  formData: FormData;
197
201
  attributeValue: string;
198
202
  }) => boolean>;
199
- acceptMultipleErrors?: ({ name, intent, payload, }: {
200
- name: string;
201
- intent: string;
202
- payload: Record<string, any>;
203
- }) => boolean;
204
203
  formatMessages?: ({ name, validity, constraint, defaultErrors, }: {
205
204
  name: string;
206
205
  validity: ValidityState;
@@ -208,6 +207,7 @@ export declare function validateConstraint(options: {
208
207
  defaultErrors: string[];
209
208
  }) => string[];
210
209
  }): Submission;
210
+ export declare function getUniqueKey(): string;
211
211
  export declare function reportSubmission(form: HTMLFormElement, submission: SubmissionResult): void;
212
212
  export declare function getScope(intent: ReturnType<typeof parseIntent>): string | null;
213
213
  export {};
package/hooks.js CHANGED
@@ -6,16 +6,21 @@ var _rollupPluginBabelHelpers = require('./_virtual/_rollupPluginBabelHelpers.js
6
6
  var dom = require('@conform-to/dom');
7
7
  var react = require('react');
8
8
 
9
+ function report(submission, options) {
10
+ var _submission$error$;
11
+ return {
12
+ intent: submission.intent,
13
+ payload: options !== null && options !== void 0 && options.resetForm ? null : submission.payload,
14
+ error: options !== null && options !== void 0 && options.formError ? _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, submission.error), {}, {
15
+ '': options.formError.concat((_submission$error$ = submission.error['']) !== null && _submission$error$ !== void 0 ? _submission$error$ : [])
16
+ }) : submission.error
17
+ };
18
+ }
19
+
9
20
  /**
10
- * Normalize error to an array of string.
21
+ * Properties to be applied to the form element
11
22
  */
12
- function normalizeError(error) {
13
- if (!error) {
14
- // This treat both empty string and undefined as no error.
15
- return [];
16
- }
17
- return [].concat(error);
18
- }
23
+
19
24
  function useNoValidate(defaultNoValidate, validateBeforeHydrate) {
20
25
  var [noValidate, setNoValidate] = react.useState(defaultNoValidate || !validateBeforeHydrate);
21
26
  react.useEffect(() => {
@@ -74,42 +79,33 @@ function useFormError(ref, config) {
74
79
  }
75
80
  var result = {};
76
81
  for (var [name, message] of Object.entries(config.initialError)) {
77
- var paths = dom.getPaths(name);
78
- if (paths.length === 1) {
79
- result[paths[0]] = normalizeError(message);
82
+ var [path, ...restPaths] = dom.getPaths(name);
83
+ if (typeof path !== 'undefined' && restPaths.length === 0) {
84
+ result[path] = message;
80
85
  }
81
86
  }
82
87
  return result;
83
88
  });
84
89
  react.useEffect(() => {
85
90
  var handleInvalid = event => {
91
+ var _config$name;
86
92
  var form = dom.getFormElement(ref.current);
87
93
  var element = event.target;
88
- if (!dom.isFieldElement(element) || element.form !== form || !element.dataset.conformTouched) {
94
+ var prefix = (_config$name = config.name) !== null && _config$name !== void 0 ? _config$name : '';
95
+ if (!dom.isFieldElement(element) || element.form !== form || !element.name.startsWith(prefix) || !element.dataset.conformTouched) {
89
96
  return;
90
97
  }
91
- var key = element.name;
92
- if (config.name) {
93
- var scopePaths = dom.getPaths(config.name);
94
- var fieldPaths = dom.getPaths(element.name);
95
- for (var i = 0; i <= scopePaths.length; i++) {
96
- var path = fieldPaths[i];
97
- if (i < scopePaths.length) {
98
- // Skip if the field is not in the scope
99
- if (path !== scopePaths[i]) {
100
- return;
101
- }
102
- } else {
103
- key = path;
104
- }
105
- }
98
+ var name = element.name.slice(prefix.length);
99
+ var [path, ...restPaths] = dom.getPaths(name);
100
+ if (typeof path === 'undefined' || restPaths.length > 0) {
101
+ return;
106
102
  }
107
103
  setError(prev => {
108
- if (element.validationMessage === dom.getValidationMessage(prev[key])) {
104
+ if (element.validationMessage === dom.getValidationMessage(prev[path])) {
109
105
  return prev;
110
106
  }
111
107
  return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, prev), {}, {
112
- [key]: dom.getErrors(element.validationMessage)
108
+ [path]: dom.getErrors(element.validationMessage)
113
109
  });
114
110
  });
115
111
  event.preventDefault();
@@ -137,31 +133,35 @@ function useFormError(ref, config) {
137
133
  * @see https://conform.guide/api/react#useform
138
134
  */
139
135
  function useForm() {
140
- var _config$lastSubmissio2, _config$lastSubmissio3;
136
+ var _config$lastSubmissio3, _config$lastSubmissio4;
141
137
  var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
142
138
  var configRef = useConfigRef(config);
143
139
  var ref = useFormRef(config.ref);
144
140
  var noValidate = useNoValidate(config.noValidate, config.fallbackNative);
145
141
  var report = useFormReporter(ref, config.lastSubmission);
146
142
  var [errors, setErrors] = react.useState(() => {
147
- var _config$lastSubmissio;
148
- return normalizeError((_config$lastSubmissio = config.lastSubmission) === null || _config$lastSubmissio === void 0 ? void 0 : _config$lastSubmissio.error['']);
143
+ var _config$lastSubmissio, _config$lastSubmissio2;
144
+ return (_config$lastSubmissio = (_config$lastSubmissio2 = config.lastSubmission) === null || _config$lastSubmissio2 === void 0 ? void 0 : _config$lastSubmissio2.error['']) !== null && _config$lastSubmissio !== void 0 ? _config$lastSubmissio : [];
149
145
  });
150
146
  var initialError = react.useMemo(() => {
147
+ var _submission$error$sco;
151
148
  var submission = config.lastSubmission;
152
149
  if (!submission) {
153
150
  return {};
154
151
  }
155
152
  var intent = dom.parseIntent(submission.intent);
156
153
  var scope = getScope(intent);
157
- return scope === null ? submission.error : {
158
- [scope]: submission.error[scope]
154
+ if (typeof scope !== 'string') {
155
+ return submission.error;
156
+ }
157
+ return {
158
+ [scope]: (_submission$error$sco = submission.error[scope]) !== null && _submission$error$sco !== void 0 ? _submission$error$sco : []
159
159
  };
160
160
  }, [config.lastSubmission]);
161
161
  // This payload from lastSubmission is only useful before hydration
162
162
  // After hydration, any new payload on lastSubmission will be ignored
163
163
  var [defaultValueFromLastSubmission, setDefaultValueFromLastSubmission] = react.useState( // @ts-expect-error defaultValue is not in Submission type
164
- (_config$lastSubmissio2 = (_config$lastSubmissio3 = config.lastSubmission) === null || _config$lastSubmissio3 === void 0 ? void 0 : _config$lastSubmissio3.payload) !== null && _config$lastSubmissio2 !== void 0 ? _config$lastSubmissio2 : null);
164
+ (_config$lastSubmissio3 = (_config$lastSubmissio4 = config.lastSubmission) === null || _config$lastSubmissio4 === void 0 ? void 0 : _config$lastSubmissio4.payload) !== null && _config$lastSubmissio3 !== void 0 ? _config$lastSubmissio3 : null);
165
165
  var fieldset = useFieldset(ref, {
166
166
  defaultValue: defaultValueFromLastSubmission !== null && defaultValueFromLastSubmission !== void 0 ? defaultValueFromLastSubmission : config.defaultValue,
167
167
  initialError,
@@ -248,7 +248,7 @@ function useForm() {
248
248
  shouldServerValidate
249
249
  } = Object.entries(submission.error).reduce((result, _ref) => {
250
250
  var [, error] = _ref;
251
- for (var message of normalizeError(error)) {
251
+ for (var message of error) {
252
252
  if (message === dom.VALIDATION_UNDEFINED) {
253
253
  result.shouldServerValidate = true;
254
254
  } else if (message !== dom.VALIDATION_SKIPPED) {
@@ -302,6 +302,12 @@ function useForm() {
302
302
  * A set of field configuration
303
303
  */
304
304
 
305
+ /**
306
+ * Returns all the information about the fieldset.
307
+ *
308
+ * @see https://conform.guide/api/react#usefieldset
309
+ */
310
+
305
311
  function useFieldset(ref, config) {
306
312
  var [error] = useFormError(ref, {
307
313
  initialError: config.initialError,
@@ -383,7 +389,7 @@ function useFieldList(ref, config) {
383
389
  return dom.updateList(list, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, intent.payload), {}, {
384
390
  defaultValue: [
385
391
  // Generate a random key to avoid conflicts
386
- crypto.getRandomValues(new Uint32Array(1))[0].toString(36), intent.payload.defaultValue]
392
+ getUniqueKey(), intent.payload.defaultValue]
387
393
  }));
388
394
  default:
389
395
  return dom.updateList(list, intent.payload);
@@ -465,7 +471,6 @@ function useFieldList(ref, config) {
465
471
  * This basically makes it a no-op on server
466
472
  */
467
473
  var useSafeLayoutEffect = typeof document === 'undefined' ? react.useEffect : react.useLayoutEffect;
468
-
469
474
  /**
470
475
  * Returns a ref object and a set of helpers that dispatch corresponding dom event.
471
476
  *
@@ -488,7 +493,7 @@ function useInputEvent(options) {
488
493
  if (listener !== 'onReset') {
489
494
  eventDispatched.current[listener] = true;
490
495
  }
491
- (_optionsRef$current4 = optionsRef.current) === null || _optionsRef$current4 === void 0 ? void 0 : (_optionsRef$current4$ = _optionsRef$current4[listener]) === null || _optionsRef$current4$ === void 0 ? void 0 : _optionsRef$current4$.call(_optionsRef$current4, event);
496
+ (_optionsRef$current4 = optionsRef.current) === null || _optionsRef$current4 === void 0 || (_optionsRef$current4$ = _optionsRef$current4[listener]) === null || _optionsRef$current4$ === void 0 ? void 0 : _optionsRef$current4$.call(_optionsRef$current4, event);
492
497
  }
493
498
  };
494
499
  };
@@ -631,12 +636,11 @@ function validateConstraint(options) {
631
636
  return defaultErrors;
632
637
  };
633
638
  return dom.parse(formData, {
634
- resolve(payload, intent) {
639
+ resolve() {
635
640
  var error = {};
636
641
  var constraintPattern = /^constraint[A-Z][^A-Z]*$/;
637
642
  var _loop = function _loop(_element3) {
638
643
  if (dom.isFieldElement(_element3)) {
639
- var _options$acceptMultip, _options$acceptMultip2;
640
644
  var name = _element3.name !== FORM_ERROR_ELEMENT_NAME ? _element3.name : '';
641
645
  var constraint = Object.entries(_element3.dataset).reduce((result, _ref6) => {
642
646
  var [name, attributeValue = ''] = _ref6;
@@ -662,13 +666,8 @@ function validateConstraint(options) {
662
666
  constraint,
663
667
  defaultErrors: getDefaultErrors(_element3.validity, constraint)
664
668
  });
665
- var shouldAcceptMultipleErrors = (_options$acceptMultip = options === null || options === void 0 ? void 0 : (_options$acceptMultip2 = options.acceptMultipleErrors) === null || _options$acceptMultip2 === void 0 ? void 0 : _options$acceptMultip2.call(options, {
666
- name,
667
- payload,
668
- intent
669
- })) !== null && _options$acceptMultip !== void 0 ? _options$acceptMultip : false;
670
669
  if (errors.length > 0) {
671
- error[name] = shouldAcceptMultipleErrors ? errors : errors[0];
670
+ error[name] = errors;
672
671
  }
673
672
  }
674
673
  };
@@ -681,10 +680,17 @@ function validateConstraint(options) {
681
680
  }
682
681
  });
683
682
  }
683
+ function getUniqueKey() {
684
+ var [value] = crypto.getRandomValues(new Uint32Array(1));
685
+ if (!value) {
686
+ throw new Error('Fail to generate an unique key');
687
+ }
688
+ return value.toString(36);
689
+ }
684
690
  function reportSubmission(form, submission) {
685
691
  for (var [name, message] of Object.entries(submission.error)) {
686
692
  // There is no need to create a placeholder button if all we want is to reset the error
687
- if (message === '') {
693
+ if (message.length === 0) {
688
694
  continue;
689
695
  }
690
696
 
@@ -713,8 +719,9 @@ function reportSubmission(form, submission) {
713
719
  var intent = dom.parseIntent(submission.intent);
714
720
  var scope = getScope(intent);
715
721
  for (var _element4 of dom.getFormControls(form)) {
722
+ var _submission$error$_el;
716
723
  var _elementName = _element4.name !== FORM_ERROR_ELEMENT_NAME ? _element4.name : '';
717
- var messages = normalizeError(submission.error[_elementName]);
724
+ var messages = (_submission$error$_el = submission.error[_elementName]) !== null && _submission$error$_el !== void 0 ? _submission$error$_el : [];
718
725
  if (scope === null || scope === _elementName) {
719
726
  _element4.dataset.conformTouched = 'true';
720
727
  }
@@ -742,6 +749,8 @@ function getScope(intent) {
742
749
 
743
750
  exports.FORM_ERROR_ELEMENT_NAME = FORM_ERROR_ELEMENT_NAME;
744
751
  exports.getScope = getScope;
752
+ exports.getUniqueKey = getUniqueKey;
753
+ exports.report = report;
745
754
  exports.reportSubmission = reportSubmission;
746
755
  exports.useFieldList = useFieldList;
747
756
  exports.useFieldset = useFieldset;
package/hooks.mjs CHANGED
@@ -2,16 +2,21 @@ import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHe
2
2
  import { parseIntent, getFormData, parse, VALIDATION_UNDEFINED, VALIDATION_SKIPPED, getFormAction, getFormEncType, getFormMethod, getPaths, getName, isFieldElement, getErrors, getFormControls, getFormElement, updateList, getValidationMessage, focusFirstInvalidControl, isFocusableFormControl, requestIntent, validate } from '@conform-to/dom';
3
3
  import { useState, useMemo, useEffect, useRef, useCallback, useLayoutEffect } from 'react';
4
4
 
5
+ function report(submission, options) {
6
+ var _submission$error$;
7
+ return {
8
+ intent: submission.intent,
9
+ payload: options !== null && options !== void 0 && options.resetForm ? null : submission.payload,
10
+ error: options !== null && options !== void 0 && options.formError ? _objectSpread2(_objectSpread2({}, submission.error), {}, {
11
+ '': options.formError.concat((_submission$error$ = submission.error['']) !== null && _submission$error$ !== void 0 ? _submission$error$ : [])
12
+ }) : submission.error
13
+ };
14
+ }
15
+
5
16
  /**
6
- * Normalize error to an array of string.
17
+ * Properties to be applied to the form element
7
18
  */
8
- function normalizeError(error) {
9
- if (!error) {
10
- // This treat both empty string and undefined as no error.
11
- return [];
12
- }
13
- return [].concat(error);
14
- }
19
+
15
20
  function useNoValidate(defaultNoValidate, validateBeforeHydrate) {
16
21
  var [noValidate, setNoValidate] = useState(defaultNoValidate || !validateBeforeHydrate);
17
22
  useEffect(() => {
@@ -70,42 +75,33 @@ function useFormError(ref, config) {
70
75
  }
71
76
  var result = {};
72
77
  for (var [name, message] of Object.entries(config.initialError)) {
73
- var paths = getPaths(name);
74
- if (paths.length === 1) {
75
- result[paths[0]] = normalizeError(message);
78
+ var [path, ...restPaths] = getPaths(name);
79
+ if (typeof path !== 'undefined' && restPaths.length === 0) {
80
+ result[path] = message;
76
81
  }
77
82
  }
78
83
  return result;
79
84
  });
80
85
  useEffect(() => {
81
86
  var handleInvalid = event => {
87
+ var _config$name;
82
88
  var form = getFormElement(ref.current);
83
89
  var element = event.target;
84
- if (!isFieldElement(element) || element.form !== form || !element.dataset.conformTouched) {
90
+ var prefix = (_config$name = config.name) !== null && _config$name !== void 0 ? _config$name : '';
91
+ if (!isFieldElement(element) || element.form !== form || !element.name.startsWith(prefix) || !element.dataset.conformTouched) {
85
92
  return;
86
93
  }
87
- var key = element.name;
88
- if (config.name) {
89
- var scopePaths = getPaths(config.name);
90
- var fieldPaths = getPaths(element.name);
91
- for (var i = 0; i <= scopePaths.length; i++) {
92
- var path = fieldPaths[i];
93
- if (i < scopePaths.length) {
94
- // Skip if the field is not in the scope
95
- if (path !== scopePaths[i]) {
96
- return;
97
- }
98
- } else {
99
- key = path;
100
- }
101
- }
94
+ var name = element.name.slice(prefix.length);
95
+ var [path, ...restPaths] = getPaths(name);
96
+ if (typeof path === 'undefined' || restPaths.length > 0) {
97
+ return;
102
98
  }
103
99
  setError(prev => {
104
- if (element.validationMessage === getValidationMessage(prev[key])) {
100
+ if (element.validationMessage === getValidationMessage(prev[path])) {
105
101
  return prev;
106
102
  }
107
103
  return _objectSpread2(_objectSpread2({}, prev), {}, {
108
- [key]: getErrors(element.validationMessage)
104
+ [path]: getErrors(element.validationMessage)
109
105
  });
110
106
  });
111
107
  event.preventDefault();
@@ -133,31 +129,35 @@ function useFormError(ref, config) {
133
129
  * @see https://conform.guide/api/react#useform
134
130
  */
135
131
  function useForm() {
136
- var _config$lastSubmissio2, _config$lastSubmissio3;
132
+ var _config$lastSubmissio3, _config$lastSubmissio4;
137
133
  var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
138
134
  var configRef = useConfigRef(config);
139
135
  var ref = useFormRef(config.ref);
140
136
  var noValidate = useNoValidate(config.noValidate, config.fallbackNative);
141
137
  var report = useFormReporter(ref, config.lastSubmission);
142
138
  var [errors, setErrors] = useState(() => {
143
- var _config$lastSubmissio;
144
- return normalizeError((_config$lastSubmissio = config.lastSubmission) === null || _config$lastSubmissio === void 0 ? void 0 : _config$lastSubmissio.error['']);
139
+ var _config$lastSubmissio, _config$lastSubmissio2;
140
+ return (_config$lastSubmissio = (_config$lastSubmissio2 = config.lastSubmission) === null || _config$lastSubmissio2 === void 0 ? void 0 : _config$lastSubmissio2.error['']) !== null && _config$lastSubmissio !== void 0 ? _config$lastSubmissio : [];
145
141
  });
146
142
  var initialError = useMemo(() => {
143
+ var _submission$error$sco;
147
144
  var submission = config.lastSubmission;
148
145
  if (!submission) {
149
146
  return {};
150
147
  }
151
148
  var intent = parseIntent(submission.intent);
152
149
  var scope = getScope(intent);
153
- return scope === null ? submission.error : {
154
- [scope]: submission.error[scope]
150
+ if (typeof scope !== 'string') {
151
+ return submission.error;
152
+ }
153
+ return {
154
+ [scope]: (_submission$error$sco = submission.error[scope]) !== null && _submission$error$sco !== void 0 ? _submission$error$sco : []
155
155
  };
156
156
  }, [config.lastSubmission]);
157
157
  // This payload from lastSubmission is only useful before hydration
158
158
  // After hydration, any new payload on lastSubmission will be ignored
159
159
  var [defaultValueFromLastSubmission, setDefaultValueFromLastSubmission] = useState( // @ts-expect-error defaultValue is not in Submission type
160
- (_config$lastSubmissio2 = (_config$lastSubmissio3 = config.lastSubmission) === null || _config$lastSubmissio3 === void 0 ? void 0 : _config$lastSubmissio3.payload) !== null && _config$lastSubmissio2 !== void 0 ? _config$lastSubmissio2 : null);
160
+ (_config$lastSubmissio3 = (_config$lastSubmissio4 = config.lastSubmission) === null || _config$lastSubmissio4 === void 0 ? void 0 : _config$lastSubmissio4.payload) !== null && _config$lastSubmissio3 !== void 0 ? _config$lastSubmissio3 : null);
161
161
  var fieldset = useFieldset(ref, {
162
162
  defaultValue: defaultValueFromLastSubmission !== null && defaultValueFromLastSubmission !== void 0 ? defaultValueFromLastSubmission : config.defaultValue,
163
163
  initialError,
@@ -244,7 +244,7 @@ function useForm() {
244
244
  shouldServerValidate
245
245
  } = Object.entries(submission.error).reduce((result, _ref) => {
246
246
  var [, error] = _ref;
247
- for (var message of normalizeError(error)) {
247
+ for (var message of error) {
248
248
  if (message === VALIDATION_UNDEFINED) {
249
249
  result.shouldServerValidate = true;
250
250
  } else if (message !== VALIDATION_SKIPPED) {
@@ -298,6 +298,12 @@ function useForm() {
298
298
  * A set of field configuration
299
299
  */
300
300
 
301
+ /**
302
+ * Returns all the information about the fieldset.
303
+ *
304
+ * @see https://conform.guide/api/react#usefieldset
305
+ */
306
+
301
307
  function useFieldset(ref, config) {
302
308
  var [error] = useFormError(ref, {
303
309
  initialError: config.initialError,
@@ -379,7 +385,7 @@ function useFieldList(ref, config) {
379
385
  return updateList(list, _objectSpread2(_objectSpread2({}, intent.payload), {}, {
380
386
  defaultValue: [
381
387
  // Generate a random key to avoid conflicts
382
- crypto.getRandomValues(new Uint32Array(1))[0].toString(36), intent.payload.defaultValue]
388
+ getUniqueKey(), intent.payload.defaultValue]
383
389
  }));
384
390
  default:
385
391
  return updateList(list, intent.payload);
@@ -461,7 +467,6 @@ function useFieldList(ref, config) {
461
467
  * This basically makes it a no-op on server
462
468
  */
463
469
  var useSafeLayoutEffect = typeof document === 'undefined' ? useEffect : useLayoutEffect;
464
-
465
470
  /**
466
471
  * Returns a ref object and a set of helpers that dispatch corresponding dom event.
467
472
  *
@@ -484,7 +489,7 @@ function useInputEvent(options) {
484
489
  if (listener !== 'onReset') {
485
490
  eventDispatched.current[listener] = true;
486
491
  }
487
- (_optionsRef$current4 = optionsRef.current) === null || _optionsRef$current4 === void 0 ? void 0 : (_optionsRef$current4$ = _optionsRef$current4[listener]) === null || _optionsRef$current4$ === void 0 ? void 0 : _optionsRef$current4$.call(_optionsRef$current4, event);
492
+ (_optionsRef$current4 = optionsRef.current) === null || _optionsRef$current4 === void 0 || (_optionsRef$current4$ = _optionsRef$current4[listener]) === null || _optionsRef$current4$ === void 0 ? void 0 : _optionsRef$current4$.call(_optionsRef$current4, event);
488
493
  }
489
494
  };
490
495
  };
@@ -627,12 +632,11 @@ function validateConstraint(options) {
627
632
  return defaultErrors;
628
633
  };
629
634
  return parse(formData, {
630
- resolve(payload, intent) {
635
+ resolve() {
631
636
  var error = {};
632
637
  var constraintPattern = /^constraint[A-Z][^A-Z]*$/;
633
638
  var _loop = function _loop(_element3) {
634
639
  if (isFieldElement(_element3)) {
635
- var _options$acceptMultip, _options$acceptMultip2;
636
640
  var name = _element3.name !== FORM_ERROR_ELEMENT_NAME ? _element3.name : '';
637
641
  var constraint = Object.entries(_element3.dataset).reduce((result, _ref6) => {
638
642
  var [name, attributeValue = ''] = _ref6;
@@ -658,13 +662,8 @@ function validateConstraint(options) {
658
662
  constraint,
659
663
  defaultErrors: getDefaultErrors(_element3.validity, constraint)
660
664
  });
661
- var shouldAcceptMultipleErrors = (_options$acceptMultip = options === null || options === void 0 ? void 0 : (_options$acceptMultip2 = options.acceptMultipleErrors) === null || _options$acceptMultip2 === void 0 ? void 0 : _options$acceptMultip2.call(options, {
662
- name,
663
- payload,
664
- intent
665
- })) !== null && _options$acceptMultip !== void 0 ? _options$acceptMultip : false;
666
665
  if (errors.length > 0) {
667
- error[name] = shouldAcceptMultipleErrors ? errors : errors[0];
666
+ error[name] = errors;
668
667
  }
669
668
  }
670
669
  };
@@ -677,10 +676,17 @@ function validateConstraint(options) {
677
676
  }
678
677
  });
679
678
  }
679
+ function getUniqueKey() {
680
+ var [value] = crypto.getRandomValues(new Uint32Array(1));
681
+ if (!value) {
682
+ throw new Error('Fail to generate an unique key');
683
+ }
684
+ return value.toString(36);
685
+ }
680
686
  function reportSubmission(form, submission) {
681
687
  for (var [name, message] of Object.entries(submission.error)) {
682
688
  // There is no need to create a placeholder button if all we want is to reset the error
683
- if (message === '') {
689
+ if (message.length === 0) {
684
690
  continue;
685
691
  }
686
692
 
@@ -709,8 +715,9 @@ function reportSubmission(form, submission) {
709
715
  var intent = parseIntent(submission.intent);
710
716
  var scope = getScope(intent);
711
717
  for (var _element4 of getFormControls(form)) {
718
+ var _submission$error$_el;
712
719
  var _elementName = _element4.name !== FORM_ERROR_ELEMENT_NAME ? _element4.name : '';
713
- var messages = normalizeError(submission.error[_elementName]);
720
+ var messages = (_submission$error$_el = submission.error[_elementName]) !== null && _submission$error$_el !== void 0 ? _submission$error$_el : [];
714
721
  if (scope === null || scope === _elementName) {
715
722
  _element4.dataset.conformTouched = 'true';
716
723
  }
@@ -736,4 +743,4 @@ function getScope(intent) {
736
743
  return null;
737
744
  }
738
745
 
739
- export { FORM_ERROR_ELEMENT_NAME, getScope, reportSubmission, useFieldList, useFieldset, useForm, useInputEvent, validateConstraint };
746
+ export { FORM_ERROR_ELEMENT_NAME, getScope, getUniqueKey, report, reportSubmission, useFieldList, useFieldset, useForm, useInputEvent, validateConstraint };
package/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { type FieldsetConstraint, type Submission, parse, list, validate, requestIntent, isFieldElement, } from '@conform-to/dom';
2
- export { type Fieldset, type FieldConfig, type FieldsetConfig, type FormConfig, useForm, useFieldset, useFieldList, useInputEvent, validateConstraint, } from './hooks.js';
2
+ export { type Fieldset, type FieldConfig, type FieldsetConfig, type FormConfig, useForm, useFieldset, useFieldList, useInputEvent, validateConstraint, report, } from './hooks.js';
3
3
  export * as conform from './helpers.js';
package/index.js CHANGED
@@ -28,6 +28,7 @@ Object.defineProperty(exports, 'validate', {
28
28
  enumerable: true,
29
29
  get: function () { return dom.validate; }
30
30
  });
31
+ exports.report = hooks.report;
31
32
  exports.useFieldList = hooks.useFieldList;
32
33
  exports.useFieldset = hooks.useFieldset;
33
34
  exports.useForm = hooks.useForm;
package/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  export { isFieldElement, list, parse, requestIntent, validate } from '@conform-to/dom';
2
- export { useFieldList, useFieldset, useForm, useInputEvent, validateConstraint } from './hooks.mjs';
2
+ export { report, useFieldList, useFieldset, useForm, useInputEvent, validateConstraint } from './hooks.mjs';
3
3
  import * as helpers from './helpers.mjs';
4
4
  export { helpers as conform };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Conform view adapter for react",
4
4
  "homepage": "https://conform.guide",
5
5
  "license": "MIT",
6
- "version": "0.7.3",
6
+ "version": "0.8.0-pre.0",
7
7
  "main": "index.js",
8
8
  "module": "index.mjs",
9
9
  "types": "index.d.ts",
@@ -30,7 +30,7 @@
30
30
  "url": "https://github.com/edmundhung/conform/issues"
31
31
  },
32
32
  "dependencies": {
33
- "@conform-to/dom": "0.7.3"
33
+ "@conform-to/dom": "0.8.0-pre.0"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": ">=16.8"