@conform-to/react 1.8.2 → 1.9.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,23 +7,23 @@
7
7
  ╚══════╝ ╚═════╝ ╚═╝ ╚══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
8
8
  ```
9
9
 
10
- Version 1.8.2 / License MIT / Copyright (c) 2024 Edmund Hung
10
+ Version 1.9.1 / License MIT / Copyright (c) 2025 Edmund Hung
11
11
 
12
- A type-safe form validation library utilizing web fundamentals to progressively enhance HTML Forms with full support for server frameworks like Remix and Next.js.
12
+ Progressively enhance HTML forms with React. Build resilient, type-safe forms with no hassle using web standards.
13
13
 
14
- # Getting Started
14
+ ## Getting Started
15
15
 
16
16
  Check out the overview and tutorial at our website https://conform.guide
17
17
 
18
- # Features
18
+ ## Features
19
19
 
20
- - Progressive enhancement first APIs
21
- - Type-safe field inference
22
- - Fine-grained subscription
23
- - Built-in accessibility helpers
24
- - Automatic type coercion with Zod
20
+ - Full type safety with schema field inference
21
+ - Standard Schema support with enhanced Zod and Valibot integration
22
+ - Progressive enhancement first design with built-in accessibility features
23
+ - Native Server Actions support for Remix and Next.js
24
+ - Built on web standards for flexible composition with other tools
25
25
 
26
- # Documentation
26
+ ## Documentation
27
27
 
28
28
  - Validation: https://conform.guide/validation
29
29
  - Nested object and Array: https://conform.guide/complex-structures
@@ -31,6 +31,6 @@ Check out the overview and tutorial at our website https://conform.guide
31
31
  - Intent button: https://conform.guide/intent-button
32
32
  - Accessibility Guide: https://conform.guide/accessibility
33
33
 
34
- # Support
34
+ ## Support
35
35
 
36
36
  To report a bug, please open an issue on the repository at https://github.com/edmundhung/conform. For feature requests and questions, you can post them in the Discussions section.
@@ -0,0 +1,36 @@
1
+ import { Serialize } from '@conform-to/dom/future';
2
+ import type { ErrorContext, FormRef, InputSnapshot, IntentDispatcher } from './types';
3
+ export declare function getFormElement(formRef: FormRef | undefined): HTMLFormElement | null;
4
+ export declare function getSubmitEvent(event: React.FormEvent<HTMLFormElement>): SubmitEvent;
5
+ export declare function initializeField(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, options: {
6
+ defaultValue?: string | string[] | File | File[] | null;
7
+ defaultChecked?: boolean;
8
+ value?: string;
9
+ } | undefined): void;
10
+ /**
11
+ * Makes hidden form inputs focusable with visually hidden styles
12
+ */
13
+ export declare function makeInputFocusable(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement): void;
14
+ export declare function getRadioGroupValue(inputs: Array<HTMLInputElement>): string | undefined;
15
+ export declare function getCheckboxGroupValue(inputs: Array<HTMLInputElement>): string[] | undefined;
16
+ export declare function getInputSnapshot(input: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement): InputSnapshot;
17
+ /**
18
+ * Creates an InputSnapshot based on the provided options:
19
+ * - checkbox/radio: value / defaultChecked
20
+ * - file inputs: defaultValue is File or FileList
21
+ * - select multiple: defaultValue is string array
22
+ * - others: defaultValue is string
23
+ */
24
+ export declare function createDefaultSnapshot(defaultValue: string | string[] | File | File[] | FileList | null | undefined, defaultChecked: boolean | undefined, value: string | undefined): InputSnapshot;
25
+ /**
26
+ * Focuses the first field with validation errors on default form submission.
27
+ * Does nothing if the submission was triggered with a specific intent (e.g. validate / insert)
28
+ */
29
+ export declare function focusFirstInvalidField<ErrorShape>(ctx: ErrorContext<ErrorShape>): void;
30
+ export declare function updateFormValue(form: HTMLFormElement, intendedValue: Record<string, unknown>, serialize: Serialize): void;
31
+ /**
32
+ * Creates a proxy that dynamically generates intent dispatch functions.
33
+ * Each property access returns a function that submits the intent to the form.
34
+ */
35
+ export declare function createIntentDispatcher(formElement: HTMLFormElement | (() => HTMLFormElement | null), intentName: string): IntentDispatcher;
36
+ //# sourceMappingURL=dom.d.ts.map
@@ -0,0 +1,226 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var future = require('@conform-to/dom/future');
6
+ var intent = require('./intent.js');
7
+
8
+ function getFormElement(formRef) {
9
+ var _element$form;
10
+ if (typeof formRef === 'string') {
11
+ return document.forms.namedItem(formRef);
12
+ }
13
+ var element = formRef === null || formRef === void 0 ? void 0 : formRef.current;
14
+ if (element instanceof HTMLFormElement) {
15
+ return element;
16
+ }
17
+ return (_element$form = element === null || element === void 0 ? void 0 : element.form) !== null && _element$form !== void 0 ? _element$form : null;
18
+ }
19
+ function getSubmitEvent(event) {
20
+ if (event.type !== 'submit') {
21
+ throw new Error('The event is not a submit event');
22
+ }
23
+ return event.nativeEvent;
24
+ }
25
+ function initializeField(element, options) {
26
+ var _options$value;
27
+ if (element.dataset.conform) {
28
+ return;
29
+ }
30
+ var defaultValue = typeof (options === null || options === void 0 ? void 0 : options.value) === 'string' || typeof (options === null || options === void 0 ? void 0 : options.defaultChecked) === 'boolean' ? options.defaultChecked ? (_options$value = options.value) !== null && _options$value !== void 0 ? _options$value : 'on' : null : options === null || options === void 0 ? void 0 : options.defaultValue;
31
+
32
+ // Update the value of the element, including the default value
33
+ future.updateField(element, {
34
+ value: defaultValue,
35
+ defaultValue
36
+ });
37
+ element.dataset.conform = 'initialized';
38
+ }
39
+
40
+ /**
41
+ * Makes hidden form inputs focusable with visually hidden styles
42
+ */
43
+ function makeInputFocusable(element) {
44
+ if (!element.hidden && element.type !== 'hidden') {
45
+ return;
46
+ }
47
+
48
+ // Style the element to be visually hidden
49
+ element.style.position = 'absolute';
50
+ element.style.width = '1px';
51
+ element.style.height = '1px';
52
+ element.style.padding = '0';
53
+ element.style.margin = '-1px';
54
+ element.style.overflow = 'hidden';
55
+ element.style.clip = 'rect(0,0,0,0)';
56
+ element.style.whiteSpace = 'nowrap';
57
+ element.style.border = '0';
58
+
59
+ // Hide the element from screen readers
60
+ element.setAttribute('aria-hidden', 'true');
61
+
62
+ // Make sure people won't tab to this element
63
+ element.tabIndex = -1;
64
+
65
+ // Set the element to be visible again so it can be focused
66
+ if (element.hidden) {
67
+ element.hidden = false;
68
+ }
69
+ if (element.type === 'hidden') {
70
+ element.setAttribute('type', 'text');
71
+ }
72
+ }
73
+ function getRadioGroupValue(inputs) {
74
+ for (var input of inputs) {
75
+ if (input.type === 'radio' && input.checked) {
76
+ return input.value;
77
+ }
78
+ }
79
+ }
80
+ function getCheckboxGroupValue(inputs) {
81
+ var values;
82
+ for (var input of inputs) {
83
+ if (input.type === 'checkbox') {
84
+ var _values;
85
+ (_values = values) !== null && _values !== void 0 ? _values : values = [];
86
+ if (input.checked) {
87
+ values.push(input.value);
88
+ }
89
+ }
90
+ }
91
+ return values;
92
+ }
93
+ function getInputSnapshot(input) {
94
+ if (input instanceof HTMLInputElement) {
95
+ switch (input.type) {
96
+ case 'file':
97
+ return {
98
+ files: input.files ? Array.from(input.files) : undefined
99
+ };
100
+ case 'radio':
101
+ case 'checkbox':
102
+ return {
103
+ value: input.value,
104
+ checked: input.checked
105
+ };
106
+ }
107
+ } else if (input instanceof HTMLSelectElement && input.multiple) {
108
+ return {
109
+ options: Array.from(input.selectedOptions).map(option => option.value)
110
+ };
111
+ }
112
+ return {
113
+ value: input.value
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Creates an InputSnapshot based on the provided options:
119
+ * - checkbox/radio: value / defaultChecked
120
+ * - file inputs: defaultValue is File or FileList
121
+ * - select multiple: defaultValue is string array
122
+ * - others: defaultValue is string
123
+ */
124
+ function createDefaultSnapshot(defaultValue, defaultChecked, value) {
125
+ if (typeof value === 'string' || typeof defaultChecked === 'boolean') {
126
+ return {
127
+ value: value !== null && value !== void 0 ? value : 'on',
128
+ checked: defaultChecked
129
+ };
130
+ }
131
+ if (typeof defaultValue === 'string') {
132
+ return {
133
+ value: defaultValue
134
+ };
135
+ }
136
+ if (Array.isArray(defaultValue)) {
137
+ if (defaultValue.every(item => typeof item === 'string')) {
138
+ return {
139
+ options: defaultValue
140
+ };
141
+ } else {
142
+ return {
143
+ files: defaultValue
144
+ };
145
+ }
146
+ }
147
+ if (future.isGlobalInstance(defaultValue, 'File')) {
148
+ return {
149
+ files: [defaultValue]
150
+ };
151
+ }
152
+ if (future.isGlobalInstance(defaultValue, 'FileList')) {
153
+ return {
154
+ files: Array.from(defaultValue)
155
+ };
156
+ }
157
+ return {};
158
+ }
159
+
160
+ /**
161
+ * Focuses the first field with validation errors on default form submission.
162
+ * Does nothing if the submission was triggered with a specific intent (e.g. validate / insert)
163
+ */
164
+ function focusFirstInvalidField(ctx) {
165
+ if (ctx.intent) {
166
+ return;
167
+ }
168
+ for (var element of ctx.formElement.elements) {
169
+ var _ctx$error$fieldError;
170
+ if (future.isFieldElement(element) && (_ctx$error$fieldError = ctx.error.fieldErrors[element.name]) !== null && _ctx$error$fieldError !== void 0 && _ctx$error$fieldError.length) {
171
+ element.focus();
172
+ break;
173
+ }
174
+ }
175
+ }
176
+ function updateFormValue(form, intendedValue, serialize) {
177
+ for (var element of form.elements) {
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
+ });
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Creates a proxy that dynamically generates intent dispatch functions.
192
+ * Each property access returns a function that submits the intent to the form.
193
+ */
194
+ function createIntentDispatcher(formElement, intentName) {
195
+ return new Proxy({}, {
196
+ get(target, type, receiver) {
197
+ if (typeof type === 'string') {
198
+ var _target$type;
199
+ // @ts-expect-error
200
+ (_target$type = target[type]) !== null && _target$type !== void 0 ? _target$type : target[type] = payload => {
201
+ var form = typeof formElement === 'function' ? formElement() : formElement;
202
+ if (!form) {
203
+ throw new Error("Dispatching \"".concat(type, "\" intent failed; No form element found."));
204
+ }
205
+ future.requestIntent(form, intentName, intent.serializeIntent({
206
+ type,
207
+ payload
208
+ }));
209
+ };
210
+ }
211
+ return Reflect.get(target, type, receiver);
212
+ }
213
+ });
214
+ }
215
+
216
+ exports.createDefaultSnapshot = createDefaultSnapshot;
217
+ exports.createIntentDispatcher = createIntentDispatcher;
218
+ exports.focusFirstInvalidField = focusFirstInvalidField;
219
+ exports.getCheckboxGroupValue = getCheckboxGroupValue;
220
+ exports.getFormElement = getFormElement;
221
+ exports.getInputSnapshot = getInputSnapshot;
222
+ exports.getRadioGroupValue = getRadioGroupValue;
223
+ exports.getSubmitEvent = getSubmitEvent;
224
+ exports.initializeField = initializeField;
225
+ exports.makeInputFocusable = makeInputFocusable;
226
+ exports.updateFormValue = updateFormValue;
@@ -0,0 +1,212 @@
1
+ import { updateField, isGlobalInstance, isFieldElement, requestIntent, getValueAtPath, change } from '@conform-to/dom/future';
2
+ import { serializeIntent } from './intent.mjs';
3
+
4
+ function getFormElement(formRef) {
5
+ var _element$form;
6
+ if (typeof formRef === 'string') {
7
+ return document.forms.namedItem(formRef);
8
+ }
9
+ var element = formRef === null || formRef === void 0 ? void 0 : formRef.current;
10
+ if (element instanceof HTMLFormElement) {
11
+ return element;
12
+ }
13
+ return (_element$form = element === null || element === void 0 ? void 0 : element.form) !== null && _element$form !== void 0 ? _element$form : null;
14
+ }
15
+ function getSubmitEvent(event) {
16
+ if (event.type !== 'submit') {
17
+ throw new Error('The event is not a submit event');
18
+ }
19
+ return event.nativeEvent;
20
+ }
21
+ function initializeField(element, options) {
22
+ var _options$value;
23
+ if (element.dataset.conform) {
24
+ return;
25
+ }
26
+ var defaultValue = typeof (options === null || options === void 0 ? void 0 : options.value) === 'string' || typeof (options === null || options === void 0 ? void 0 : options.defaultChecked) === 'boolean' ? options.defaultChecked ? (_options$value = options.value) !== null && _options$value !== void 0 ? _options$value : 'on' : null : options === null || options === void 0 ? void 0 : options.defaultValue;
27
+
28
+ // Update the value of the element, including the default value
29
+ updateField(element, {
30
+ value: defaultValue,
31
+ defaultValue
32
+ });
33
+ element.dataset.conform = 'initialized';
34
+ }
35
+
36
+ /**
37
+ * Makes hidden form inputs focusable with visually hidden styles
38
+ */
39
+ function makeInputFocusable(element) {
40
+ if (!element.hidden && element.type !== 'hidden') {
41
+ return;
42
+ }
43
+
44
+ // Style the element to be visually hidden
45
+ element.style.position = 'absolute';
46
+ element.style.width = '1px';
47
+ element.style.height = '1px';
48
+ element.style.padding = '0';
49
+ element.style.margin = '-1px';
50
+ element.style.overflow = 'hidden';
51
+ element.style.clip = 'rect(0,0,0,0)';
52
+ element.style.whiteSpace = 'nowrap';
53
+ element.style.border = '0';
54
+
55
+ // Hide the element from screen readers
56
+ element.setAttribute('aria-hidden', 'true');
57
+
58
+ // Make sure people won't tab to this element
59
+ element.tabIndex = -1;
60
+
61
+ // Set the element to be visible again so it can be focused
62
+ if (element.hidden) {
63
+ element.hidden = false;
64
+ }
65
+ if (element.type === 'hidden') {
66
+ element.setAttribute('type', 'text');
67
+ }
68
+ }
69
+ function getRadioGroupValue(inputs) {
70
+ for (var input of inputs) {
71
+ if (input.type === 'radio' && input.checked) {
72
+ return input.value;
73
+ }
74
+ }
75
+ }
76
+ function getCheckboxGroupValue(inputs) {
77
+ var values;
78
+ for (var input of inputs) {
79
+ if (input.type === 'checkbox') {
80
+ var _values;
81
+ (_values = values) !== null && _values !== void 0 ? _values : values = [];
82
+ if (input.checked) {
83
+ values.push(input.value);
84
+ }
85
+ }
86
+ }
87
+ return values;
88
+ }
89
+ function getInputSnapshot(input) {
90
+ if (input instanceof HTMLInputElement) {
91
+ switch (input.type) {
92
+ case 'file':
93
+ return {
94
+ files: input.files ? Array.from(input.files) : undefined
95
+ };
96
+ case 'radio':
97
+ case 'checkbox':
98
+ return {
99
+ value: input.value,
100
+ checked: input.checked
101
+ };
102
+ }
103
+ } else if (input instanceof HTMLSelectElement && input.multiple) {
104
+ return {
105
+ options: Array.from(input.selectedOptions).map(option => option.value)
106
+ };
107
+ }
108
+ return {
109
+ value: input.value
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Creates an InputSnapshot based on the provided options:
115
+ * - checkbox/radio: value / defaultChecked
116
+ * - file inputs: defaultValue is File or FileList
117
+ * - select multiple: defaultValue is string array
118
+ * - others: defaultValue is string
119
+ */
120
+ function createDefaultSnapshot(defaultValue, defaultChecked, value) {
121
+ if (typeof value === 'string' || typeof defaultChecked === 'boolean') {
122
+ return {
123
+ value: value !== null && value !== void 0 ? value : 'on',
124
+ checked: defaultChecked
125
+ };
126
+ }
127
+ if (typeof defaultValue === 'string') {
128
+ return {
129
+ value: defaultValue
130
+ };
131
+ }
132
+ if (Array.isArray(defaultValue)) {
133
+ if (defaultValue.every(item => typeof item === 'string')) {
134
+ return {
135
+ options: defaultValue
136
+ };
137
+ } else {
138
+ return {
139
+ files: defaultValue
140
+ };
141
+ }
142
+ }
143
+ if (isGlobalInstance(defaultValue, 'File')) {
144
+ return {
145
+ files: [defaultValue]
146
+ };
147
+ }
148
+ if (isGlobalInstance(defaultValue, 'FileList')) {
149
+ return {
150
+ files: Array.from(defaultValue)
151
+ };
152
+ }
153
+ return {};
154
+ }
155
+
156
+ /**
157
+ * Focuses the first field with validation errors on default form submission.
158
+ * Does nothing if the submission was triggered with a specific intent (e.g. validate / insert)
159
+ */
160
+ function focusFirstInvalidField(ctx) {
161
+ if (ctx.intent) {
162
+ return;
163
+ }
164
+ for (var element of ctx.formElement.elements) {
165
+ var _ctx$error$fieldError;
166
+ if (isFieldElement(element) && (_ctx$error$fieldError = ctx.error.fieldErrors[element.name]) !== null && _ctx$error$fieldError !== void 0 && _ctx$error$fieldError.length) {
167
+ element.focus();
168
+ break;
169
+ }
170
+ }
171
+ }
172
+ function updateFormValue(form, intendedValue, serialize) {
173
+ for (var element of form.elements) {
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
+ });
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Creates a proxy that dynamically generates intent dispatch functions.
188
+ * Each property access returns a function that submits the intent to the form.
189
+ */
190
+ function createIntentDispatcher(formElement, intentName) {
191
+ return new Proxy({}, {
192
+ get(target, type, receiver) {
193
+ if (typeof type === 'string') {
194
+ var _target$type;
195
+ // @ts-expect-error
196
+ (_target$type = target[type]) !== null && _target$type !== void 0 ? _target$type : target[type] = payload => {
197
+ var form = typeof formElement === 'function' ? formElement() : formElement;
198
+ if (!form) {
199
+ throw new Error("Dispatching \"".concat(type, "\" intent failed; No form element found."));
200
+ }
201
+ requestIntent(form, intentName, serializeIntent({
202
+ type,
203
+ payload
204
+ }));
205
+ };
206
+ }
207
+ return Reflect.get(target, type, receiver);
208
+ }
209
+ });
210
+ }
211
+
212
+ export { createDefaultSnapshot, createIntentDispatcher, focusFirstInvalidField, getCheckboxGroupValue, getFormElement, getInputSnapshot, getRadioGroupValue, getSubmitEvent, initializeField, makeInputFocusable, updateFormValue };