@conform-to/dom 0.5.1 → 0.6.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 +10 -4
- package/_virtual/_rollupPluginBabelHelpers.js +17 -0
- package/index.d.ts +114 -39
- package/index.js +249 -153
- package/module/_virtual/_rollupPluginBabelHelpers.js +16 -1
- package/module/index.js +239 -146
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
> This package is a transitive dependency for the rest of the conform packages with no intention to be used directly at the moment. Use at your own risk.
|
|
4
4
|
|
|
5
|
-
Conform is a form validation library
|
|
5
|
+
Conform is a progressive enhancement first form validation library for [Remix](https://remix.run)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
7
|
+
### Highlights
|
|
8
|
+
|
|
9
|
+
- Focused on progressive enhancment by default
|
|
10
|
+
- Simplifed intergration through event delegation
|
|
11
|
+
- Server first validation with Zod / Yup schema support
|
|
12
|
+
- Field name inference with type checking
|
|
13
|
+
- Focus management
|
|
14
|
+
- Accessibility support
|
|
15
|
+
- About 5kb compressed
|
|
10
16
|
|
|
11
17
|
Checkout the [repository](https://github.com/edmundhung/conform) if you want to know more!
|
|
@@ -24,6 +24,7 @@ function _objectSpread2(target) {
|
|
|
24
24
|
return target;
|
|
25
25
|
}
|
|
26
26
|
function _defineProperty(obj, key, value) {
|
|
27
|
+
key = _toPropertyKey(key);
|
|
27
28
|
if (key in obj) {
|
|
28
29
|
Object.defineProperty(obj, key, {
|
|
29
30
|
value: value,
|
|
@@ -36,6 +37,22 @@ function _defineProperty(obj, key, value) {
|
|
|
36
37
|
}
|
|
37
38
|
return obj;
|
|
38
39
|
}
|
|
40
|
+
function _toPrimitive(input, hint) {
|
|
41
|
+
if (typeof input !== "object" || input === null) return input;
|
|
42
|
+
var prim = input[Symbol.toPrimitive];
|
|
43
|
+
if (prim !== undefined) {
|
|
44
|
+
var res = prim.call(input, hint || "default");
|
|
45
|
+
if (typeof res !== "object") return res;
|
|
46
|
+
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
47
|
+
}
|
|
48
|
+
return (hint === "string" ? String : Number)(input);
|
|
49
|
+
}
|
|
50
|
+
function _toPropertyKey(arg) {
|
|
51
|
+
var key = _toPrimitive(arg, "string");
|
|
52
|
+
return typeof key === "symbol" ? key : String(key);
|
|
53
|
+
}
|
|
39
54
|
|
|
40
55
|
exports.defineProperty = _defineProperty;
|
|
41
56
|
exports.objectSpread2 = _objectSpread2;
|
|
57
|
+
exports.toPrimitive = _toPrimitive;
|
|
58
|
+
exports.toPropertyKey = _toPropertyKey;
|
package/index.d.ts
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
1
|
+
export type Primitive = null | undefined | string | number | boolean | Date;
|
|
2
|
+
export type FieldElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement;
|
|
3
3
|
export interface FieldConfig<Schema = unknown> extends FieldConstraint<Schema> {
|
|
4
4
|
id?: string;
|
|
5
5
|
name: string;
|
|
6
6
|
defaultValue?: FieldValue<Schema>;
|
|
7
|
-
initialError?:
|
|
7
|
+
initialError?: Record<string, string | string[]>;
|
|
8
8
|
form?: string;
|
|
9
9
|
errorId?: string;
|
|
10
|
+
/**
|
|
11
|
+
* The frist error of the field
|
|
12
|
+
*/
|
|
13
|
+
error?: string;
|
|
14
|
+
/**
|
|
15
|
+
* All of the field errors
|
|
16
|
+
*/
|
|
17
|
+
errors?: string[];
|
|
10
18
|
}
|
|
11
|
-
export
|
|
19
|
+
export type FieldValue<Schema> = Schema extends Primitive ? string : Schema extends File ? File : Schema extends Array<infer InnerType> ? Array<FieldValue<InnerType>> : Schema extends Record<string, any> ? {
|
|
12
20
|
[Key in keyof Schema]?: FieldValue<Schema[Key]>;
|
|
13
|
-
} :
|
|
14
|
-
export
|
|
21
|
+
} : any;
|
|
22
|
+
export type FieldConstraint<Schema = any> = {
|
|
15
23
|
required?: boolean;
|
|
16
24
|
minLength?: number;
|
|
17
25
|
maxLength?: number;
|
|
@@ -21,50 +29,95 @@ export declare type FieldConstraint<Schema = any> = {
|
|
|
21
29
|
multiple?: boolean;
|
|
22
30
|
pattern?: string;
|
|
23
31
|
};
|
|
24
|
-
export
|
|
32
|
+
export type FieldsetConstraint<Schema extends Record<string, any>> = {
|
|
25
33
|
[Key in keyof Schema]?: FieldConstraint<Schema[Key]>;
|
|
26
34
|
};
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
export type Submission<Schema extends Record<string, any> | unknown = unknown> = unknown extends Schema ? {
|
|
36
|
+
intent: string;
|
|
37
|
+
payload: Record<string, any>;
|
|
38
|
+
error: Record<string, string | string[]>;
|
|
39
|
+
} : {
|
|
40
|
+
intent: string;
|
|
41
|
+
payload: Record<string, any>;
|
|
42
|
+
value?: Schema;
|
|
43
|
+
error: Record<string, string | string[]>;
|
|
44
|
+
toJSON(): Submission;
|
|
32
45
|
};
|
|
33
|
-
export interface
|
|
34
|
-
name:
|
|
46
|
+
export interface IntentButtonProps {
|
|
47
|
+
name: typeof INTENT;
|
|
35
48
|
value: string;
|
|
36
49
|
formNoValidate?: boolean;
|
|
37
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Check if the provided reference is a form element (_input_ / _select_ / _textarea_ or _button_)
|
|
53
|
+
*/
|
|
38
54
|
export declare function isFieldElement(element: unknown): element is FieldElement;
|
|
39
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Find the corresponding paths based on the formatted name
|
|
57
|
+
* @param name formatted name
|
|
58
|
+
* @returns paths
|
|
59
|
+
*/
|
|
40
60
|
export declare function getPaths(name: string): Array<string | number>;
|
|
41
61
|
export declare function getFormData(form: HTMLFormElement, submitter?: HTMLInputElement | HTMLButtonElement | null): FormData;
|
|
62
|
+
export type FormMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
63
|
+
export type FormEncType = 'application/x-www-form-urlencoded' | 'multipart/form-data';
|
|
64
|
+
export declare function getFormAttributes(form: HTMLFormElement, submitter?: HTMLInputElement | HTMLButtonElement | null): {
|
|
65
|
+
action: string;
|
|
66
|
+
encType: FormEncType;
|
|
67
|
+
method: FormMethod;
|
|
68
|
+
};
|
|
42
69
|
export declare function getName(paths: Array<string | number>): string;
|
|
43
|
-
export declare function
|
|
44
|
-
export declare function
|
|
70
|
+
export declare function getScope(intent: string): string | null;
|
|
71
|
+
export declare function isFocusedOnIntentButton(form: HTMLFormElement, intent: string): boolean;
|
|
72
|
+
export declare function getValidationMessage(errors?: string | string[]): string;
|
|
73
|
+
export declare function getErrors(message: string | undefined): string[];
|
|
74
|
+
export declare const FORM_ERROR_ELEMENT_NAME = "__form__";
|
|
75
|
+
export declare const INTENT = "__intent__";
|
|
76
|
+
export declare const VALIDATION_UNDEFINED = "__undefined__";
|
|
77
|
+
export declare const VALIDATION_SKIPPED = "__skipped__";
|
|
45
78
|
export declare function reportSubmission(form: HTMLFormElement, submission: Submission): void;
|
|
46
79
|
export declare function setValue<T>(target: any, paths: Array<string | number>, valueFn: (prev?: T) => T): void;
|
|
47
80
|
/**
|
|
48
|
-
*
|
|
49
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/requestSubmit
|
|
50
|
-
* @see https://caniuse.com/?search=requestSubmit
|
|
51
|
-
*/
|
|
52
|
-
export declare function requestSubmit(form: HTMLFormElement, submitter?: HTMLButtonElement | HTMLInputElement): void;
|
|
53
|
-
/**
|
|
54
|
-
* Creates a command button on demand and trigger a form submit by clicking it.
|
|
81
|
+
* Creates an intent button on demand and trigger a form submit by clicking it.
|
|
55
82
|
*/
|
|
56
|
-
export declare function
|
|
83
|
+
export declare function requestIntent(form: HTMLFormElement | undefined, buttonProps: {
|
|
84
|
+
value: string;
|
|
85
|
+
formNoValidate?: boolean;
|
|
86
|
+
}): void;
|
|
57
87
|
/**
|
|
58
|
-
* Returns the properties required to configure
|
|
88
|
+
* Returns the properties required to configure an intent button for validation
|
|
59
89
|
*
|
|
60
90
|
* @see https://conform.guide/api/react#validate
|
|
61
91
|
*/
|
|
62
|
-
export declare function validate(field?: string):
|
|
92
|
+
export declare function validate(field?: string): IntentButtonProps;
|
|
63
93
|
export declare function getFormElement(element: HTMLFormElement | HTMLFieldSetElement | HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement | null): HTMLFormElement | null;
|
|
64
|
-
export declare function
|
|
65
|
-
export declare function
|
|
66
|
-
|
|
67
|
-
|
|
94
|
+
export declare function parse(payload: FormData | URLSearchParams): Submission;
|
|
95
|
+
export declare function parse<Schema>(payload: FormData | URLSearchParams, options?: {
|
|
96
|
+
resolve?: (payload: Record<string, any>, intent: string) => {
|
|
97
|
+
value: Schema;
|
|
98
|
+
} | {
|
|
99
|
+
error: Record<string, string | string[]>;
|
|
100
|
+
};
|
|
101
|
+
}): Submission<Schema>;
|
|
102
|
+
export declare function parse<Schema>(payload: FormData | URLSearchParams, options?: {
|
|
103
|
+
resolve?: (payload: Record<string, any>, intent: string) => Promise<{
|
|
104
|
+
value: Schema;
|
|
105
|
+
} | {
|
|
106
|
+
error: Record<string, string | string[]>;
|
|
107
|
+
}>;
|
|
108
|
+
}): Promise<Submission<Schema>>;
|
|
109
|
+
export declare function parse<Schema>(payload: FormData | URLSearchParams, options?: {
|
|
110
|
+
resolve?: (payload: Record<string, any>, intent: string) => ({
|
|
111
|
+
value: Schema;
|
|
112
|
+
} | {
|
|
113
|
+
error: Record<string, string | string[]>;
|
|
114
|
+
}) | Promise<{
|
|
115
|
+
value: Schema;
|
|
116
|
+
} | {
|
|
117
|
+
error: Record<string, string | string[]>;
|
|
118
|
+
}>;
|
|
119
|
+
}): Submission<Schema> | Promise<Submission<Schema>>;
|
|
120
|
+
export type ListCommand<Schema = unknown> = {
|
|
68
121
|
type: 'prepend';
|
|
69
122
|
scope: string;
|
|
70
123
|
payload: {
|
|
@@ -97,31 +150,53 @@ export declare type ListCommand<Schema = unknown> = {
|
|
|
97
150
|
to: number;
|
|
98
151
|
};
|
|
99
152
|
};
|
|
100
|
-
export declare function parseListCommand<Schema = unknown>(
|
|
153
|
+
export declare function parseListCommand<Schema = unknown>(intent: string): ListCommand<Schema> | null;
|
|
101
154
|
export declare function updateList<Schema>(list: Array<Schema>, command: ListCommand<Schema>): Array<Schema>;
|
|
102
|
-
export declare function handleList<Schema>(submission: Submission<Schema>): Submission<Schema>;
|
|
103
155
|
export interface ListCommandButtonBuilder {
|
|
104
156
|
append<Schema>(name: string, payload?: {
|
|
105
157
|
defaultValue: Schema;
|
|
106
|
-
}):
|
|
158
|
+
}): IntentButtonProps;
|
|
107
159
|
prepend<Schema>(name: string, payload?: {
|
|
108
160
|
defaultValue: Schema;
|
|
109
|
-
}):
|
|
161
|
+
}): IntentButtonProps;
|
|
110
162
|
replace<Schema>(name: string, payload: {
|
|
111
163
|
defaultValue: Schema;
|
|
112
164
|
index: number;
|
|
113
|
-
}):
|
|
165
|
+
}): IntentButtonProps;
|
|
114
166
|
remove(name: string, payload: {
|
|
115
167
|
index: number;
|
|
116
|
-
}):
|
|
168
|
+
}): IntentButtonProps;
|
|
117
169
|
reorder(name: string, payload: {
|
|
118
170
|
from: number;
|
|
119
171
|
to: number;
|
|
120
|
-
}):
|
|
172
|
+
}): IntentButtonProps;
|
|
121
173
|
}
|
|
122
174
|
/**
|
|
123
|
-
* Helpers to configure
|
|
175
|
+
* Helpers to configure an intent button for modifying a list
|
|
124
176
|
*
|
|
125
177
|
* @see https://conform.guide/api/react#list
|
|
126
178
|
*/
|
|
127
179
|
export declare const list: ListCommandButtonBuilder;
|
|
180
|
+
/**
|
|
181
|
+
* Validate the form with the Constraint Validation API
|
|
182
|
+
* @see https://conform.guide/api/react#validateconstraint
|
|
183
|
+
*/
|
|
184
|
+
export declare function validateConstraint(options: {
|
|
185
|
+
form: HTMLFormElement;
|
|
186
|
+
formData?: FormData;
|
|
187
|
+
constraint?: Record<Lowercase<string>, (value: string, context: {
|
|
188
|
+
formData: FormData;
|
|
189
|
+
attributeValue: string;
|
|
190
|
+
}) => boolean>;
|
|
191
|
+
acceptMultipleErrors?: ({ name, intent, payload, }: {
|
|
192
|
+
name: string;
|
|
193
|
+
intent: string;
|
|
194
|
+
payload: Record<string, any>;
|
|
195
|
+
}) => boolean;
|
|
196
|
+
formatMessages?: ({ name, validity, constraint, defaultErrors, }: {
|
|
197
|
+
name: string;
|
|
198
|
+
validity: ValidityState;
|
|
199
|
+
constraint: Record<string, boolean>;
|
|
200
|
+
defaultErrors: string[];
|
|
201
|
+
}) => string[];
|
|
202
|
+
}): Submission;
|
package/index.js
CHANGED
|
@@ -4,12 +4,18 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
4
4
|
|
|
5
5
|
var _rollupPluginBabelHelpers = require('./_virtual/_rollupPluginBabelHelpers.js');
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Check if the provided reference is a form element (_input_ / _select_ / _textarea_ or _button_)
|
|
9
|
+
*/
|
|
7
10
|
function isFieldElement(element) {
|
|
8
11
|
return element instanceof Element && (element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA' || element.tagName === 'BUTTON');
|
|
9
12
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Find the corresponding paths based on the formatted name
|
|
16
|
+
* @param name formatted name
|
|
17
|
+
* @returns paths
|
|
18
|
+
*/
|
|
13
19
|
function getPaths(name) {
|
|
14
20
|
var pattern = /(\w*)\[(\d+)\]/;
|
|
15
21
|
if (!name) {
|
|
@@ -33,6 +39,18 @@ function getFormData(form, submitter) {
|
|
|
33
39
|
}
|
|
34
40
|
return payload;
|
|
35
41
|
}
|
|
42
|
+
function getFormAttributes(form, submitter) {
|
|
43
|
+
var _ref, _submitter$getAttribu, _ref2, _submitter$getAttribu2, _submitter$getAttribu3;
|
|
44
|
+
var enforce = (value, list) => list.includes(value) ? value : list[0];
|
|
45
|
+
var action = (_ref = (_submitter$getAttribu = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formaction')) !== null && _submitter$getAttribu !== void 0 ? _submitter$getAttribu : form.getAttribute('action')) !== null && _ref !== void 0 ? _ref : "".concat(location.pathname).concat(location.search);
|
|
46
|
+
var method = (_ref2 = (_submitter$getAttribu2 = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formmethod')) !== null && _submitter$getAttribu2 !== void 0 ? _submitter$getAttribu2 : form.getAttribute('method')) !== null && _ref2 !== void 0 ? _ref2 : 'get';
|
|
47
|
+
var encType = (_submitter$getAttribu3 = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formenctype')) !== null && _submitter$getAttribu3 !== void 0 ? _submitter$getAttribu3 : form.enctype;
|
|
48
|
+
return {
|
|
49
|
+
action,
|
|
50
|
+
encType: enforce(encType, ['application/x-www-form-urlencoded', 'multipart/form-data']),
|
|
51
|
+
method: enforce(method, ['get', 'post', 'put', 'patch', 'delete'])
|
|
52
|
+
};
|
|
53
|
+
}
|
|
36
54
|
function getName(paths) {
|
|
37
55
|
return paths.reduce((name, path) => {
|
|
38
56
|
if (typeof path === 'number') {
|
|
@@ -44,61 +62,85 @@ function getName(paths) {
|
|
|
44
62
|
return [name, path].join('.');
|
|
45
63
|
}, '');
|
|
46
64
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
65
|
+
function getScope(intent) {
|
|
66
|
+
var _parseListCommand$sco, _parseListCommand;
|
|
67
|
+
var [type, ...rest] = intent.split('/');
|
|
68
|
+
switch (type) {
|
|
69
|
+
case 'validate':
|
|
70
|
+
return rest.length > 0 ? rest.join('/') : null;
|
|
71
|
+
case 'list':
|
|
72
|
+
return (_parseListCommand$sco = (_parseListCommand = parseListCommand(intent)) === null || _parseListCommand === void 0 ? void 0 : _parseListCommand.scope) !== null && _parseListCommand$sco !== void 0 ? _parseListCommand$sco : null;
|
|
73
|
+
default:
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function isFocusedOnIntentButton(form, intent) {
|
|
78
|
+
var element = document.activeElement;
|
|
79
|
+
return isFieldElement(element) && element.tagName === 'BUTTON' && element.form === form && element.name === INTENT && element.value === intent;
|
|
49
80
|
}
|
|
50
|
-
function
|
|
51
|
-
return
|
|
52
|
-
var [fieldName, message] = _ref;
|
|
53
|
-
return (typeof name === 'undefined' || name === fieldName) && message !== '';
|
|
54
|
-
}) !== 'undefined';
|
|
81
|
+
function getValidationMessage(errors) {
|
|
82
|
+
return [].concat(errors !== null && errors !== void 0 ? errors : []).join(String.fromCharCode(31));
|
|
55
83
|
}
|
|
84
|
+
function getErrors(message) {
|
|
85
|
+
if (!message) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
return message.split(String.fromCharCode(31));
|
|
89
|
+
}
|
|
90
|
+
var FORM_ERROR_ELEMENT_NAME = '__form__';
|
|
91
|
+
var INTENT = '__intent__';
|
|
92
|
+
var VALIDATION_UNDEFINED = '__undefined__';
|
|
93
|
+
var VALIDATION_SKIPPED = '__skipped__';
|
|
56
94
|
function reportSubmission(form, submission) {
|
|
57
|
-
var
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
95
|
+
for (var [_name, message] of Object.entries(submission.error)) {
|
|
96
|
+
// There is no need to create a placeholder button if all we want is to reset the error
|
|
97
|
+
if (message === '') {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
62
100
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
101
|
+
// We can't use empty string as button name
|
|
102
|
+
// As `form.element.namedItem('')` will always returns null
|
|
103
|
+
var elementName = _name ? _name : FORM_ERROR_ELEMENT_NAME;
|
|
104
|
+
var item = form.elements.namedItem(elementName);
|
|
105
|
+
if (item instanceof RadioNodeList) {
|
|
106
|
+
for (var field of item) {
|
|
107
|
+
if (field.type !== 'radio') {
|
|
108
|
+
console.warn('Repeated field name is not supported.');
|
|
109
|
+
continue;
|
|
72
110
|
}
|
|
73
111
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
112
|
+
}
|
|
113
|
+
if (item === null) {
|
|
114
|
+
// Create placeholder button to keep the error without contributing to the form data
|
|
115
|
+
var button = document.createElement('button');
|
|
116
|
+
button.name = elementName;
|
|
117
|
+
button.hidden = true;
|
|
118
|
+
button.dataset.conformTouched = 'true';
|
|
119
|
+
form.appendChild(button);
|
|
83
120
|
}
|
|
84
121
|
}
|
|
122
|
+
var focusedFirstInvalidField = false;
|
|
123
|
+
var scope = getScope(submission.intent);
|
|
124
|
+
var isSubmitting = submission.intent.slice(0, submission.intent.indexOf('/')) !== 'validate' && parseListCommand(submission.intent) === null;
|
|
85
125
|
for (var element of form.elements) {
|
|
86
126
|
if (isFieldElement(element) && element.willValidate) {
|
|
87
|
-
var
|
|
88
|
-
var
|
|
89
|
-
var
|
|
90
|
-
|
|
127
|
+
var _submission$error$_el;
|
|
128
|
+
var _elementName = element.name !== FORM_ERROR_ELEMENT_NAME ? element.name : '';
|
|
129
|
+
var messages = [].concat((_submission$error$_el = submission.error[_elementName]) !== null && _submission$error$_el !== void 0 ? _submission$error$_el : []);
|
|
130
|
+
var shouldValidate = scope === null || scope === _elementName;
|
|
131
|
+
if (shouldValidate) {
|
|
91
132
|
element.dataset.conformTouched = 'true';
|
|
92
133
|
}
|
|
93
|
-
if (
|
|
134
|
+
if (!messages.includes(VALIDATION_SKIPPED) && !messages.includes(VALIDATION_UNDEFINED)) {
|
|
94
135
|
var invalidEvent = new Event('invalid', {
|
|
95
136
|
cancelable: true
|
|
96
137
|
});
|
|
97
|
-
element.setCustomValidity(
|
|
138
|
+
element.setCustomValidity(getValidationMessage(messages));
|
|
98
139
|
element.dispatchEvent(invalidEvent);
|
|
99
140
|
}
|
|
100
|
-
if (
|
|
101
|
-
focus(
|
|
141
|
+
if (!focusedFirstInvalidField && (isSubmitting || isFocusedOnIntentButton(form, submission.intent)) && shouldValidate && element.tagName !== 'BUTTON' && !element.validity.valid) {
|
|
142
|
+
element.focus();
|
|
143
|
+
focusedFirstInvalidField = true;
|
|
102
144
|
}
|
|
103
145
|
}
|
|
104
146
|
}
|
|
@@ -119,29 +161,15 @@ function setValue(target, paths, valueFn) {
|
|
|
119
161
|
}
|
|
120
162
|
|
|
121
163
|
/**
|
|
122
|
-
*
|
|
123
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/requestSubmit
|
|
124
|
-
* @see https://caniuse.com/?search=requestSubmit
|
|
164
|
+
* Creates an intent button on demand and trigger a form submit by clicking it.
|
|
125
165
|
*/
|
|
126
|
-
function
|
|
127
|
-
var submitEvent = new SubmitEvent('submit', {
|
|
128
|
-
bubbles: true,
|
|
129
|
-
cancelable: true,
|
|
130
|
-
submitter
|
|
131
|
-
});
|
|
132
|
-
form.dispatchEvent(submitEvent);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Creates a command button on demand and trigger a form submit by clicking it.
|
|
137
|
-
*/
|
|
138
|
-
function requestCommand(form, buttonProps) {
|
|
166
|
+
function requestIntent(form, buttonProps) {
|
|
139
167
|
if (!form) {
|
|
140
168
|
console.warn('No form element is provided');
|
|
141
169
|
return;
|
|
142
170
|
}
|
|
143
171
|
var button = document.createElement('button');
|
|
144
|
-
button.name =
|
|
172
|
+
button.name = INTENT;
|
|
145
173
|
button.value = buttonProps.value;
|
|
146
174
|
button.hidden = true;
|
|
147
175
|
if (buttonProps.formNoValidate) {
|
|
@@ -153,14 +181,14 @@ function requestCommand(form, buttonProps) {
|
|
|
153
181
|
}
|
|
154
182
|
|
|
155
183
|
/**
|
|
156
|
-
* Returns the properties required to configure
|
|
184
|
+
* Returns the properties required to configure an intent button for validation
|
|
157
185
|
*
|
|
158
186
|
* @see https://conform.guide/api/react#validate
|
|
159
187
|
*/
|
|
160
188
|
function validate(field) {
|
|
161
189
|
return {
|
|
162
|
-
name:
|
|
163
|
-
value: field
|
|
190
|
+
name: INTENT,
|
|
191
|
+
value: field ? "validate/".concat(field) : 'validate',
|
|
164
192
|
formNoValidate: true
|
|
165
193
|
};
|
|
166
194
|
}
|
|
@@ -171,77 +199,80 @@ function getFormElement(element) {
|
|
|
171
199
|
}
|
|
172
200
|
return form;
|
|
173
201
|
}
|
|
174
|
-
function
|
|
175
|
-
var currentFocus = document.activeElement;
|
|
176
|
-
if (!isFieldElement(currentFocus) || currentFocus.tagName !== 'BUTTON' || currentFocus.form !== field.form) {
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
field.focus();
|
|
180
|
-
}
|
|
181
|
-
function getSubmissionType(name) {
|
|
182
|
-
var prefix = 'conform/';
|
|
183
|
-
if (!name.startsWith(prefix) || name.length <= prefix.length) {
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
return name.slice(prefix.length);
|
|
187
|
-
}
|
|
188
|
-
function parse(payload) {
|
|
189
|
-
var hasCommand = false;
|
|
202
|
+
function parse(payload, options) {
|
|
190
203
|
var submission = {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
error:
|
|
204
|
+
intent: 'submit',
|
|
205
|
+
payload: {},
|
|
206
|
+
error: {}
|
|
194
207
|
};
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (typeof value !== 'string') {
|
|
200
|
-
throw new Error('The conform command could not be used on a file input');
|
|
201
|
-
}
|
|
202
|
-
if (hasCommand) {
|
|
203
|
-
throw new Error('The conform command could only be set on a button');
|
|
204
|
-
}
|
|
205
|
-
submission = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, submission), {}, {
|
|
206
|
-
type: submissionType,
|
|
207
|
-
intent: value
|
|
208
|
-
});
|
|
209
|
-
hasCommand = true;
|
|
210
|
-
} else {
|
|
211
|
-
var paths = getPaths(_name2);
|
|
212
|
-
setValue(submission.value, paths, prev => {
|
|
213
|
-
if (!prev) {
|
|
214
|
-
return value;
|
|
215
|
-
} else if (Array.isArray(prev)) {
|
|
216
|
-
return prev.concat(value);
|
|
217
|
-
} else {
|
|
218
|
-
return [prev, value];
|
|
219
|
-
}
|
|
220
|
-
});
|
|
208
|
+
var _loop = function _loop(_value) {
|
|
209
|
+
if (_name2 === INTENT) {
|
|
210
|
+
if (typeof _value !== 'string' || submission.intent !== 'submit') {
|
|
211
|
+
throw new Error('The intent could only be set on a button');
|
|
221
212
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
213
|
+
submission.intent = _value;
|
|
214
|
+
} else {
|
|
215
|
+
var _paths = getPaths(_name2);
|
|
216
|
+
setValue(submission.payload, _paths, prev => {
|
|
217
|
+
if (!prev) {
|
|
218
|
+
return _value;
|
|
219
|
+
} else if (Array.isArray(prev)) {
|
|
220
|
+
return prev.concat(_value);
|
|
221
|
+
} else {
|
|
222
|
+
return [prev, _value];
|
|
223
|
+
}
|
|
224
|
+
});
|
|
230
225
|
}
|
|
231
|
-
}
|
|
232
|
-
|
|
226
|
+
};
|
|
227
|
+
for (var [_name2, _value] of payload.entries()) {
|
|
228
|
+
_loop(_value);
|
|
229
|
+
}
|
|
230
|
+
var command = parseListCommand(submission.intent);
|
|
231
|
+
if (command) {
|
|
232
|
+
var paths = getPaths(command.scope);
|
|
233
|
+
setValue(submission.payload, paths, list => {
|
|
234
|
+
if (typeof list !== 'undefined' && !Array.isArray(list)) {
|
|
235
|
+
throw new Error('The list command can only be applied to a list');
|
|
236
|
+
}
|
|
237
|
+
return updateList(list !== null && list !== void 0 ? list : [], command);
|
|
238
|
+
});
|
|
233
239
|
}
|
|
234
|
-
|
|
240
|
+
if (typeof (options === null || options === void 0 ? void 0 : options.resolve) === 'undefined') {
|
|
241
|
+
return submission;
|
|
242
|
+
}
|
|
243
|
+
var result = options.resolve(submission.payload, submission.intent);
|
|
244
|
+
var mergeResolveResult = resolved => {
|
|
245
|
+
var result = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, submission), resolved), {}, {
|
|
246
|
+
toJSON() {
|
|
247
|
+
return {
|
|
248
|
+
intent: this.intent,
|
|
249
|
+
payload: this.payload,
|
|
250
|
+
error: this.error
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
return result;
|
|
255
|
+
};
|
|
256
|
+
if (result instanceof Promise) {
|
|
257
|
+
return result.then(mergeResolveResult);
|
|
258
|
+
}
|
|
259
|
+
return mergeResolveResult(result);
|
|
235
260
|
}
|
|
236
|
-
function parseListCommand(
|
|
261
|
+
function parseListCommand(intent) {
|
|
237
262
|
try {
|
|
238
|
-
var
|
|
239
|
-
if (
|
|
240
|
-
|
|
263
|
+
var [group, type, scope, json] = intent.split('/');
|
|
264
|
+
if (group !== 'list' || !['prepend', 'append', 'replace', 'remove', 'reorder'].includes(type) || !scope) {
|
|
265
|
+
return null;
|
|
241
266
|
}
|
|
242
|
-
|
|
267
|
+
var _payload = JSON.parse(json);
|
|
268
|
+
return {
|
|
269
|
+
// @ts-expect-error
|
|
270
|
+
type,
|
|
271
|
+
scope,
|
|
272
|
+
payload: _payload
|
|
273
|
+
};
|
|
243
274
|
} catch (error) {
|
|
244
|
-
|
|
275
|
+
return null;
|
|
245
276
|
}
|
|
246
277
|
}
|
|
247
278
|
function updateList(list, command) {
|
|
@@ -272,23 +303,8 @@ function updateList(list, command) {
|
|
|
272
303
|
}
|
|
273
304
|
return list;
|
|
274
305
|
}
|
|
275
|
-
function handleList(submission) {
|
|
276
|
-
var _submission$intent;
|
|
277
|
-
if (submission.type !== 'list') {
|
|
278
|
-
return submission;
|
|
279
|
-
}
|
|
280
|
-
var command = parseListCommand((_submission$intent = submission.intent) !== null && _submission$intent !== void 0 ? _submission$intent : '');
|
|
281
|
-
var paths = getPaths(command.scope);
|
|
282
|
-
setValue(submission.value, paths, list => {
|
|
283
|
-
if (typeof list !== 'undefined' && !Array.isArray(list)) {
|
|
284
|
-
throw new Error('The list command can only be applied to a list');
|
|
285
|
-
}
|
|
286
|
-
return updateList(list !== null && list !== void 0 ? list : [], command);
|
|
287
|
-
});
|
|
288
|
-
return submission;
|
|
289
|
-
}
|
|
290
306
|
/**
|
|
291
|
-
* Helpers to configure
|
|
307
|
+
* Helpers to configure an intent button for modifying a list
|
|
292
308
|
*
|
|
293
309
|
* @see https://conform.guide/api/react#list
|
|
294
310
|
*/
|
|
@@ -303,12 +319,8 @@ var list = new Proxy({}, {
|
|
|
303
319
|
return function (scope) {
|
|
304
320
|
var payload = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
305
321
|
return {
|
|
306
|
-
name:
|
|
307
|
-
value: JSON.stringify(
|
|
308
|
-
type,
|
|
309
|
-
scope,
|
|
310
|
-
payload
|
|
311
|
-
}),
|
|
322
|
+
name: INTENT,
|
|
323
|
+
value: "list/".concat(type, "/").concat(scope, "/").concat(JSON.stringify(payload)),
|
|
312
324
|
formNoValidate: true
|
|
313
325
|
};
|
|
314
326
|
};
|
|
@@ -316,23 +328,107 @@ var list = new Proxy({}, {
|
|
|
316
328
|
}
|
|
317
329
|
});
|
|
318
330
|
|
|
319
|
-
|
|
331
|
+
/**
|
|
332
|
+
* Validate the form with the Constraint Validation API
|
|
333
|
+
* @see https://conform.guide/api/react#validateconstraint
|
|
334
|
+
*/
|
|
335
|
+
function validateConstraint(options) {
|
|
336
|
+
var _options$formData, _options$formatMessag;
|
|
337
|
+
var formData = (_options$formData = options === null || options === void 0 ? void 0 : options.formData) !== null && _options$formData !== void 0 ? _options$formData : new FormData(options.form);
|
|
338
|
+
var getDefaultErrors = (validity, result) => {
|
|
339
|
+
var errors = [];
|
|
340
|
+
if (validity.valueMissing) errors.push('required');
|
|
341
|
+
if (validity.typeMismatch || validity.badInput) errors.push('type');
|
|
342
|
+
if (validity.tooShort) errors.push('minLength');
|
|
343
|
+
if (validity.rangeUnderflow) errors.push('min');
|
|
344
|
+
if (validity.stepMismatch) errors.push('step');
|
|
345
|
+
if (validity.tooLong) errors.push('maxLength');
|
|
346
|
+
if (validity.rangeOverflow) errors.push('max');
|
|
347
|
+
if (validity.patternMismatch) errors.push('pattern');
|
|
348
|
+
for (var [constraintName, valid] of Object.entries(result)) {
|
|
349
|
+
if (!valid) {
|
|
350
|
+
errors.push(constraintName);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return errors;
|
|
354
|
+
};
|
|
355
|
+
var formatMessages = (_options$formatMessag = options === null || options === void 0 ? void 0 : options.formatMessages) !== null && _options$formatMessag !== void 0 ? _options$formatMessag : _ref3 => {
|
|
356
|
+
var {
|
|
357
|
+
defaultErrors
|
|
358
|
+
} = _ref3;
|
|
359
|
+
return defaultErrors;
|
|
360
|
+
};
|
|
361
|
+
return parse(formData, {
|
|
362
|
+
resolve(payload, intent) {
|
|
363
|
+
var error = {};
|
|
364
|
+
var constraintPattern = /^constraint[A-Z][^A-Z]*$/;
|
|
365
|
+
var _loop2 = function _loop2(element) {
|
|
366
|
+
if (isFieldElement(element)) {
|
|
367
|
+
var _options$acceptMultip, _options$acceptMultip2;
|
|
368
|
+
var _name3 = element.name !== FORM_ERROR_ELEMENT_NAME ? element.name : '';
|
|
369
|
+
var constraint = Object.entries(element.dataset).reduce((result, _ref4) => {
|
|
370
|
+
var [name, attributeValue = ''] = _ref4;
|
|
371
|
+
if (constraintPattern.test(name)) {
|
|
372
|
+
var _options$constraint;
|
|
373
|
+
var constraintName = name.slice(10).toLowerCase();
|
|
374
|
+
var _validate = (_options$constraint = options.constraint) === null || _options$constraint === void 0 ? void 0 : _options$constraint[constraintName];
|
|
375
|
+
if (typeof _validate === 'function') {
|
|
376
|
+
result[constraintName] = _validate(element.value, {
|
|
377
|
+
formData,
|
|
378
|
+
attributeValue
|
|
379
|
+
});
|
|
380
|
+
} else {
|
|
381
|
+
console.warn("Found an \"".concat(constraintName, "\" constraint with undefined definition; Please specify it on the validateConstraint API."));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return result;
|
|
385
|
+
}, {});
|
|
386
|
+
var errors = formatMessages({
|
|
387
|
+
name: _name3,
|
|
388
|
+
validity: element.validity,
|
|
389
|
+
constraint,
|
|
390
|
+
defaultErrors: getDefaultErrors(element.validity, constraint)
|
|
391
|
+
});
|
|
392
|
+
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, {
|
|
393
|
+
name: _name3,
|
|
394
|
+
payload,
|
|
395
|
+
intent
|
|
396
|
+
})) !== null && _options$acceptMultip !== void 0 ? _options$acceptMultip : false;
|
|
397
|
+
if (errors.length > 0) {
|
|
398
|
+
error[_name3] = shouldAcceptMultipleErrors ? errors : errors[0];
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
for (var element of options.form.elements) {
|
|
403
|
+
_loop2(element);
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
error
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
exports.FORM_ERROR_ELEMENT_NAME = FORM_ERROR_ELEMENT_NAME;
|
|
413
|
+
exports.INTENT = INTENT;
|
|
414
|
+
exports.VALIDATION_SKIPPED = VALIDATION_SKIPPED;
|
|
415
|
+
exports.VALIDATION_UNDEFINED = VALIDATION_UNDEFINED;
|
|
416
|
+
exports.getErrors = getErrors;
|
|
417
|
+
exports.getFormAttributes = getFormAttributes;
|
|
320
418
|
exports.getFormData = getFormData;
|
|
321
419
|
exports.getFormElement = getFormElement;
|
|
322
|
-
exports.getFormElements = getFormElements;
|
|
323
420
|
exports.getName = getName;
|
|
324
421
|
exports.getPaths = getPaths;
|
|
325
|
-
exports.
|
|
326
|
-
exports.
|
|
327
|
-
exports.hasError = hasError;
|
|
422
|
+
exports.getScope = getScope;
|
|
423
|
+
exports.getValidationMessage = getValidationMessage;
|
|
328
424
|
exports.isFieldElement = isFieldElement;
|
|
425
|
+
exports.isFocusedOnIntentButton = isFocusedOnIntentButton;
|
|
329
426
|
exports.list = list;
|
|
330
427
|
exports.parse = parse;
|
|
331
428
|
exports.parseListCommand = parseListCommand;
|
|
332
429
|
exports.reportSubmission = reportSubmission;
|
|
333
|
-
exports.
|
|
334
|
-
exports.requestSubmit = requestSubmit;
|
|
430
|
+
exports.requestIntent = requestIntent;
|
|
335
431
|
exports.setValue = setValue;
|
|
336
|
-
exports.shouldValidate = shouldValidate;
|
|
337
432
|
exports.updateList = updateList;
|
|
338
433
|
exports.validate = validate;
|
|
434
|
+
exports.validateConstraint = validateConstraint;
|
|
@@ -20,6 +20,7 @@ function _objectSpread2(target) {
|
|
|
20
20
|
return target;
|
|
21
21
|
}
|
|
22
22
|
function _defineProperty(obj, key, value) {
|
|
23
|
+
key = _toPropertyKey(key);
|
|
23
24
|
if (key in obj) {
|
|
24
25
|
Object.defineProperty(obj, key, {
|
|
25
26
|
value: value,
|
|
@@ -32,5 +33,19 @@ function _defineProperty(obj, key, value) {
|
|
|
32
33
|
}
|
|
33
34
|
return obj;
|
|
34
35
|
}
|
|
36
|
+
function _toPrimitive(input, hint) {
|
|
37
|
+
if (typeof input !== "object" || input === null) return input;
|
|
38
|
+
var prim = input[Symbol.toPrimitive];
|
|
39
|
+
if (prim !== undefined) {
|
|
40
|
+
var res = prim.call(input, hint || "default");
|
|
41
|
+
if (typeof res !== "object") return res;
|
|
42
|
+
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
43
|
+
}
|
|
44
|
+
return (hint === "string" ? String : Number)(input);
|
|
45
|
+
}
|
|
46
|
+
function _toPropertyKey(arg) {
|
|
47
|
+
var key = _toPrimitive(arg, "string");
|
|
48
|
+
return typeof key === "symbol" ? key : String(key);
|
|
49
|
+
}
|
|
35
50
|
|
|
36
|
-
export { _defineProperty as defineProperty, _objectSpread2 as objectSpread2 };
|
|
51
|
+
export { _defineProperty as defineProperty, _objectSpread2 as objectSpread2, _toPrimitive as toPrimitive, _toPropertyKey as toPropertyKey };
|
package/module/index.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Check if the provided reference is a form element (_input_ / _select_ / _textarea_ or _button_)
|
|
5
|
+
*/
|
|
3
6
|
function isFieldElement(element) {
|
|
4
7
|
return element instanceof Element && (element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA' || element.tagName === 'BUTTON');
|
|
5
8
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Find the corresponding paths based on the formatted name
|
|
12
|
+
* @param name formatted name
|
|
13
|
+
* @returns paths
|
|
14
|
+
*/
|
|
9
15
|
function getPaths(name) {
|
|
10
16
|
var pattern = /(\w*)\[(\d+)\]/;
|
|
11
17
|
if (!name) {
|
|
@@ -29,6 +35,18 @@ function getFormData(form, submitter) {
|
|
|
29
35
|
}
|
|
30
36
|
return payload;
|
|
31
37
|
}
|
|
38
|
+
function getFormAttributes(form, submitter) {
|
|
39
|
+
var _ref, _submitter$getAttribu, _ref2, _submitter$getAttribu2, _submitter$getAttribu3;
|
|
40
|
+
var enforce = (value, list) => list.includes(value) ? value : list[0];
|
|
41
|
+
var action = (_ref = (_submitter$getAttribu = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formaction')) !== null && _submitter$getAttribu !== void 0 ? _submitter$getAttribu : form.getAttribute('action')) !== null && _ref !== void 0 ? _ref : "".concat(location.pathname).concat(location.search);
|
|
42
|
+
var method = (_ref2 = (_submitter$getAttribu2 = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formmethod')) !== null && _submitter$getAttribu2 !== void 0 ? _submitter$getAttribu2 : form.getAttribute('method')) !== null && _ref2 !== void 0 ? _ref2 : 'get';
|
|
43
|
+
var encType = (_submitter$getAttribu3 = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formenctype')) !== null && _submitter$getAttribu3 !== void 0 ? _submitter$getAttribu3 : form.enctype;
|
|
44
|
+
return {
|
|
45
|
+
action,
|
|
46
|
+
encType: enforce(encType, ['application/x-www-form-urlencoded', 'multipart/form-data']),
|
|
47
|
+
method: enforce(method, ['get', 'post', 'put', 'patch', 'delete'])
|
|
48
|
+
};
|
|
49
|
+
}
|
|
32
50
|
function getName(paths) {
|
|
33
51
|
return paths.reduce((name, path) => {
|
|
34
52
|
if (typeof path === 'number') {
|
|
@@ -40,61 +58,85 @@ function getName(paths) {
|
|
|
40
58
|
return [name, path].join('.');
|
|
41
59
|
}, '');
|
|
42
60
|
}
|
|
43
|
-
function
|
|
44
|
-
|
|
61
|
+
function getScope(intent) {
|
|
62
|
+
var _parseListCommand$sco, _parseListCommand;
|
|
63
|
+
var [type, ...rest] = intent.split('/');
|
|
64
|
+
switch (type) {
|
|
65
|
+
case 'validate':
|
|
66
|
+
return rest.length > 0 ? rest.join('/') : null;
|
|
67
|
+
case 'list':
|
|
68
|
+
return (_parseListCommand$sco = (_parseListCommand = parseListCommand(intent)) === null || _parseListCommand === void 0 ? void 0 : _parseListCommand.scope) !== null && _parseListCommand$sco !== void 0 ? _parseListCommand$sco : null;
|
|
69
|
+
default:
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function isFocusedOnIntentButton(form, intent) {
|
|
74
|
+
var element = document.activeElement;
|
|
75
|
+
return isFieldElement(element) && element.tagName === 'BUTTON' && element.form === form && element.name === INTENT && element.value === intent;
|
|
45
76
|
}
|
|
46
|
-
function
|
|
47
|
-
return
|
|
48
|
-
var [fieldName, message] = _ref;
|
|
49
|
-
return (typeof name === 'undefined' || name === fieldName) && message !== '';
|
|
50
|
-
}) !== 'undefined';
|
|
77
|
+
function getValidationMessage(errors) {
|
|
78
|
+
return [].concat(errors !== null && errors !== void 0 ? errors : []).join(String.fromCharCode(31));
|
|
51
79
|
}
|
|
80
|
+
function getErrors(message) {
|
|
81
|
+
if (!message) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
return message.split(String.fromCharCode(31));
|
|
85
|
+
}
|
|
86
|
+
var FORM_ERROR_ELEMENT_NAME = '__form__';
|
|
87
|
+
var INTENT = '__intent__';
|
|
88
|
+
var VALIDATION_UNDEFINED = '__undefined__';
|
|
89
|
+
var VALIDATION_SKIPPED = '__skipped__';
|
|
52
90
|
function reportSubmission(form, submission) {
|
|
53
|
-
var
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
91
|
+
for (var [_name, message] of Object.entries(submission.error)) {
|
|
92
|
+
// There is no need to create a placeholder button if all we want is to reset the error
|
|
93
|
+
if (message === '') {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
58
96
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
97
|
+
// We can't use empty string as button name
|
|
98
|
+
// As `form.element.namedItem('')` will always returns null
|
|
99
|
+
var elementName = _name ? _name : FORM_ERROR_ELEMENT_NAME;
|
|
100
|
+
var item = form.elements.namedItem(elementName);
|
|
101
|
+
if (item instanceof RadioNodeList) {
|
|
102
|
+
for (var field of item) {
|
|
103
|
+
if (field.type !== 'radio') {
|
|
104
|
+
console.warn('Repeated field name is not supported.');
|
|
105
|
+
continue;
|
|
68
106
|
}
|
|
69
107
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
108
|
+
}
|
|
109
|
+
if (item === null) {
|
|
110
|
+
// Create placeholder button to keep the error without contributing to the form data
|
|
111
|
+
var button = document.createElement('button');
|
|
112
|
+
button.name = elementName;
|
|
113
|
+
button.hidden = true;
|
|
114
|
+
button.dataset.conformTouched = 'true';
|
|
115
|
+
form.appendChild(button);
|
|
79
116
|
}
|
|
80
117
|
}
|
|
118
|
+
var focusedFirstInvalidField = false;
|
|
119
|
+
var scope = getScope(submission.intent);
|
|
120
|
+
var isSubmitting = submission.intent.slice(0, submission.intent.indexOf('/')) !== 'validate' && parseListCommand(submission.intent) === null;
|
|
81
121
|
for (var element of form.elements) {
|
|
82
122
|
if (isFieldElement(element) && element.willValidate) {
|
|
83
|
-
var
|
|
84
|
-
var
|
|
85
|
-
var
|
|
86
|
-
|
|
123
|
+
var _submission$error$_el;
|
|
124
|
+
var _elementName = element.name !== FORM_ERROR_ELEMENT_NAME ? element.name : '';
|
|
125
|
+
var messages = [].concat((_submission$error$_el = submission.error[_elementName]) !== null && _submission$error$_el !== void 0 ? _submission$error$_el : []);
|
|
126
|
+
var shouldValidate = scope === null || scope === _elementName;
|
|
127
|
+
if (shouldValidate) {
|
|
87
128
|
element.dataset.conformTouched = 'true';
|
|
88
129
|
}
|
|
89
|
-
if (
|
|
130
|
+
if (!messages.includes(VALIDATION_SKIPPED) && !messages.includes(VALIDATION_UNDEFINED)) {
|
|
90
131
|
var invalidEvent = new Event('invalid', {
|
|
91
132
|
cancelable: true
|
|
92
133
|
});
|
|
93
|
-
element.setCustomValidity(
|
|
134
|
+
element.setCustomValidity(getValidationMessage(messages));
|
|
94
135
|
element.dispatchEvent(invalidEvent);
|
|
95
136
|
}
|
|
96
|
-
if (
|
|
97
|
-
focus(
|
|
137
|
+
if (!focusedFirstInvalidField && (isSubmitting || isFocusedOnIntentButton(form, submission.intent)) && shouldValidate && element.tagName !== 'BUTTON' && !element.validity.valid) {
|
|
138
|
+
element.focus();
|
|
139
|
+
focusedFirstInvalidField = true;
|
|
98
140
|
}
|
|
99
141
|
}
|
|
100
142
|
}
|
|
@@ -115,29 +157,15 @@ function setValue(target, paths, valueFn) {
|
|
|
115
157
|
}
|
|
116
158
|
|
|
117
159
|
/**
|
|
118
|
-
*
|
|
119
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/requestSubmit
|
|
120
|
-
* @see https://caniuse.com/?search=requestSubmit
|
|
160
|
+
* Creates an intent button on demand and trigger a form submit by clicking it.
|
|
121
161
|
*/
|
|
122
|
-
function
|
|
123
|
-
var submitEvent = new SubmitEvent('submit', {
|
|
124
|
-
bubbles: true,
|
|
125
|
-
cancelable: true,
|
|
126
|
-
submitter
|
|
127
|
-
});
|
|
128
|
-
form.dispatchEvent(submitEvent);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Creates a command button on demand and trigger a form submit by clicking it.
|
|
133
|
-
*/
|
|
134
|
-
function requestCommand(form, buttonProps) {
|
|
162
|
+
function requestIntent(form, buttonProps) {
|
|
135
163
|
if (!form) {
|
|
136
164
|
console.warn('No form element is provided');
|
|
137
165
|
return;
|
|
138
166
|
}
|
|
139
167
|
var button = document.createElement('button');
|
|
140
|
-
button.name =
|
|
168
|
+
button.name = INTENT;
|
|
141
169
|
button.value = buttonProps.value;
|
|
142
170
|
button.hidden = true;
|
|
143
171
|
if (buttonProps.formNoValidate) {
|
|
@@ -149,14 +177,14 @@ function requestCommand(form, buttonProps) {
|
|
|
149
177
|
}
|
|
150
178
|
|
|
151
179
|
/**
|
|
152
|
-
* Returns the properties required to configure
|
|
180
|
+
* Returns the properties required to configure an intent button for validation
|
|
153
181
|
*
|
|
154
182
|
* @see https://conform.guide/api/react#validate
|
|
155
183
|
*/
|
|
156
184
|
function validate(field) {
|
|
157
185
|
return {
|
|
158
|
-
name:
|
|
159
|
-
value: field
|
|
186
|
+
name: INTENT,
|
|
187
|
+
value: field ? "validate/".concat(field) : 'validate',
|
|
160
188
|
formNoValidate: true
|
|
161
189
|
};
|
|
162
190
|
}
|
|
@@ -167,77 +195,80 @@ function getFormElement(element) {
|
|
|
167
195
|
}
|
|
168
196
|
return form;
|
|
169
197
|
}
|
|
170
|
-
function
|
|
171
|
-
var currentFocus = document.activeElement;
|
|
172
|
-
if (!isFieldElement(currentFocus) || currentFocus.tagName !== 'BUTTON' || currentFocus.form !== field.form) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
field.focus();
|
|
176
|
-
}
|
|
177
|
-
function getSubmissionType(name) {
|
|
178
|
-
var prefix = 'conform/';
|
|
179
|
-
if (!name.startsWith(prefix) || name.length <= prefix.length) {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
return name.slice(prefix.length);
|
|
183
|
-
}
|
|
184
|
-
function parse(payload) {
|
|
185
|
-
var hasCommand = false;
|
|
198
|
+
function parse(payload, options) {
|
|
186
199
|
var submission = {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
error:
|
|
200
|
+
intent: 'submit',
|
|
201
|
+
payload: {},
|
|
202
|
+
error: {}
|
|
190
203
|
};
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (typeof value !== 'string') {
|
|
196
|
-
throw new Error('The conform command could not be used on a file input');
|
|
197
|
-
}
|
|
198
|
-
if (hasCommand) {
|
|
199
|
-
throw new Error('The conform command could only be set on a button');
|
|
200
|
-
}
|
|
201
|
-
submission = _objectSpread2(_objectSpread2({}, submission), {}, {
|
|
202
|
-
type: submissionType,
|
|
203
|
-
intent: value
|
|
204
|
-
});
|
|
205
|
-
hasCommand = true;
|
|
206
|
-
} else {
|
|
207
|
-
var paths = getPaths(_name2);
|
|
208
|
-
setValue(submission.value, paths, prev => {
|
|
209
|
-
if (!prev) {
|
|
210
|
-
return value;
|
|
211
|
-
} else if (Array.isArray(prev)) {
|
|
212
|
-
return prev.concat(value);
|
|
213
|
-
} else {
|
|
214
|
-
return [prev, value];
|
|
215
|
-
}
|
|
216
|
-
});
|
|
204
|
+
var _loop = function _loop(_value) {
|
|
205
|
+
if (_name2 === INTENT) {
|
|
206
|
+
if (typeof _value !== 'string' || submission.intent !== 'submit') {
|
|
207
|
+
throw new Error('The intent could only be set on a button');
|
|
217
208
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
209
|
+
submission.intent = _value;
|
|
210
|
+
} else {
|
|
211
|
+
var _paths = getPaths(_name2);
|
|
212
|
+
setValue(submission.payload, _paths, prev => {
|
|
213
|
+
if (!prev) {
|
|
214
|
+
return _value;
|
|
215
|
+
} else if (Array.isArray(prev)) {
|
|
216
|
+
return prev.concat(_value);
|
|
217
|
+
} else {
|
|
218
|
+
return [prev, _value];
|
|
219
|
+
}
|
|
220
|
+
});
|
|
226
221
|
}
|
|
227
|
-
}
|
|
228
|
-
|
|
222
|
+
};
|
|
223
|
+
for (var [_name2, _value] of payload.entries()) {
|
|
224
|
+
_loop(_value);
|
|
225
|
+
}
|
|
226
|
+
var command = parseListCommand(submission.intent);
|
|
227
|
+
if (command) {
|
|
228
|
+
var paths = getPaths(command.scope);
|
|
229
|
+
setValue(submission.payload, paths, list => {
|
|
230
|
+
if (typeof list !== 'undefined' && !Array.isArray(list)) {
|
|
231
|
+
throw new Error('The list command can only be applied to a list');
|
|
232
|
+
}
|
|
233
|
+
return updateList(list !== null && list !== void 0 ? list : [], command);
|
|
234
|
+
});
|
|
229
235
|
}
|
|
230
|
-
|
|
236
|
+
if (typeof (options === null || options === void 0 ? void 0 : options.resolve) === 'undefined') {
|
|
237
|
+
return submission;
|
|
238
|
+
}
|
|
239
|
+
var result = options.resolve(submission.payload, submission.intent);
|
|
240
|
+
var mergeResolveResult = resolved => {
|
|
241
|
+
var result = _objectSpread2(_objectSpread2(_objectSpread2({}, submission), resolved), {}, {
|
|
242
|
+
toJSON() {
|
|
243
|
+
return {
|
|
244
|
+
intent: this.intent,
|
|
245
|
+
payload: this.payload,
|
|
246
|
+
error: this.error
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
return result;
|
|
251
|
+
};
|
|
252
|
+
if (result instanceof Promise) {
|
|
253
|
+
return result.then(mergeResolveResult);
|
|
254
|
+
}
|
|
255
|
+
return mergeResolveResult(result);
|
|
231
256
|
}
|
|
232
|
-
function parseListCommand(
|
|
257
|
+
function parseListCommand(intent) {
|
|
233
258
|
try {
|
|
234
|
-
var
|
|
235
|
-
if (
|
|
236
|
-
|
|
259
|
+
var [group, type, scope, json] = intent.split('/');
|
|
260
|
+
if (group !== 'list' || !['prepend', 'append', 'replace', 'remove', 'reorder'].includes(type) || !scope) {
|
|
261
|
+
return null;
|
|
237
262
|
}
|
|
238
|
-
|
|
263
|
+
var _payload = JSON.parse(json);
|
|
264
|
+
return {
|
|
265
|
+
// @ts-expect-error
|
|
266
|
+
type,
|
|
267
|
+
scope,
|
|
268
|
+
payload: _payload
|
|
269
|
+
};
|
|
239
270
|
} catch (error) {
|
|
240
|
-
|
|
271
|
+
return null;
|
|
241
272
|
}
|
|
242
273
|
}
|
|
243
274
|
function updateList(list, command) {
|
|
@@ -268,23 +299,8 @@ function updateList(list, command) {
|
|
|
268
299
|
}
|
|
269
300
|
return list;
|
|
270
301
|
}
|
|
271
|
-
function handleList(submission) {
|
|
272
|
-
var _submission$intent;
|
|
273
|
-
if (submission.type !== 'list') {
|
|
274
|
-
return submission;
|
|
275
|
-
}
|
|
276
|
-
var command = parseListCommand((_submission$intent = submission.intent) !== null && _submission$intent !== void 0 ? _submission$intent : '');
|
|
277
|
-
var paths = getPaths(command.scope);
|
|
278
|
-
setValue(submission.value, paths, list => {
|
|
279
|
-
if (typeof list !== 'undefined' && !Array.isArray(list)) {
|
|
280
|
-
throw new Error('The list command can only be applied to a list');
|
|
281
|
-
}
|
|
282
|
-
return updateList(list !== null && list !== void 0 ? list : [], command);
|
|
283
|
-
});
|
|
284
|
-
return submission;
|
|
285
|
-
}
|
|
286
302
|
/**
|
|
287
|
-
* Helpers to configure
|
|
303
|
+
* Helpers to configure an intent button for modifying a list
|
|
288
304
|
*
|
|
289
305
|
* @see https://conform.guide/api/react#list
|
|
290
306
|
*/
|
|
@@ -299,12 +315,8 @@ var list = new Proxy({}, {
|
|
|
299
315
|
return function (scope) {
|
|
300
316
|
var payload = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
301
317
|
return {
|
|
302
|
-
name:
|
|
303
|
-
value: JSON.stringify(
|
|
304
|
-
type,
|
|
305
|
-
scope,
|
|
306
|
-
payload
|
|
307
|
-
}),
|
|
318
|
+
name: INTENT,
|
|
319
|
+
value: "list/".concat(type, "/").concat(scope, "/").concat(JSON.stringify(payload)),
|
|
308
320
|
formNoValidate: true
|
|
309
321
|
};
|
|
310
322
|
};
|
|
@@ -312,4 +324,85 @@ var list = new Proxy({}, {
|
|
|
312
324
|
}
|
|
313
325
|
});
|
|
314
326
|
|
|
315
|
-
|
|
327
|
+
/**
|
|
328
|
+
* Validate the form with the Constraint Validation API
|
|
329
|
+
* @see https://conform.guide/api/react#validateconstraint
|
|
330
|
+
*/
|
|
331
|
+
function validateConstraint(options) {
|
|
332
|
+
var _options$formData, _options$formatMessag;
|
|
333
|
+
var formData = (_options$formData = options === null || options === void 0 ? void 0 : options.formData) !== null && _options$formData !== void 0 ? _options$formData : new FormData(options.form);
|
|
334
|
+
var getDefaultErrors = (validity, result) => {
|
|
335
|
+
var errors = [];
|
|
336
|
+
if (validity.valueMissing) errors.push('required');
|
|
337
|
+
if (validity.typeMismatch || validity.badInput) errors.push('type');
|
|
338
|
+
if (validity.tooShort) errors.push('minLength');
|
|
339
|
+
if (validity.rangeUnderflow) errors.push('min');
|
|
340
|
+
if (validity.stepMismatch) errors.push('step');
|
|
341
|
+
if (validity.tooLong) errors.push('maxLength');
|
|
342
|
+
if (validity.rangeOverflow) errors.push('max');
|
|
343
|
+
if (validity.patternMismatch) errors.push('pattern');
|
|
344
|
+
for (var [constraintName, valid] of Object.entries(result)) {
|
|
345
|
+
if (!valid) {
|
|
346
|
+
errors.push(constraintName);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return errors;
|
|
350
|
+
};
|
|
351
|
+
var formatMessages = (_options$formatMessag = options === null || options === void 0 ? void 0 : options.formatMessages) !== null && _options$formatMessag !== void 0 ? _options$formatMessag : _ref3 => {
|
|
352
|
+
var {
|
|
353
|
+
defaultErrors
|
|
354
|
+
} = _ref3;
|
|
355
|
+
return defaultErrors;
|
|
356
|
+
};
|
|
357
|
+
return parse(formData, {
|
|
358
|
+
resolve(payload, intent) {
|
|
359
|
+
var error = {};
|
|
360
|
+
var constraintPattern = /^constraint[A-Z][^A-Z]*$/;
|
|
361
|
+
var _loop2 = function _loop2(element) {
|
|
362
|
+
if (isFieldElement(element)) {
|
|
363
|
+
var _options$acceptMultip, _options$acceptMultip2;
|
|
364
|
+
var _name3 = element.name !== FORM_ERROR_ELEMENT_NAME ? element.name : '';
|
|
365
|
+
var constraint = Object.entries(element.dataset).reduce((result, _ref4) => {
|
|
366
|
+
var [name, attributeValue = ''] = _ref4;
|
|
367
|
+
if (constraintPattern.test(name)) {
|
|
368
|
+
var _options$constraint;
|
|
369
|
+
var constraintName = name.slice(10).toLowerCase();
|
|
370
|
+
var _validate = (_options$constraint = options.constraint) === null || _options$constraint === void 0 ? void 0 : _options$constraint[constraintName];
|
|
371
|
+
if (typeof _validate === 'function') {
|
|
372
|
+
result[constraintName] = _validate(element.value, {
|
|
373
|
+
formData,
|
|
374
|
+
attributeValue
|
|
375
|
+
});
|
|
376
|
+
} else {
|
|
377
|
+
console.warn("Found an \"".concat(constraintName, "\" constraint with undefined definition; Please specify it on the validateConstraint API."));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return result;
|
|
381
|
+
}, {});
|
|
382
|
+
var errors = formatMessages({
|
|
383
|
+
name: _name3,
|
|
384
|
+
validity: element.validity,
|
|
385
|
+
constraint,
|
|
386
|
+
defaultErrors: getDefaultErrors(element.validity, constraint)
|
|
387
|
+
});
|
|
388
|
+
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, {
|
|
389
|
+
name: _name3,
|
|
390
|
+
payload,
|
|
391
|
+
intent
|
|
392
|
+
})) !== null && _options$acceptMultip !== void 0 ? _options$acceptMultip : false;
|
|
393
|
+
if (errors.length > 0) {
|
|
394
|
+
error[_name3] = shouldAcceptMultipleErrors ? errors : errors[0];
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
for (var element of options.form.elements) {
|
|
399
|
+
_loop2(element);
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
error
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export { FORM_ERROR_ELEMENT_NAME, INTENT, VALIDATION_SKIPPED, VALIDATION_UNDEFINED, getErrors, getFormAttributes, getFormData, getFormElement, getName, getPaths, getScope, getValidationMessage, isFieldElement, isFocusedOnIntentButton, list, parse, parseListCommand, reportSubmission, requestIntent, setValue, updateList, validate, validateConstraint };
|
package/package.json
CHANGED