@conform-to/dom 1.7.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/dom.js +30 -2
- package/dist/dom.mjs +30 -2
- package/dist/formdata.d.ts +136 -1
- package/dist/formdata.js +190 -21
- package/dist/formdata.mjs +188 -22
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -0
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
╚══════╝ ╚═════╝ ╚═╝ ╚══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
Version 1.
|
|
10
|
+
Version 1.8.0 / License MIT / Copyright (c) 2024 Edmund Hung
|
|
11
11
|
|
|
12
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.
|
|
13
13
|
|
package/dist/dom.js
CHANGED
|
@@ -121,6 +121,8 @@ function createGlobalFormsObserver() {
|
|
|
121
121
|
observer.observe(document.body, {
|
|
122
122
|
subtree: true,
|
|
123
123
|
childList: true,
|
|
124
|
+
attributes: true,
|
|
125
|
+
attributeOldValue: true,
|
|
124
126
|
attributeFilter: ['form', 'name', 'data-conform']
|
|
125
127
|
});
|
|
126
128
|
document.addEventListener('input', handleInput);
|
|
@@ -187,6 +189,15 @@ function createGlobalFormsObserver() {
|
|
|
187
189
|
}));
|
|
188
190
|
}
|
|
189
191
|
}
|
|
192
|
+
function getAssociatedFormElement(formId, node) {
|
|
193
|
+
if (formId !== null) {
|
|
194
|
+
return document.forms.namedItem(formId);
|
|
195
|
+
}
|
|
196
|
+
if (node instanceof Element) {
|
|
197
|
+
return node.closest('form');
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
190
201
|
function handleMutation(mutations) {
|
|
191
202
|
var seenForms = new Set();
|
|
192
203
|
var seenInputs = new Set();
|
|
@@ -196,16 +207,27 @@ function createGlobalFormsObserver() {
|
|
|
196
207
|
}
|
|
197
208
|
return node instanceof Element ? Array.from(node.querySelectorAll('input,select,textarea')) : [];
|
|
198
209
|
};
|
|
210
|
+
var collectForms = node => {
|
|
211
|
+
if (node instanceof HTMLFormElement) {
|
|
212
|
+
return [node];
|
|
213
|
+
}
|
|
214
|
+
return node instanceof Element ? Array.from(node.querySelectorAll('form')) : [];
|
|
215
|
+
};
|
|
199
216
|
for (var mutation of mutations) {
|
|
200
217
|
switch (mutation.type) {
|
|
201
218
|
case 'childList':
|
|
202
219
|
{
|
|
203
220
|
var nodes = [...mutation.addedNodes, ...mutation.removedNodes];
|
|
204
221
|
for (var node of nodes) {
|
|
222
|
+
for (var form of collectForms(node)) {
|
|
223
|
+
seenForms.add(form);
|
|
224
|
+
}
|
|
205
225
|
for (var input of collectInputs(node)) {
|
|
226
|
+
var _input$form;
|
|
206
227
|
seenInputs.add(input);
|
|
207
|
-
|
|
208
|
-
|
|
228
|
+
var _form = (_input$form = input.form) !== null && _input$form !== void 0 ? _input$form : getAssociatedFormElement(input.getAttribute('form'), mutation.target);
|
|
229
|
+
if (_form) {
|
|
230
|
+
seenForms.add(_form);
|
|
209
231
|
}
|
|
210
232
|
}
|
|
211
233
|
}
|
|
@@ -218,6 +240,12 @@ function createGlobalFormsObserver() {
|
|
|
218
240
|
if (mutation.target.form) {
|
|
219
241
|
seenForms.add(mutation.target.form);
|
|
220
242
|
}
|
|
243
|
+
if (mutation.attributeName === 'form') {
|
|
244
|
+
var oldForm = getAssociatedFormElement(mutation.oldValue, mutation.target);
|
|
245
|
+
if (oldForm) {
|
|
246
|
+
seenForms.add(oldForm);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
221
249
|
}
|
|
222
250
|
break;
|
|
223
251
|
}
|
package/dist/dom.mjs
CHANGED
|
@@ -117,6 +117,8 @@ function createGlobalFormsObserver() {
|
|
|
117
117
|
observer.observe(document.body, {
|
|
118
118
|
subtree: true,
|
|
119
119
|
childList: true,
|
|
120
|
+
attributes: true,
|
|
121
|
+
attributeOldValue: true,
|
|
120
122
|
attributeFilter: ['form', 'name', 'data-conform']
|
|
121
123
|
});
|
|
122
124
|
document.addEventListener('input', handleInput);
|
|
@@ -183,6 +185,15 @@ function createGlobalFormsObserver() {
|
|
|
183
185
|
}));
|
|
184
186
|
}
|
|
185
187
|
}
|
|
188
|
+
function getAssociatedFormElement(formId, node) {
|
|
189
|
+
if (formId !== null) {
|
|
190
|
+
return document.forms.namedItem(formId);
|
|
191
|
+
}
|
|
192
|
+
if (node instanceof Element) {
|
|
193
|
+
return node.closest('form');
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
186
197
|
function handleMutation(mutations) {
|
|
187
198
|
var seenForms = new Set();
|
|
188
199
|
var seenInputs = new Set();
|
|
@@ -192,16 +203,27 @@ function createGlobalFormsObserver() {
|
|
|
192
203
|
}
|
|
193
204
|
return node instanceof Element ? Array.from(node.querySelectorAll('input,select,textarea')) : [];
|
|
194
205
|
};
|
|
206
|
+
var collectForms = node => {
|
|
207
|
+
if (node instanceof HTMLFormElement) {
|
|
208
|
+
return [node];
|
|
209
|
+
}
|
|
210
|
+
return node instanceof Element ? Array.from(node.querySelectorAll('form')) : [];
|
|
211
|
+
};
|
|
195
212
|
for (var mutation of mutations) {
|
|
196
213
|
switch (mutation.type) {
|
|
197
214
|
case 'childList':
|
|
198
215
|
{
|
|
199
216
|
var nodes = [...mutation.addedNodes, ...mutation.removedNodes];
|
|
200
217
|
for (var node of nodes) {
|
|
218
|
+
for (var form of collectForms(node)) {
|
|
219
|
+
seenForms.add(form);
|
|
220
|
+
}
|
|
201
221
|
for (var input of collectInputs(node)) {
|
|
222
|
+
var _input$form;
|
|
202
223
|
seenInputs.add(input);
|
|
203
|
-
|
|
204
|
-
|
|
224
|
+
var _form = (_input$form = input.form) !== null && _input$form !== void 0 ? _input$form : getAssociatedFormElement(input.getAttribute('form'), mutation.target);
|
|
225
|
+
if (_form) {
|
|
226
|
+
seenForms.add(_form);
|
|
205
227
|
}
|
|
206
228
|
}
|
|
207
229
|
}
|
|
@@ -214,6 +236,12 @@ function createGlobalFormsObserver() {
|
|
|
214
236
|
if (mutation.target.form) {
|
|
215
237
|
seenForms.add(mutation.target.form);
|
|
216
238
|
}
|
|
239
|
+
if (mutation.attributeName === 'form') {
|
|
240
|
+
var oldForm = getAssociatedFormElement(mutation.oldValue, mutation.target);
|
|
241
|
+
if (oldForm) {
|
|
242
|
+
seenForms.add(oldForm);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
217
245
|
}
|
|
218
246
|
break;
|
|
219
247
|
}
|
package/dist/formdata.d.ts
CHANGED
|
@@ -64,6 +64,141 @@ export declare function flatten(data: unknown, options?: {
|
|
|
64
64
|
resolve?: (data: unknown) => unknown;
|
|
65
65
|
prefix?: string;
|
|
66
66
|
}): Record<string, unknown>;
|
|
67
|
-
export declare function deepEqual
|
|
67
|
+
export declare function deepEqual(left: unknown, right: unknown): boolean;
|
|
68
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
69
|
+
/**
|
|
70
|
+
* The form value of a submission. This is usually constructed from a FormData or URLSearchParams.
|
|
71
|
+
* It may contains JSON primitives if the value is updated based on a form intent.
|
|
72
|
+
*/
|
|
73
|
+
export type FormValue<Type extends JsonPrimitive | FormDataEntryValue = JsonPrimitive | FormDataEntryValue> = Type | FormValue<Type | null>[] | {
|
|
74
|
+
[key: string]: FormValue<Type>;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* The data of a form submission.
|
|
78
|
+
*/
|
|
79
|
+
export type Submission<ValueType extends FormDataEntryValue = FormDataEntryValue> = {
|
|
80
|
+
/**
|
|
81
|
+
* The form value structured following the naming convention.
|
|
82
|
+
*/
|
|
83
|
+
value: Record<string, FormValue<ValueType>>;
|
|
84
|
+
/**
|
|
85
|
+
* The field names that are included in the FormData or URLSearchParams.
|
|
86
|
+
*/
|
|
87
|
+
fields: string[];
|
|
88
|
+
/**
|
|
89
|
+
* The intent of the submission. This is usally included by specifying a name and value on a submit button.
|
|
90
|
+
*/
|
|
91
|
+
intent: string | null;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Parse `FormData` or `URLSearchParams` into a submission object.
|
|
95
|
+
* This function structures the form values based on the naming convention.
|
|
96
|
+
* It also includes all the field names and the intent if the `intentName` option is provided.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* const formData = new FormData();
|
|
101
|
+
*
|
|
102
|
+
* formData.append('email', 'test@example.com');
|
|
103
|
+
* formData.append('password', 'secret');
|
|
104
|
+
*
|
|
105
|
+
* parseSubmission(formData)
|
|
106
|
+
* // {
|
|
107
|
+
* // value: { email: 'test@example.com', password: 'secret' },
|
|
108
|
+
* // fields: ['email', 'password'],
|
|
109
|
+
* // intent: null,
|
|
110
|
+
* // }
|
|
111
|
+
*
|
|
112
|
+
* // If you have an intent field
|
|
113
|
+
* formData.append('intent', 'login');
|
|
114
|
+
* parseSubmission(formData, { intentName: 'intent' })
|
|
115
|
+
* // {
|
|
116
|
+
* // value: { email: 'test@example.com', password: 'secret' },
|
|
117
|
+
* // fields: ['email', 'password'],
|
|
118
|
+
* // intent: 'login',
|
|
119
|
+
* // }
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export declare function parseSubmission(formData: FormData | URLSearchParams, options?: {
|
|
123
|
+
/**
|
|
124
|
+
* The name of the submit button that triggered the form submission.
|
|
125
|
+
* Used to extract the submission's intent.
|
|
126
|
+
*/
|
|
127
|
+
intentName?: string;
|
|
128
|
+
/**
|
|
129
|
+
* A filter function that excludes specific entries from being parsed.
|
|
130
|
+
* Return `true` to skip the entry.
|
|
131
|
+
*/
|
|
132
|
+
skipEntry?: (name: string) => boolean;
|
|
133
|
+
}): Submission;
|
|
134
|
+
export type ParseSubmissionOptions = Required<Parameters<typeof parseSubmission>>[1];
|
|
135
|
+
export declare function defaultSerialize(value: unknown): FormDataEntryValue | undefined;
|
|
136
|
+
/**
|
|
137
|
+
* A utility function that checks whether the current form data differs from the default values.
|
|
138
|
+
*
|
|
139
|
+
* @see https://conform.guide/api/react/future/isDirty
|
|
140
|
+
* @example Enable a submit button only if the form is dirty
|
|
141
|
+
*
|
|
142
|
+
* ```tsx
|
|
143
|
+
* const dirty = useFormData(
|
|
144
|
+
* formRef,
|
|
145
|
+
* (formData) => isDirty(formData, { defaultValue }) ?? false,
|
|
146
|
+
* );
|
|
147
|
+
*
|
|
148
|
+
* return (
|
|
149
|
+
* <button type="submit" disabled={!dirty}>
|
|
150
|
+
* Save changes
|
|
151
|
+
* </button>
|
|
152
|
+
* );
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export declare function isDirty(
|
|
156
|
+
/**
|
|
157
|
+
* The current form data to compare. It can be:
|
|
158
|
+
*
|
|
159
|
+
* - A `FormData` object
|
|
160
|
+
* - A `URLSearchParams` object
|
|
161
|
+
* - A plain object that was parsed from form data (i.e. `submission.payload`)
|
|
162
|
+
*/
|
|
163
|
+
formData: FormData | URLSearchParams | FormValue<FormDataEntryValue> | null, options?: {
|
|
164
|
+
/**
|
|
165
|
+
* An object representing the default values of the form to compare against.
|
|
166
|
+
* Defaults to an empty object if not provided.
|
|
167
|
+
*/
|
|
168
|
+
defaultValue?: unknown;
|
|
169
|
+
/**
|
|
170
|
+
* The name of the submit button that triggered the submission.
|
|
171
|
+
* It will be excluded from the dirty comparison.
|
|
172
|
+
*/
|
|
173
|
+
intentName?: string;
|
|
174
|
+
/**
|
|
175
|
+
* A function to serialize values in defaultValue before comparing them to the form data.
|
|
176
|
+
* If not provided, a default serializer is used that behaves as follows:
|
|
177
|
+
*
|
|
178
|
+
* - string / File:
|
|
179
|
+
* - Returned as-is
|
|
180
|
+
* - boolean:
|
|
181
|
+
* - true → 'on'
|
|
182
|
+
* - false → undefined
|
|
183
|
+
* - number / bigint:
|
|
184
|
+
* - Converted to string using `.toString()`
|
|
185
|
+
* - Date:
|
|
186
|
+
* - Converted to ISO string using `.toISOString()`
|
|
187
|
+
*/
|
|
188
|
+
serialize?: (value: unknown, defaultSerialize: (value: unknown) => FormDataEntryValue | undefined) => FormDataEntryValue | undefined;
|
|
189
|
+
/**
|
|
190
|
+
* A function to exclude specific fields from the comparison.
|
|
191
|
+
* Useful for ignoring hidden inputs like CSRF tokens or internal fields added by frameworks
|
|
192
|
+
* (e.g. Next.js uses hidden inputs to support server actions).
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```ts
|
|
196
|
+
* isDirty(formData, {
|
|
197
|
+
* skipEntry: (name) => name === 'csrf-token',
|
|
198
|
+
* });
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
skipEntry?: (name: string) => boolean;
|
|
202
|
+
}): boolean | undefined;
|
|
68
203
|
export {};
|
|
69
204
|
//# sourceMappingURL=formdata.d.ts.map
|
package/dist/formdata.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
+
var submission = require('./submission.js');
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Construct a form data with the submitter value.
|
|
7
9
|
* It utilizes the submitter argument on the FormData constructor from modern browsers
|
|
@@ -104,11 +106,11 @@ function setValue(target, name, valueFn) {
|
|
|
104
106
|
var index = -1;
|
|
105
107
|
var pointer = target;
|
|
106
108
|
while (pointer != null && ++index < length) {
|
|
107
|
-
var
|
|
109
|
+
var _key = paths[index];
|
|
108
110
|
var nextKey = paths[index + 1];
|
|
109
|
-
var newValue = index != lastIndex ? Object.prototype.hasOwnProperty.call(pointer,
|
|
110
|
-
pointer[
|
|
111
|
-
pointer = pointer[
|
|
111
|
+
var newValue = index != lastIndex ? Object.prototype.hasOwnProperty.call(pointer, _key) && pointer[_key] !== null ? pointer[_key] : typeof nextKey === 'number' ? [] : {} : valueFn(pointer[_key]);
|
|
112
|
+
pointer[_key] = newValue;
|
|
113
|
+
pointer = pointer[_key];
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
116
|
|
|
@@ -195,8 +197,8 @@ function flatten(data) {
|
|
|
195
197
|
process(data[i], "".concat(prefix, "[").concat(i, "]"));
|
|
196
198
|
}
|
|
197
199
|
} else if (isPlainObject(data)) {
|
|
198
|
-
for (var [
|
|
199
|
-
process(_value, prefix ? "".concat(prefix, ".").concat(
|
|
200
|
+
for (var [_key2, _value] of Object.entries(data)) {
|
|
201
|
+
process(_value, prefix ? "".concat(prefix, ".").concat(_key2) : _key2);
|
|
200
202
|
}
|
|
201
203
|
}
|
|
202
204
|
}
|
|
@@ -206,34 +208,36 @@ function flatten(data) {
|
|
|
206
208
|
}
|
|
207
209
|
return result;
|
|
208
210
|
}
|
|
209
|
-
function deepEqual(
|
|
210
|
-
if (
|
|
211
|
+
function deepEqual(left, right) {
|
|
212
|
+
if (Object.is(left, right)) {
|
|
211
213
|
return true;
|
|
212
214
|
}
|
|
213
|
-
if (
|
|
215
|
+
if (left == null || right == null) {
|
|
214
216
|
return false;
|
|
215
217
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
+
|
|
219
|
+
// Compare plain objects
|
|
220
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
221
|
+
var prevKeys = Object.keys(left);
|
|
222
|
+
var nextKeys = Object.keys(right);
|
|
223
|
+
if (prevKeys.length !== nextKeys.length) {
|
|
218
224
|
return false;
|
|
219
225
|
}
|
|
220
|
-
for (var
|
|
221
|
-
if (!deepEqual(
|
|
226
|
+
for (var _key3 of prevKeys) {
|
|
227
|
+
if (!Object.prototype.hasOwnProperty.call(right, _key3) || !deepEqual(left[_key3], right[_key3])) {
|
|
222
228
|
return false;
|
|
223
229
|
}
|
|
224
230
|
}
|
|
225
231
|
return true;
|
|
226
232
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (
|
|
233
|
+
|
|
234
|
+
// Compare arrays
|
|
235
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
236
|
+
if (left.length !== right.length) {
|
|
231
237
|
return false;
|
|
232
238
|
}
|
|
233
|
-
for (var
|
|
234
|
-
if (!
|
|
235
|
-
// @ts-expect-error FIXME
|
|
236
|
-
!deepEqual(prev[key], next[key])) {
|
|
239
|
+
for (var i = 0; i < left.length; i++) {
|
|
240
|
+
if (!deepEqual(left[i], right[i])) {
|
|
237
241
|
return false;
|
|
238
242
|
}
|
|
239
243
|
}
|
|
@@ -242,7 +246,170 @@ function deepEqual(prev, next) {
|
|
|
242
246
|
return false;
|
|
243
247
|
}
|
|
244
248
|
|
|
249
|
+
/**
|
|
250
|
+
* The form value of a submission. This is usually constructed from a FormData or URLSearchParams.
|
|
251
|
+
* It may contains JSON primitives if the value is updated based on a form intent.
|
|
252
|
+
*/
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The data of a form submission.
|
|
256
|
+
*/
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Parse `FormData` or `URLSearchParams` into a submission object.
|
|
260
|
+
* This function structures the form values based on the naming convention.
|
|
261
|
+
* It also includes all the field names and the intent if the `intentName` option is provided.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```ts
|
|
265
|
+
* const formData = new FormData();
|
|
266
|
+
*
|
|
267
|
+
* formData.append('email', 'test@example.com');
|
|
268
|
+
* formData.append('password', 'secret');
|
|
269
|
+
*
|
|
270
|
+
* parseSubmission(formData)
|
|
271
|
+
* // {
|
|
272
|
+
* // value: { email: 'test@example.com', password: 'secret' },
|
|
273
|
+
* // fields: ['email', 'password'],
|
|
274
|
+
* // intent: null,
|
|
275
|
+
* // }
|
|
276
|
+
*
|
|
277
|
+
* // If you have an intent field
|
|
278
|
+
* formData.append('intent', 'login');
|
|
279
|
+
* parseSubmission(formData, { intentName: 'intent' })
|
|
280
|
+
* // {
|
|
281
|
+
* // value: { email: 'test@example.com', password: 'secret' },
|
|
282
|
+
* // fields: ['email', 'password'],
|
|
283
|
+
* // intent: 'login',
|
|
284
|
+
* // }
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
function parseSubmission(formData, options) {
|
|
288
|
+
var _options$intentName;
|
|
289
|
+
var intentName = (_options$intentName = options === null || options === void 0 ? void 0 : options.intentName) !== null && _options$intentName !== void 0 ? _options$intentName : submission.INTENT;
|
|
290
|
+
var submission$1 = {
|
|
291
|
+
value: {},
|
|
292
|
+
fields: [],
|
|
293
|
+
intent: null
|
|
294
|
+
};
|
|
295
|
+
var _loop = function _loop() {
|
|
296
|
+
var _options$skipEntry;
|
|
297
|
+
if (_name !== intentName && !(options !== null && options !== void 0 && (_options$skipEntry = options.skipEntry) !== null && _options$skipEntry !== void 0 && _options$skipEntry.call(options, _name))) {
|
|
298
|
+
var _value2 = formData.getAll(_name);
|
|
299
|
+
setValue(submission$1.value, _name, () => _value2.length > 1 ? _value2 : _value2[0]);
|
|
300
|
+
submission$1.fields.push(_name);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
for (var _name of new Set(formData.keys())) {
|
|
304
|
+
_loop();
|
|
305
|
+
}
|
|
306
|
+
if (intentName) {
|
|
307
|
+
// We take the first value of the intent field if it exists.
|
|
308
|
+
var intent = formData.get(intentName);
|
|
309
|
+
if (typeof intent === 'string') {
|
|
310
|
+
submission$1.intent = intent;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return submission$1;
|
|
314
|
+
}
|
|
315
|
+
function defaultSerialize(value) {
|
|
316
|
+
if (typeof value === 'string' || isGlobalInstance(value, 'File')) {
|
|
317
|
+
return value;
|
|
318
|
+
}
|
|
319
|
+
if (typeof value === 'boolean') {
|
|
320
|
+
return value ? 'on' : undefined;
|
|
321
|
+
}
|
|
322
|
+
if (value instanceof Date) {
|
|
323
|
+
return value.toISOString();
|
|
324
|
+
}
|
|
325
|
+
return value === null || value === void 0 ? void 0 : value.toString();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* A utility function that checks whether the current form data differs from the default values.
|
|
330
|
+
*
|
|
331
|
+
* @see https://conform.guide/api/react/future/isDirty
|
|
332
|
+
* @example Enable a submit button only if the form is dirty
|
|
333
|
+
*
|
|
334
|
+
* ```tsx
|
|
335
|
+
* const dirty = useFormData(
|
|
336
|
+
* formRef,
|
|
337
|
+
* (formData) => isDirty(formData, { defaultValue }) ?? false,
|
|
338
|
+
* );
|
|
339
|
+
*
|
|
340
|
+
* return (
|
|
341
|
+
* <button type="submit" disabled={!dirty}>
|
|
342
|
+
* Save changes
|
|
343
|
+
* </button>
|
|
344
|
+
* );
|
|
345
|
+
* ```
|
|
346
|
+
*/
|
|
347
|
+
function isDirty(
|
|
348
|
+
/**
|
|
349
|
+
* The current form data to compare. It can be:
|
|
350
|
+
*
|
|
351
|
+
* - A `FormData` object
|
|
352
|
+
* - A `URLSearchParams` object
|
|
353
|
+
* - A plain object that was parsed from form data (i.e. `submission.payload`)
|
|
354
|
+
*/
|
|
355
|
+
formData, options) {
|
|
356
|
+
var _options$serialize;
|
|
357
|
+
if (!formData) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
var formValue = formData instanceof FormData || formData instanceof URLSearchParams ? parseSubmission(formData, {
|
|
361
|
+
intentName: options === null || options === void 0 ? void 0 : options.intentName,
|
|
362
|
+
skipEntry: options === null || options === void 0 ? void 0 : options.skipEntry
|
|
363
|
+
}).value : formData;
|
|
364
|
+
var defaultValue = options === null || options === void 0 ? void 0 : options.defaultValue;
|
|
365
|
+
var serialize = (_options$serialize = options === null || options === void 0 ? void 0 : options.serialize) !== null && _options$serialize !== void 0 ? _options$serialize : defaultSerialize;
|
|
366
|
+
function normalize(value) {
|
|
367
|
+
if (Array.isArray(value)) {
|
|
368
|
+
if (value.length === 0) {
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
var array = value.map(normalize);
|
|
372
|
+
if (array.length === 1 && (typeof array[0] === 'string' || array[0] === undefined)) {
|
|
373
|
+
return array[0];
|
|
374
|
+
}
|
|
375
|
+
return array;
|
|
376
|
+
}
|
|
377
|
+
if (isPlainObject(value)) {
|
|
378
|
+
var entries = Object.entries(value).reduce((list, _ref) => {
|
|
379
|
+
var [key, value] = _ref;
|
|
380
|
+
var normalizedValue = normalize(value);
|
|
381
|
+
if (typeof normalizedValue !== 'undefined') {
|
|
382
|
+
list.push([key, normalizedValue]);
|
|
383
|
+
}
|
|
384
|
+
return list;
|
|
385
|
+
}, []);
|
|
386
|
+
if (entries.length === 0) {
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
return Object.fromEntries(entries);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// If the value is null or undefined, treat it as undefined
|
|
393
|
+
if (value == null) {
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Removes empty strings, so that bpth empty string and undefined are treated as the same
|
|
398
|
+
if (typeof value === 'string' && value === '') {
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Remove empty File as well, which happens if no File was selected
|
|
403
|
+
if (isGlobalInstance(value, 'File') && value.name === '' && value.size === 0) {
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
return serialize(value, defaultSerialize);
|
|
407
|
+
}
|
|
408
|
+
return !deepEqual(normalize(formValue), normalize(defaultValue));
|
|
409
|
+
}
|
|
410
|
+
|
|
245
411
|
exports.deepEqual = deepEqual;
|
|
412
|
+
exports.defaultSerialize = defaultSerialize;
|
|
246
413
|
exports.flatten = flatten;
|
|
247
414
|
exports.formatName = formatName;
|
|
248
415
|
exports.formatPaths = formatPaths;
|
|
@@ -250,8 +417,10 @@ exports.getChildPaths = getChildPaths;
|
|
|
250
417
|
exports.getFormData = getFormData;
|
|
251
418
|
exports.getPaths = getPaths;
|
|
252
419
|
exports.getValue = getValue;
|
|
420
|
+
exports.isDirty = isDirty;
|
|
253
421
|
exports.isGlobalInstance = isGlobalInstance;
|
|
254
422
|
exports.isPlainObject = isPlainObject;
|
|
255
423
|
exports.isPrefix = isPrefix;
|
|
256
424
|
exports.normalize = normalize;
|
|
425
|
+
exports.parseSubmission = parseSubmission;
|
|
257
426
|
exports.setValue = setValue;
|
package/dist/formdata.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { INTENT } from './submission.mjs';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Construct a form data with the submitter value.
|
|
3
5
|
* It utilizes the submitter argument on the FormData constructor from modern browsers
|
|
@@ -100,11 +102,11 @@ function setValue(target, name, valueFn) {
|
|
|
100
102
|
var index = -1;
|
|
101
103
|
var pointer = target;
|
|
102
104
|
while (pointer != null && ++index < length) {
|
|
103
|
-
var
|
|
105
|
+
var _key = paths[index];
|
|
104
106
|
var nextKey = paths[index + 1];
|
|
105
|
-
var newValue = index != lastIndex ? Object.prototype.hasOwnProperty.call(pointer,
|
|
106
|
-
pointer[
|
|
107
|
-
pointer = pointer[
|
|
107
|
+
var newValue = index != lastIndex ? Object.prototype.hasOwnProperty.call(pointer, _key) && pointer[_key] !== null ? pointer[_key] : typeof nextKey === 'number' ? [] : {} : valueFn(pointer[_key]);
|
|
108
|
+
pointer[_key] = newValue;
|
|
109
|
+
pointer = pointer[_key];
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
|
|
@@ -191,8 +193,8 @@ function flatten(data) {
|
|
|
191
193
|
process(data[i], "".concat(prefix, "[").concat(i, "]"));
|
|
192
194
|
}
|
|
193
195
|
} else if (isPlainObject(data)) {
|
|
194
|
-
for (var [
|
|
195
|
-
process(_value, prefix ? "".concat(prefix, ".").concat(
|
|
196
|
+
for (var [_key2, _value] of Object.entries(data)) {
|
|
197
|
+
process(_value, prefix ? "".concat(prefix, ".").concat(_key2) : _key2);
|
|
196
198
|
}
|
|
197
199
|
}
|
|
198
200
|
}
|
|
@@ -202,34 +204,36 @@ function flatten(data) {
|
|
|
202
204
|
}
|
|
203
205
|
return result;
|
|
204
206
|
}
|
|
205
|
-
function deepEqual(
|
|
206
|
-
if (
|
|
207
|
+
function deepEqual(left, right) {
|
|
208
|
+
if (Object.is(left, right)) {
|
|
207
209
|
return true;
|
|
208
210
|
}
|
|
209
|
-
if (
|
|
211
|
+
if (left == null || right == null) {
|
|
210
212
|
return false;
|
|
211
213
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
|
|
215
|
+
// Compare plain objects
|
|
216
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
217
|
+
var prevKeys = Object.keys(left);
|
|
218
|
+
var nextKeys = Object.keys(right);
|
|
219
|
+
if (prevKeys.length !== nextKeys.length) {
|
|
214
220
|
return false;
|
|
215
221
|
}
|
|
216
|
-
for (var
|
|
217
|
-
if (!deepEqual(
|
|
222
|
+
for (var _key3 of prevKeys) {
|
|
223
|
+
if (!Object.prototype.hasOwnProperty.call(right, _key3) || !deepEqual(left[_key3], right[_key3])) {
|
|
218
224
|
return false;
|
|
219
225
|
}
|
|
220
226
|
}
|
|
221
227
|
return true;
|
|
222
228
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
229
|
+
|
|
230
|
+
// Compare arrays
|
|
231
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
232
|
+
if (left.length !== right.length) {
|
|
227
233
|
return false;
|
|
228
234
|
}
|
|
229
|
-
for (var
|
|
230
|
-
if (!
|
|
231
|
-
// @ts-expect-error FIXME
|
|
232
|
-
!deepEqual(prev[key], next[key])) {
|
|
235
|
+
for (var i = 0; i < left.length; i++) {
|
|
236
|
+
if (!deepEqual(left[i], right[i])) {
|
|
233
237
|
return false;
|
|
234
238
|
}
|
|
235
239
|
}
|
|
@@ -238,4 +242,166 @@ function deepEqual(prev, next) {
|
|
|
238
242
|
return false;
|
|
239
243
|
}
|
|
240
244
|
|
|
241
|
-
|
|
245
|
+
/**
|
|
246
|
+
* The form value of a submission. This is usually constructed from a FormData or URLSearchParams.
|
|
247
|
+
* It may contains JSON primitives if the value is updated based on a form intent.
|
|
248
|
+
*/
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The data of a form submission.
|
|
252
|
+
*/
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Parse `FormData` or `URLSearchParams` into a submission object.
|
|
256
|
+
* This function structures the form values based on the naming convention.
|
|
257
|
+
* It also includes all the field names and the intent if the `intentName` option is provided.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```ts
|
|
261
|
+
* const formData = new FormData();
|
|
262
|
+
*
|
|
263
|
+
* formData.append('email', 'test@example.com');
|
|
264
|
+
* formData.append('password', 'secret');
|
|
265
|
+
*
|
|
266
|
+
* parseSubmission(formData)
|
|
267
|
+
* // {
|
|
268
|
+
* // value: { email: 'test@example.com', password: 'secret' },
|
|
269
|
+
* // fields: ['email', 'password'],
|
|
270
|
+
* // intent: null,
|
|
271
|
+
* // }
|
|
272
|
+
*
|
|
273
|
+
* // If you have an intent field
|
|
274
|
+
* formData.append('intent', 'login');
|
|
275
|
+
* parseSubmission(formData, { intentName: 'intent' })
|
|
276
|
+
* // {
|
|
277
|
+
* // value: { email: 'test@example.com', password: 'secret' },
|
|
278
|
+
* // fields: ['email', 'password'],
|
|
279
|
+
* // intent: 'login',
|
|
280
|
+
* // }
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
function parseSubmission(formData, options) {
|
|
284
|
+
var _options$intentName;
|
|
285
|
+
var intentName = (_options$intentName = options === null || options === void 0 ? void 0 : options.intentName) !== null && _options$intentName !== void 0 ? _options$intentName : INTENT;
|
|
286
|
+
var submission = {
|
|
287
|
+
value: {},
|
|
288
|
+
fields: [],
|
|
289
|
+
intent: null
|
|
290
|
+
};
|
|
291
|
+
var _loop = function _loop() {
|
|
292
|
+
var _options$skipEntry;
|
|
293
|
+
if (_name !== intentName && !(options !== null && options !== void 0 && (_options$skipEntry = options.skipEntry) !== null && _options$skipEntry !== void 0 && _options$skipEntry.call(options, _name))) {
|
|
294
|
+
var _value2 = formData.getAll(_name);
|
|
295
|
+
setValue(submission.value, _name, () => _value2.length > 1 ? _value2 : _value2[0]);
|
|
296
|
+
submission.fields.push(_name);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
for (var _name of new Set(formData.keys())) {
|
|
300
|
+
_loop();
|
|
301
|
+
}
|
|
302
|
+
if (intentName) {
|
|
303
|
+
// We take the first value of the intent field if it exists.
|
|
304
|
+
var intent = formData.get(intentName);
|
|
305
|
+
if (typeof intent === 'string') {
|
|
306
|
+
submission.intent = intent;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return submission;
|
|
310
|
+
}
|
|
311
|
+
function defaultSerialize(value) {
|
|
312
|
+
if (typeof value === 'string' || isGlobalInstance(value, 'File')) {
|
|
313
|
+
return value;
|
|
314
|
+
}
|
|
315
|
+
if (typeof value === 'boolean') {
|
|
316
|
+
return value ? 'on' : undefined;
|
|
317
|
+
}
|
|
318
|
+
if (value instanceof Date) {
|
|
319
|
+
return value.toISOString();
|
|
320
|
+
}
|
|
321
|
+
return value === null || value === void 0 ? void 0 : value.toString();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* A utility function that checks whether the current form data differs from the default values.
|
|
326
|
+
*
|
|
327
|
+
* @see https://conform.guide/api/react/future/isDirty
|
|
328
|
+
* @example Enable a submit button only if the form is dirty
|
|
329
|
+
*
|
|
330
|
+
* ```tsx
|
|
331
|
+
* const dirty = useFormData(
|
|
332
|
+
* formRef,
|
|
333
|
+
* (formData) => isDirty(formData, { defaultValue }) ?? false,
|
|
334
|
+
* );
|
|
335
|
+
*
|
|
336
|
+
* return (
|
|
337
|
+
* <button type="submit" disabled={!dirty}>
|
|
338
|
+
* Save changes
|
|
339
|
+
* </button>
|
|
340
|
+
* );
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
function isDirty(
|
|
344
|
+
/**
|
|
345
|
+
* The current form data to compare. It can be:
|
|
346
|
+
*
|
|
347
|
+
* - A `FormData` object
|
|
348
|
+
* - A `URLSearchParams` object
|
|
349
|
+
* - A plain object that was parsed from form data (i.e. `submission.payload`)
|
|
350
|
+
*/
|
|
351
|
+
formData, options) {
|
|
352
|
+
var _options$serialize;
|
|
353
|
+
if (!formData) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
var formValue = formData instanceof FormData || formData instanceof URLSearchParams ? parseSubmission(formData, {
|
|
357
|
+
intentName: options === null || options === void 0 ? void 0 : options.intentName,
|
|
358
|
+
skipEntry: options === null || options === void 0 ? void 0 : options.skipEntry
|
|
359
|
+
}).value : formData;
|
|
360
|
+
var defaultValue = options === null || options === void 0 ? void 0 : options.defaultValue;
|
|
361
|
+
var serialize = (_options$serialize = options === null || options === void 0 ? void 0 : options.serialize) !== null && _options$serialize !== void 0 ? _options$serialize : defaultSerialize;
|
|
362
|
+
function normalize(value) {
|
|
363
|
+
if (Array.isArray(value)) {
|
|
364
|
+
if (value.length === 0) {
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
var array = value.map(normalize);
|
|
368
|
+
if (array.length === 1 && (typeof array[0] === 'string' || array[0] === undefined)) {
|
|
369
|
+
return array[0];
|
|
370
|
+
}
|
|
371
|
+
return array;
|
|
372
|
+
}
|
|
373
|
+
if (isPlainObject(value)) {
|
|
374
|
+
var entries = Object.entries(value).reduce((list, _ref) => {
|
|
375
|
+
var [key, value] = _ref;
|
|
376
|
+
var normalizedValue = normalize(value);
|
|
377
|
+
if (typeof normalizedValue !== 'undefined') {
|
|
378
|
+
list.push([key, normalizedValue]);
|
|
379
|
+
}
|
|
380
|
+
return list;
|
|
381
|
+
}, []);
|
|
382
|
+
if (entries.length === 0) {
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
return Object.fromEntries(entries);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// If the value is null or undefined, treat it as undefined
|
|
389
|
+
if (value == null) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Removes empty strings, so that bpth empty string and undefined are treated as the same
|
|
394
|
+
if (typeof value === 'string' && value === '') {
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Remove empty File as well, which happens if no File was selected
|
|
399
|
+
if (isGlobalInstance(value, 'File') && value.name === '' && value.size === 0) {
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
return serialize(value, defaultSerialize);
|
|
403
|
+
}
|
|
404
|
+
return !deepEqual(normalize(formValue), normalize(defaultValue));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export { deepEqual, defaultSerialize, flatten, formatName, formatPaths, getChildPaths, getFormData, getPaths, getValue, isDirty, isGlobalInstance, isPlainObject, isPrefix, normalize, parseSubmission, setValue };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { type Combine, type Constraint, type ControlButtonProps, type FormId, type FieldName, type DefaultValue, type FormValue, type FormOptions, type FormState, type FormContext, type SubscriptionSubject, type SubscriptionScope, createFormContext as unstable_createFormContext, } from './form';
|
|
2
2
|
export { type FieldElement, isFieldElement, updateField as unstable_updateField, createFileList, createGlobalFormsObserver as unstable_createGlobalFormsObserver, focus as unstable_focus, change as unstable_change, blur as unstable_blur, } from './dom';
|
|
3
3
|
export { type Submission, type SubmissionResult, type Intent, INTENT, STATE, serializeIntent, parse, } from './submission';
|
|
4
|
-
export { getPaths, formatPaths, isPrefix, isGlobalInstance, deepEqual as unstable_deepEqual, } from './formdata';
|
|
4
|
+
export { getFormData, getPaths, formatPaths, isPrefix, isGlobalInstance, deepEqual as unstable_deepEqual, isDirty as unstable_isDirty, } from './formdata';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,9 @@ exports.STATE = submission.STATE;
|
|
|
22
22
|
exports.parse = submission.parse;
|
|
23
23
|
exports.serializeIntent = submission.serializeIntent;
|
|
24
24
|
exports.formatPaths = formdata.formatPaths;
|
|
25
|
+
exports.getFormData = formdata.getFormData;
|
|
25
26
|
exports.getPaths = formdata.getPaths;
|
|
26
27
|
exports.isGlobalInstance = formdata.isGlobalInstance;
|
|
27
28
|
exports.isPrefix = formdata.isPrefix;
|
|
28
29
|
exports.unstable_deepEqual = formdata.deepEqual;
|
|
30
|
+
exports.unstable_isDirty = formdata.isDirty;
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { createFormContext as unstable_createFormContext } from './form.mjs';
|
|
2
2
|
export { createFileList, isFieldElement, blur as unstable_blur, change as unstable_change, createGlobalFormsObserver as unstable_createGlobalFormsObserver, focus as unstable_focus, updateField as unstable_updateField } from './dom.mjs';
|
|
3
3
|
export { INTENT, STATE, parse, serializeIntent } from './submission.mjs';
|
|
4
|
-
export { formatPaths, getPaths, isGlobalInstance, isPrefix, deepEqual as unstable_deepEqual } from './formdata.mjs';
|
|
4
|
+
export { formatPaths, getFormData, getPaths, isGlobalInstance, isPrefix, deepEqual as unstable_deepEqual, isDirty as unstable_isDirty } from './formdata.mjs';
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "A set of opinionated helpers built on top of the Constraint Validation API",
|
|
4
4
|
"homepage": "https://conform.guide",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.8.0",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"module": "./dist/index.mjs",
|
|
9
9
|
"types": "./dist/index.d.ts",
|