@conform-to/react 1.12.1 → 1.13.1

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
@@ -7,7 +7,7 @@
7
7
  ╚══════╝ ╚═════╝ ╚═╝ ╚══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
8
8
  ```
9
9
 
10
- Version 1.12.1 / License MIT / Copyright (c) 2025 Edmund Hung
10
+ Version 1.13.1 / License MIT / Copyright (c) 2025 Edmund Hung
11
11
 
12
12
  Progressively enhance HTML forms with React. Build resilient, type-safe forms with no hassle using web standards.
13
13
 
@@ -32,5 +32,5 @@ export declare function updateFormValue(form: HTMLFormElement, intendedValue: Re
32
32
  * Creates a proxy that dynamically generates intent dispatch functions.
33
33
  * Each property access returns a function that submits the intent to the form.
34
34
  */
35
- export declare function createIntentDispatcher(formElement: HTMLFormElement | (() => HTMLFormElement | null), intentName: string): IntentDispatcher;
35
+ export declare function createIntentDispatcher<FormShape extends Record<string, any>>(formElement: HTMLFormElement | (() => HTMLFormElement | null), intentName: string): IntentDispatcher<FormShape>;
36
36
  //# sourceMappingURL=dom.d.ts.map
@@ -176,13 +176,16 @@ function focusFirstInvalidField(ctx) {
176
176
  function updateFormValue(form, intendedValue, serialize) {
177
177
  for (var element of form.elements) {
178
178
  if (future.isFieldElement(element) && element.name) {
179
- var value = future.getValueAtPath(intendedValue, element.name);
180
- var serializedValue = serialize(value);
181
- if (typeof serializedValue !== 'undefined') {
182
- future.change(element, serializedValue, {
183
- preventDefault: true
184
- });
179
+ var fieldValue = future.getValueAtPath(intendedValue, element.name);
180
+ if (element.type === 'file' && typeof fieldValue === 'undefined') {
181
+ // Do not update file inputs unless there's an intended value
182
+ continue;
185
183
  }
184
+ var serializedValue = serialize(fieldValue);
185
+ var value = typeof serializedValue !== 'undefined' ? serializedValue : future.getFieldDefaultValue(element);
186
+ future.change(element, value, {
187
+ preventDefault: true
188
+ });
186
189
  }
187
190
  }
188
191
  }
@@ -1,4 +1,4 @@
1
- import { updateField, isGlobalInstance, isFieldElement, requestIntent, getValueAtPath, change } from '@conform-to/dom/future';
1
+ import { updateField, isGlobalInstance, isFieldElement, requestIntent, getValueAtPath, getFieldDefaultValue, change } from '@conform-to/dom/future';
2
2
  import { serializeIntent } from './intent.mjs';
3
3
 
4
4
  function getFormElement(formRef) {
@@ -172,13 +172,16 @@ function focusFirstInvalidField(ctx) {
172
172
  function updateFormValue(form, intendedValue, serialize) {
173
173
  for (var element of form.elements) {
174
174
  if (isFieldElement(element) && element.name) {
175
- var value = getValueAtPath(intendedValue, element.name);
176
- var serializedValue = serialize(value);
177
- if (typeof serializedValue !== 'undefined') {
178
- change(element, serializedValue, {
179
- preventDefault: true
180
- });
175
+ var fieldValue = getValueAtPath(intendedValue, element.name);
176
+ if (element.type === 'file' && typeof fieldValue === 'undefined') {
177
+ // Do not update file inputs unless there's an intended value
178
+ continue;
181
179
  }
180
+ var serializedValue = serialize(fieldValue);
181
+ var value = typeof serializedValue !== 'undefined' ? serializedValue : getFieldDefaultValue(element);
182
+ change(element, value, {
183
+ preventDefault: true
184
+ });
182
185
  }
183
186
  }
184
187
  }
@@ -58,7 +58,7 @@ export declare function useConform<ErrorShape, Value = undefined>(formRef: FormR
58
58
  export declare function useForm<FormShape extends Record<string, any> = Record<string, any>, ErrorShape extends BaseErrorShape = DefaultErrorShape, Value = undefined>(options: FormOptions<FormShape, ErrorShape, Value>): {
59
59
  form: FormMetadata<ErrorShape>;
60
60
  fields: Fieldset<FormShape, ErrorShape>;
61
- intent: IntentDispatcher;
61
+ intent: IntentDispatcher<FormShape>;
62
62
  };
63
63
  /**
64
64
  * A React hook that provides access to form-level metadata and state.
@@ -124,7 +124,7 @@ export declare function useField<FieldShape = any>(name: FieldName<FieldShape>,
124
124
  * }
125
125
  * ```
126
126
  */
127
- export declare function useIntent(formRef: FormRef): IntentDispatcher;
127
+ export declare function useIntent<FormShape extends Record<string, any>>(formRef: FormRef): IntentDispatcher<FormShape>;
128
128
  /**
129
129
  * A React hook that lets you sync the state of an input and dispatch native form events from it.
130
130
  * This is useful when emulating native input behavior — typically by rendering a hidden base input
@@ -164,7 +164,7 @@ export declare function useControl(options?: {
164
164
  * @see https://conform.guide/api/react/future/useFormData
165
165
  * @example
166
166
  * ```ts
167
- * const value = useFormData(formRef, formData => formData?.get('fieldName').toString() ?? '');
167
+ * const value = useFormData(formRef, formData => formData?.get('fieldName') ?? '');
168
168
  * ```
169
169
  */
170
170
  export declare function useFormData<Value = any>(formRef: FormRef, select: Selector<FormData, Value>, options: UseFormDataOptions & {
@@ -534,6 +534,16 @@ function useControl(options) {
534
534
  observer
535
535
  } = react.useContext(GlobalFormOptionsContext);
536
536
  var inputRef = react.useRef(null);
537
+ var formRef = react.useMemo(() => ({
538
+ get current() {
539
+ var _input$0$form, _input$;
540
+ var input = inputRef.current;
541
+ if (!input) {
542
+ return null;
543
+ }
544
+ return Array.isArray(input) ? (_input$0$form = (_input$ = input[0]) === null || _input$ === void 0 ? void 0 : _input$.form) !== null && _input$0$form !== void 0 ? _input$0$form : null : input.form;
545
+ }
546
+ }), []);
537
547
  var eventDispatched = react.useRef({});
538
548
  var defaultSnapshot = dom.createDefaultSnapshot(options === null || options === void 0 ? void 0 : options.defaultValue, options === null || options === void 0 ? void 0 : options.defaultChecked, options === null || options === void 0 ? void 0 : options.value);
539
549
  var snapshotRef = react.useRef(defaultSnapshot);
@@ -598,6 +608,7 @@ function useControl(options) {
598
608
  checked: snapshot.checked,
599
609
  options: snapshot.options,
600
610
  files: snapshot.files,
611
+ formRef,
601
612
  register: react.useCallback(element => {
602
613
  if (!element) {
603
614
  inputRef.current = null;
@@ -697,7 +708,7 @@ function useControl(options) {
697
708
  * @see https://conform.guide/api/react/future/useFormData
698
709
  * @example
699
710
  * ```ts
700
- * const value = useFormData(formRef, formData => formData?.get('fieldName').toString() ?? '');
711
+ * const value = useFormData(formRef, formData => formData?.get('fieldName') ?? '');
701
712
  * ```
702
713
  */
703
714
 
@@ -530,6 +530,16 @@ function useControl(options) {
530
530
  observer
531
531
  } = useContext(GlobalFormOptionsContext);
532
532
  var inputRef = useRef(null);
533
+ var formRef = useMemo(() => ({
534
+ get current() {
535
+ var _input$0$form, _input$;
536
+ var input = inputRef.current;
537
+ if (!input) {
538
+ return null;
539
+ }
540
+ return Array.isArray(input) ? (_input$0$form = (_input$ = input[0]) === null || _input$ === void 0 ? void 0 : _input$.form) !== null && _input$0$form !== void 0 ? _input$0$form : null : input.form;
541
+ }
542
+ }), []);
533
543
  var eventDispatched = useRef({});
534
544
  var defaultSnapshot = createDefaultSnapshot(options === null || options === void 0 ? void 0 : options.defaultValue, options === null || options === void 0 ? void 0 : options.defaultChecked, options === null || options === void 0 ? void 0 : options.value);
535
545
  var snapshotRef = useRef(defaultSnapshot);
@@ -594,6 +604,7 @@ function useControl(options) {
594
604
  checked: snapshot.checked,
595
605
  options: snapshot.options,
596
606
  files: snapshot.files,
607
+ formRef,
597
608
  register: useCallback(element => {
598
609
  if (!element) {
599
610
  inputRef.current = null;
@@ -693,7 +704,7 @@ function useControl(options) {
693
704
  * @see https://conform.guide/api/react/future/useFormData
694
705
  * @example
695
706
  * ```ts
696
- * const value = useFormData(formRef, formData => formData?.get('fieldName').toString() ?? '');
707
+ * const value = useFormData(formRef, formData => formData?.get('fieldName') ?? '');
697
708
  * ```
698
709
  */
699
710
 
@@ -92,22 +92,38 @@ function updateListKeys() {
92
92
  */
93
93
  var actionHandlers = {
94
94
  reset: {
95
- onApply() {
96
- return null;
95
+ validatePayload(options) {
96
+ return util.isOptional(options, future.isPlainObject) && (util.isUndefined(options === null || options === void 0 ? void 0 : options.defaultValue) || util.isNullable(options === null || options === void 0 ? void 0 : options.defaultValue, future.isPlainObject));
97
+ },
98
+ onApply(_, options) {
99
+ var {
100
+ defaultValue
101
+ } = options !== null && options !== void 0 ? options : {};
102
+ return defaultValue !== null && defaultValue !== void 0 ? defaultValue : defaultValue === null ? {} : null;
103
+ },
104
+ onUpdate(_, _ref) {
105
+ var _intent$payload;
106
+ var {
107
+ intent
108
+ } = _ref;
109
+ var defaultValue = (_intent$payload = intent.payload) === null || _intent$payload === void 0 ? void 0 : _intent$payload.defaultValue;
110
+ return util.merge(state.initializeState(), {
111
+ serverIntendedValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : defaultValue === null ? {} : null
112
+ });
97
113
  }
98
114
  },
99
115
  validate: {
100
116
  validatePayload(name) {
101
117
  return util.isOptional(name, util.isString);
102
118
  },
103
- onUpdate(state, _ref) {
104
- var _intent$payload;
119
+ onUpdate(state, _ref2) {
120
+ var _intent$payload2;
105
121
  var {
106
122
  submission,
107
123
  intent,
108
124
  error
109
- } = _ref;
110
- var name = (_intent$payload = intent.payload) !== null && _intent$payload !== void 0 ? _intent$payload : '';
125
+ } = _ref2;
126
+ var name = (_intent$payload2 = intent.payload) !== null && _intent$payload2 !== void 0 ? _intent$payload2 : '';
111
127
  var basePath = future.getPathSegments(name);
112
128
  var allFields = error ?
113
129
  // Consider fields / fieldset with errors as touched too
@@ -129,14 +145,17 @@ var actionHandlers = {
129
145
  return future.isPlainObject(options) && util.isOptional(options.name, util.isString) && util.isOptional(options.index, util.isNumber) && !util.isUndefined(options.value);
130
146
  },
131
147
  onApply(value, options) {
132
- return util.updateValueAtPath(value, future.appendPathSegment(options.name, options.index), options.value);
148
+ var _options$value;
149
+ var name = future.appendPathSegment(options.name, options.index);
150
+ var newValue = (_options$value = options.value) !== null && _options$value !== void 0 ? _options$value : name === '' ? {} : null;
151
+ return util.updateValueAtPath(value, name, newValue);
133
152
  },
134
- onUpdate(state, _ref2) {
153
+ onUpdate(state, _ref3) {
135
154
  var {
136
155
  type,
137
156
  submission,
138
157
  intent
139
- } = _ref2;
158
+ } = _ref3;
140
159
  if (type === 'server') {
141
160
  return state;
142
161
  }
@@ -172,13 +191,13 @@ var actionHandlers = {
172
191
  insertItem(list, options.defaultValue, (_options$index = options.index) !== null && _options$index !== void 0 ? _options$index : list.length);
173
192
  return util.updateValueAtPath(value, options.name, list);
174
193
  },
175
- onUpdate(state$1, _ref3) {
194
+ onUpdate(state$1, _ref4) {
176
195
  var _intent$payload$index;
177
196
  var {
178
197
  type,
179
198
  submission,
180
199
  intent
181
- } = _ref3;
200
+ } = _ref4;
182
201
  if (type === 'server') {
183
202
  return state$1;
184
203
  }
@@ -214,12 +233,12 @@ var actionHandlers = {
214
233
  removeItem(list, options.index);
215
234
  return util.updateValueAtPath(value, options.name, list);
216
235
  },
217
- onUpdate(state$1, _ref4) {
236
+ onUpdate(state$1, _ref5) {
218
237
  var {
219
238
  type,
220
239
  submission,
221
240
  intent
222
- } = _ref4;
241
+ } = _ref5;
223
242
  if (type === 'server') {
224
243
  return state$1;
225
244
  }
@@ -258,12 +277,12 @@ var actionHandlers = {
258
277
  reorderItems(list, options.from, options.to);
259
278
  return util.updateValueAtPath(value, options.name, list);
260
279
  },
261
- onUpdate(state$1, _ref5) {
280
+ onUpdate(state$1, _ref6) {
262
281
  var {
263
282
  type,
264
283
  submission,
265
284
  intent
266
- } = _ref5;
285
+ } = _ref6;
267
286
  if (type === 'server') {
268
287
  return state$1;
269
288
  }
@@ -1,7 +1,7 @@
1
1
  import { objectSpread2 as _objectSpread2 } from '../_virtual/_rollupPluginBabelHelpers.mjs';
2
2
  import { getPathSegments, getRelativePath, isPlainObject, appendPathSegment } from '@conform-to/dom/future';
3
- import { isOptional, appendUniqueItem, merge, isUndefined, updateValueAtPath, isString, getArrayAtPath, createPathIndexUpdater, compactMap, generateUniqueKey, isNumber, transformKeys } from './util.mjs';
4
- import { getDefaultListKey } from './state.mjs';
3
+ import { isOptional, isUndefined, isNullable, merge, appendUniqueItem, updateValueAtPath, isString, getArrayAtPath, createPathIndexUpdater, compactMap, generateUniqueKey, isNumber, transformKeys } from './util.mjs';
4
+ import { initializeState, getDefaultListKey } from './state.mjs';
5
5
 
6
6
  /**
7
7
  * Serializes intent to string format: "type" or "type(payload)".
@@ -88,22 +88,38 @@ function updateListKeys() {
88
88
  */
89
89
  var actionHandlers = {
90
90
  reset: {
91
- onApply() {
92
- return null;
91
+ validatePayload(options) {
92
+ return isOptional(options, isPlainObject) && (isUndefined(options === null || options === void 0 ? void 0 : options.defaultValue) || isNullable(options === null || options === void 0 ? void 0 : options.defaultValue, isPlainObject));
93
+ },
94
+ onApply(_, options) {
95
+ var {
96
+ defaultValue
97
+ } = options !== null && options !== void 0 ? options : {};
98
+ return defaultValue !== null && defaultValue !== void 0 ? defaultValue : defaultValue === null ? {} : null;
99
+ },
100
+ onUpdate(_, _ref) {
101
+ var _intent$payload;
102
+ var {
103
+ intent
104
+ } = _ref;
105
+ var defaultValue = (_intent$payload = intent.payload) === null || _intent$payload === void 0 ? void 0 : _intent$payload.defaultValue;
106
+ return merge(initializeState(), {
107
+ serverIntendedValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : defaultValue === null ? {} : null
108
+ });
93
109
  }
94
110
  },
95
111
  validate: {
96
112
  validatePayload(name) {
97
113
  return isOptional(name, isString);
98
114
  },
99
- onUpdate(state, _ref) {
100
- var _intent$payload;
115
+ onUpdate(state, _ref2) {
116
+ var _intent$payload2;
101
117
  var {
102
118
  submission,
103
119
  intent,
104
120
  error
105
- } = _ref;
106
- var name = (_intent$payload = intent.payload) !== null && _intent$payload !== void 0 ? _intent$payload : '';
121
+ } = _ref2;
122
+ var name = (_intent$payload2 = intent.payload) !== null && _intent$payload2 !== void 0 ? _intent$payload2 : '';
107
123
  var basePath = getPathSegments(name);
108
124
  var allFields = error ?
109
125
  // Consider fields / fieldset with errors as touched too
@@ -125,14 +141,17 @@ var actionHandlers = {
125
141
  return isPlainObject(options) && isOptional(options.name, isString) && isOptional(options.index, isNumber) && !isUndefined(options.value);
126
142
  },
127
143
  onApply(value, options) {
128
- return updateValueAtPath(value, appendPathSegment(options.name, options.index), options.value);
144
+ var _options$value;
145
+ var name = appendPathSegment(options.name, options.index);
146
+ var newValue = (_options$value = options.value) !== null && _options$value !== void 0 ? _options$value : name === '' ? {} : null;
147
+ return updateValueAtPath(value, name, newValue);
129
148
  },
130
- onUpdate(state, _ref2) {
149
+ onUpdate(state, _ref3) {
131
150
  var {
132
151
  type,
133
152
  submission,
134
153
  intent
135
- } = _ref2;
154
+ } = _ref3;
136
155
  if (type === 'server') {
137
156
  return state;
138
157
  }
@@ -168,13 +187,13 @@ var actionHandlers = {
168
187
  insertItem(list, options.defaultValue, (_options$index = options.index) !== null && _options$index !== void 0 ? _options$index : list.length);
169
188
  return updateValueAtPath(value, options.name, list);
170
189
  },
171
- onUpdate(state, _ref3) {
190
+ onUpdate(state, _ref4) {
172
191
  var _intent$payload$index;
173
192
  var {
174
193
  type,
175
194
  submission,
176
195
  intent
177
- } = _ref3;
196
+ } = _ref4;
178
197
  if (type === 'server') {
179
198
  return state;
180
199
  }
@@ -210,12 +229,12 @@ var actionHandlers = {
210
229
  removeItem(list, options.index);
211
230
  return updateValueAtPath(value, options.name, list);
212
231
  },
213
- onUpdate(state, _ref4) {
232
+ onUpdate(state, _ref5) {
214
233
  var {
215
234
  type,
216
235
  submission,
217
236
  intent
218
- } = _ref4;
237
+ } = _ref5;
219
238
  if (type === 'server') {
220
239
  return state;
221
240
  }
@@ -254,12 +273,12 @@ var actionHandlers = {
254
273
  reorderItems(list, options.from, options.to);
255
274
  return updateValueAtPath(value, options.name, list);
256
275
  },
257
- onUpdate(state, _ref5) {
276
+ onUpdate(state, _ref6) {
258
277
  var {
259
278
  type,
260
279
  submission,
261
280
  intent
262
- } = _ref5;
281
+ } = _ref6;
263
282
  if (type === 'server') {
264
283
  return state;
265
284
  }
@@ -11,8 +11,8 @@ export declare function updateState<ErrorShape>(state: FormState<ErrorShape>, ac
11
11
  handlers: Record<string, ActionHandler>;
12
12
  reset: () => FormState<ErrorShape>;
13
13
  }>): FormState<ErrorShape>;
14
- export declare function getDefaultValue(context: FormContext<any>, name: string, serialize?: Serialize): string | undefined;
15
- export declare function getDefaultOptions(context: FormContext<any>, name: string, serialize?: Serialize): string[] | undefined;
14
+ export declare function getDefaultValue(context: FormContext<any>, name: string, serialize?: Serialize): string;
15
+ export declare function getDefaultOptions(context: FormContext<any>, name: string, serialize?: Serialize): string[];
16
16
  export declare function isDefaultChecked(context: FormContext<any>, name: string, serialize?: Serialize): boolean;
17
17
  /**
18
18
  * Determine if the field is touched
@@ -74,6 +74,7 @@ function getDefaultValue(context, name) {
74
74
  if (typeof serializedValue === 'string') {
75
75
  return serializedValue;
76
76
  }
77
+ return '';
77
78
  }
78
79
  function getDefaultOptions(context, name) {
79
80
  var _ref2, _context$state$server2;
@@ -86,13 +87,17 @@ function getDefaultOptions(context, name) {
86
87
  if (typeof serializedValue === 'string') {
87
88
  return [serializedValue];
88
89
  }
90
+ return [];
89
91
  }
90
92
  function isDefaultChecked(context, name) {
91
93
  var _ref3, _context$state$server3;
92
94
  var serialize = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : future.serialize;
93
95
  var value = future.getValueAtPath((_ref3 = (_context$state$server3 = context.state.serverIntendedValue) !== null && _context$state$server3 !== void 0 ? _context$state$server3 : context.state.clientIntendedValue) !== null && _ref3 !== void 0 ? _ref3 : context.defaultValue, name);
94
96
  var serializedValue = serialize(value);
95
- return serializedValue === 'on';
97
+ if (typeof serializedValue === 'string') {
98
+ return serializedValue === 'on';
99
+ }
100
+ return false;
96
101
  }
97
102
 
98
103
  /**
@@ -70,6 +70,7 @@ function getDefaultValue(context, name) {
70
70
  if (typeof serializedValue === 'string') {
71
71
  return serializedValue;
72
72
  }
73
+ return '';
73
74
  }
74
75
  function getDefaultOptions(context, name) {
75
76
  var _ref2, _context$state$server2;
@@ -82,13 +83,17 @@ function getDefaultOptions(context, name) {
82
83
  if (typeof serializedValue === 'string') {
83
84
  return [serializedValue];
84
85
  }
86
+ return [];
85
87
  }
86
88
  function isDefaultChecked(context, name) {
87
89
  var _ref3, _context$state$server3;
88
90
  var serialize$1 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : serialize;
89
91
  var value = getValueAtPath((_ref3 = (_context$state$server3 = context.state.serverIntendedValue) !== null && _context$state$server3 !== void 0 ? _context$state$server3 : context.state.clientIntendedValue) !== null && _ref3 !== void 0 ? _ref3 : context.defaultValue, name);
90
92
  var serializedValue = serialize$1(value);
91
- return serializedValue === 'on';
93
+ if (typeof serializedValue === 'string') {
94
+ return serializedValue === 'on';
95
+ }
96
+ return false;
92
97
  }
93
98
 
94
99
  /**
@@ -36,6 +36,11 @@ export type Control = {
36
36
  * Registers the base input element(s). Accepts a single input or an array for groups.
37
37
  */
38
38
  register: (element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLCollectionOf<HTMLInputElement> | NodeListOf<HTMLInputElement> | null | undefined) => void;
39
+ /**
40
+ * A ref object containing the form element associated with the registered input.
41
+ * Use this with hooks like useFormData() and useIntent().
42
+ */
43
+ formRef: React.RefObject<HTMLFormElement | null>;
39
44
  /**
40
45
  * Programmatically updates the input value and emits
41
46
  * both [change](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event) and
@@ -64,9 +69,9 @@ export type UseFormDataOptions = {
64
69
  */
65
70
  acceptFiles?: boolean;
66
71
  };
67
- export type DefaultValue<FormShape> = FormShape extends Record<string, any> ? {
68
- [Key in keyof FormShape]?: DefaultValue<FormShape[Key]>;
69
- } | null | undefined : FormShape extends Array<infer Item> ? Array<DefaultValue<Item>> | null | undefined : FormShape | string | null | undefined;
72
+ export type DefaultValue<Shape> = Shape extends Record<string, any> ? {
73
+ [Key in keyof Shape]?: DefaultValue<Shape[Key]>;
74
+ } | null | undefined : Shape extends Array<infer Item> ? Array<DefaultValue<Item>> | null | undefined : Shape extends File | File[] ? null | undefined : Shape | string | null | undefined;
70
75
  export type FormState<ErrorShape extends BaseErrorShape = DefaultErrorShape> = {
71
76
  /** Unique identifier that changes on form reset to trigger reset side effects */
72
77
  resetKey: string;
@@ -186,20 +191,39 @@ export type UnknownIntent = {
186
191
  export type UnknownArgs<Args extends any[]> = {
187
192
  [Key in keyof Args]: unknown;
188
193
  };
189
- export interface IntentDispatcher {
194
+ export interface IntentDispatcher<FormShape extends Record<string, any> = Record<string, any>> {
190
195
  /**
191
196
  * Validate the whole form or a specific field?
192
197
  */
193
198
  validate(name?: string): void;
194
199
  /**
195
- * Reset the form to its initial state.
200
+ * Reset the form to a specific default value.
201
+ *
202
+ * @param options.defaultValue - The value to reset the form to. Pass `null` to clear all fields, or omit to reset to the initial default value from `useForm`.
203
+ *
204
+ * @example
205
+ * ```tsx
206
+ * // Reset to initial default value
207
+ * intent.reset()
208
+ *
209
+ * // Clear all fields
210
+ * intent.reset({ defaultValue: null })
211
+ *
212
+ * // Restore to a specific snapshot
213
+ * intent.reset({ defaultValue: snapshotValue })
214
+ * ```
196
215
  */
197
- reset(): void;
216
+ reset(options?: {
217
+ /**
218
+ * The value to reset the form to. If not provided, resets to the default value from `useForm`. Pass `null` to clear all fields instead.
219
+ */
220
+ defaultValue?: DefaultValue<FormShape>;
221
+ }): void;
198
222
  /**
199
223
  * Update a field or a fieldset.
200
224
  * If you provide a fieldset name, it will update all fields within that fieldset
201
225
  */
202
- update<FieldShape>(options: {
226
+ update<FieldShape = FormShape>(options: {
203
227
  /**
204
228
  * The name of the field. If you provide a fieldset name, it will update all fields within that fieldset.
205
229
  */
@@ -207,7 +231,7 @@ export interface IntentDispatcher {
207
231
  /**
208
232
  * Specify the index of the item to update if the field is an array.
209
233
  */
210
- index?: FieldShape extends Array<any> ? number : never;
234
+ index?: FieldShape extends Array<any> ? number : unknown extends FieldShape ? number : never;
211
235
  /**
212
236
  * The new value for the field or fieldset.
213
237
  */
@@ -305,12 +329,28 @@ export type BaseMetadata<FieldShape, ErrorShape extends BaseErrorShape> = Valida
305
329
  errorId: string;
306
330
  /** The form's unique identifier for associating field via the `form` attribute. */
307
331
  formId: string;
308
- /** The field's default value as a string. */
309
- defaultValue: string | undefined;
310
- /** Default selected options for multi-select fields or checkbox group. */
311
- defaultOptions: string[] | undefined;
312
- /** Default checked state for checkbox/radio inputs. */
313
- defaultChecked: boolean | undefined;
332
+ /**
333
+ * The field's default value as a string.
334
+ *
335
+ * Returns an empty string `''` when:
336
+ * - No default value is set (field value is `null` or `undefined`)
337
+ * - The field value cannot be serialized to a string (e.g., objects or arrays)
338
+ */
339
+ defaultValue: string;
340
+ /**
341
+ * Default selected options for multi-select fields or checkbox group.
342
+ *
343
+ * Returns an empty array `[]` when:
344
+ * - No default options are set (field value is `null` or `undefined`)
345
+ * - The field value cannot be serialized to a string array (e.g., nested objects or arrays of objects)
346
+ */
347
+ defaultOptions: string[];
348
+ /**
349
+ * Default checked state for checkbox inputs. Returns `true` if the field value is `'on'`.
350
+ *
351
+ * For radio buttons, compare the field's `defaultValue` with the radio button's value attribute instead.
352
+ */
353
+ defaultChecked: boolean;
314
354
  /** Whether this field has been touched (through intent.validate() or the shouldValidate option). */
315
355
  touched: boolean;
316
356
  /** Whether this field currently has no validation errors. */
@@ -4,6 +4,7 @@ import { ValidateHandler, ValidateResult } from './types';
4
4
  export declare function isUndefined(value: unknown): value is undefined;
5
5
  export declare function isString(value: unknown): value is string;
6
6
  export declare function isNumber(value: unknown): value is number;
7
+ export declare function isNullable<T>(value: unknown, typeGuard: (value: unknown) => value is T): value is T | null;
7
8
  export declare function isOptional<T>(value: unknown, typeGuard: (value: unknown) => value is T): value is T | undefined;
8
9
  export declare function getArrayAtPath<Type>(formValue: Record<string, Type> | null, name: string): Array<Type>;
9
10
  /**
@@ -13,6 +13,9 @@ function isString(value) {
13
13
  function isNumber(value) {
14
14
  return typeof value === 'number';
15
15
  }
16
+ function isNullable(value, typeGuard) {
17
+ return value === null || typeGuard(value);
18
+ }
16
19
  function isOptional(value, typeGuard) {
17
20
  return isUndefined(value) || typeGuard(value);
18
21
  }
@@ -188,6 +191,7 @@ exports.compactMap = compactMap;
188
191
  exports.createPathIndexUpdater = createPathIndexUpdater;
189
192
  exports.generateUniqueKey = generateUniqueKey;
190
193
  exports.getArrayAtPath = getArrayAtPath;
194
+ exports.isNullable = isNullable;
191
195
  exports.isNumber = isNumber;
192
196
  exports.isOptional = isOptional;
193
197
  exports.isString = isString;
@@ -9,6 +9,9 @@ function isString(value) {
9
9
  function isNumber(value) {
10
10
  return typeof value === 'number';
11
11
  }
12
+ function isNullable(value, typeGuard) {
13
+ return value === null || typeGuard(value);
14
+ }
12
15
  function isOptional(value, typeGuard) {
13
16
  return isUndefined(value) || typeGuard(value);
14
17
  }
@@ -179,4 +182,4 @@ function generateUniqueKey() {
179
182
  return Math.trunc(Date.now() * Math.random()).toString(36);
180
183
  }
181
184
 
182
- export { appendUniqueItem, compactMap, createPathIndexUpdater, generateUniqueKey, getArrayAtPath, isNumber, isOptional, isString, isUndefined, merge, normalizeFormError, normalizeValidateResult, resolveStandardSchemaResult, resolveValidateResult, transformKeys, updateValueAtPath };
185
+ export { appendUniqueItem, compactMap, createPathIndexUpdater, generateUniqueKey, getArrayAtPath, isNullable, isNumber, isOptional, isString, isUndefined, merge, normalizeFormError, normalizeValidateResult, resolveStandardSchemaResult, resolveValidateResult, transformKeys, updateValueAtPath };
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": "1.12.1",
6
+ "version": "1.13.1",
7
7
  "main": "./dist/index.js",
8
8
  "module": "./dist/index.mjs",
9
9
  "types": "./dist/index.d.ts",
@@ -41,7 +41,7 @@
41
41
  "url": "https://github.com/edmundhung/conform/issues"
42
42
  },
43
43
  "dependencies": {
44
- "@conform-to/dom": "1.12.1"
44
+ "@conform-to/dom": "1.13.1"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@babel/core": "^7.17.8",
@@ -56,7 +56,8 @@
56
56
  "react-dom": "^18.2.0",
57
57
  "rollup-plugin-copy": "^3.4.0",
58
58
  "rollup": "^2.79.1",
59
- "vitest-browser-react": "^0.3.0"
59
+ "typescript": "^5.8.3",
60
+ "vitest-browser-react": "^1.0.1"
60
61
  },
61
62
  "peerDependencies": {
62
63
  "react": ">=18"