@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 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 built on top of the [Constraint Validation](https://caniuse.com/constraint-validation) API.
5
+ Conform is a progressive enhancement first form validation library for [Remix](https://remix.run)
6
6
 
7
- - **Progressive Enhancement**: It is designed based on the [HTML specification](https://html.spec.whatwg.org/dev/form-control-infrastructure.html#the-constraint-validation-api). From validating the form to reporting error messages for each field, if you don't like part of the solution, just replace it with your own.
8
- - **Framework Agnostic**: The DOM is the only dependency. Conform makes use of native [Web APIs](https://developer.mozilla.org/en-US/docs/Web/API) exclusively. You don't have to use React / Vue / Svelte to utilise this library.
9
- - **Flexible Setup**: It can validates fields anywhere in the dom with the help of [form attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#form). Also enables CSS pseudo-classes like `:valid` and `:invalid`, allowing flexible styling across your form without the need to manipulate the class names.
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 declare type Primitive = null | undefined | string | number | boolean | Date;
2
- export declare type FieldElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement;
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?: Array<[string, string]>;
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 declare type FieldValue<Schema> = Schema extends Primitive ? string : Schema extends File ? File : Schema extends Array<infer InnerType> ? Array<FieldValue<InnerType>> : Schema extends Record<string, any> ? {
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
- } : unknown;
14
- export declare type FieldConstraint<Schema = any> = {
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 declare type FieldsetConstraint<Schema extends Record<string, any>> = {
32
+ export type FieldsetConstraint<Schema extends Record<string, any>> = {
25
33
  [Key in keyof Schema]?: FieldConstraint<Schema[Key]>;
26
34
  };
27
- export declare type Submission<Schema = unknown> = {
28
- type: string;
29
- intent?: string;
30
- value: FieldValue<Schema>;
31
- error: Array<[string, string]>;
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 CommandButtonProps<Name extends string = string> {
34
- name: `conform/${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
- export declare function getFormElements(form: HTMLFormElement): FieldElement[];
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 shouldValidate(submission: Submission, name: string): boolean;
44
- export declare function hasError(error: Array<[string, string]>, name?: string): boolean;
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
- * The ponyfill of `HTMLFormElement.requestSubmit()`
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 requestCommand(form: HTMLFormElement | undefined, buttonProps: CommandButtonProps): void;
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 a command button for validation
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): CommandButtonProps<'validate'>;
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 focus(field: FieldElement): void;
65
- export declare function getSubmissionType(name: string): string | null;
66
- export declare function parse<Schema extends Record<string, any>>(payload: FormData | URLSearchParams): Submission<Schema>;
67
- export declare type ListCommand<Schema = unknown> = {
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>(data: string): ListCommand<Schema>;
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
- }): CommandButtonProps<'list'>;
158
+ }): IntentButtonProps;
107
159
  prepend<Schema>(name: string, payload?: {
108
160
  defaultValue: Schema;
109
- }): CommandButtonProps<'list'>;
161
+ }): IntentButtonProps;
110
162
  replace<Schema>(name: string, payload: {
111
163
  defaultValue: Schema;
112
164
  index: number;
113
- }): CommandButtonProps<'list'>;
165
+ }): IntentButtonProps;
114
166
  remove(name: string, payload: {
115
167
  index: number;
116
- }): CommandButtonProps<'list'>;
168
+ }): IntentButtonProps;
117
169
  reorder(name: string, payload: {
118
170
  from: number;
119
171
  to: number;
120
- }): CommandButtonProps<'list'>;
172
+ }): IntentButtonProps;
121
173
  }
122
174
  /**
123
- * Helpers to configure a command button for modifying a list
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
- function getFormElements(form) {
11
- return Array.from(form.elements).filter(isFieldElement);
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 shouldValidate(submission, name) {
48
- return submission.type === 'submit' || submission.type === 'validate' && (submission.intent === '' || submission.intent === name);
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 hasError(error, name) {
51
- return typeof error.find(_ref => {
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 messageByName = new Map();
58
- for (var [_name, message] of submission.error) {
59
- if (!messageByName.has(_name)) {
60
- // Only keep the first error message (for now)
61
- messageByName.set(_name, message);
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
- // We can't use empty string as button name
64
- // As `form.element.namedItem('')` will always returns null
65
- var elementName = _name ? _name : '__form__';
66
- var item = form.elements.namedItem(elementName);
67
- if (item instanceof RadioNodeList) {
68
- for (var field of item) {
69
- if (field.type !== 'radio') {
70
- throw new Error('Repeated field name is not supported');
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
- if (item === null) {
75
- // Create placeholder button to keep the error without contributing to the form data
76
- var button = document.createElement('button');
77
- button.name = elementName;
78
- button.hidden = true;
79
- button.dataset.conformTouched = 'true';
80
- item = button;
81
- form.appendChild(button);
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 _elementName = element.name !== '__form__' ? element.name : '';
88
- var _message = messageByName.get(_elementName);
89
- var elementShouldValidate = shouldValidate(submission, _elementName);
90
- if (elementShouldValidate) {
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 (typeof _message !== 'undefined' || elementShouldValidate) {
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(_message !== null && _message !== void 0 ? _message : '');
138
+ element.setCustomValidity(getValidationMessage(messages));
98
139
  element.dispatchEvent(invalidEvent);
99
140
  }
100
- if (elementShouldValidate && !element.validity.valid) {
101
- focus(element);
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
- * The ponyfill of `HTMLFormElement.requestSubmit()`
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 requestSubmit(form, submitter) {
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 = buttonProps.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 a command button for validation
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: 'conform/validate',
163
- value: field !== null && field !== void 0 ? 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 focus(field) {
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
- type: 'submit',
192
- value: {},
193
- error: []
204
+ intent: 'submit',
205
+ payload: {},
206
+ error: {}
194
207
  };
195
- try {
196
- var _loop = function _loop(value, _name2) {
197
- var submissionType = getSubmissionType(_name2);
198
- if (submissionType) {
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
- for (var [_name2, value] of payload.entries()) {
224
- _loop(value, _name2);
225
- }
226
- switch (submission.type) {
227
- case 'list':
228
- submission = handleList(submission);
229
- break;
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
- } catch (e) {
232
- submission.error.push(['', e instanceof Error ? e.message : 'Invalid payload received']);
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
- return submission;
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(data) {
261
+ function parseListCommand(intent) {
237
262
  try {
238
- var command = JSON.parse(data);
239
- if (typeof command.type !== 'string' || !['prepend', 'append', 'replace', 'remove', 'reorder', 'combine'].includes(command.type)) {
240
- throw new Error("Unknown list command received: ".concat(command.type));
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
- return command;
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
- throw new Error("Invalid list command: \"".concat(data, "\"; ").concat(error));
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 a command button for modifying a list
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: 'conform/list',
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
- exports.focus = focus;
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.getSubmissionType = getSubmissionType;
326
- exports.handleList = handleList;
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.requestCommand = requestCommand;
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
- function getFormElements(form) {
7
- return Array.from(form.elements).filter(isFieldElement);
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 shouldValidate(submission, name) {
44
- return submission.type === 'submit' || submission.type === 'validate' && (submission.intent === '' || submission.intent === name);
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 hasError(error, name) {
47
- return typeof error.find(_ref => {
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 messageByName = new Map();
54
- for (var [_name, message] of submission.error) {
55
- if (!messageByName.has(_name)) {
56
- // Only keep the first error message (for now)
57
- messageByName.set(_name, message);
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
- // We can't use empty string as button name
60
- // As `form.element.namedItem('')` will always returns null
61
- var elementName = _name ? _name : '__form__';
62
- var item = form.elements.namedItem(elementName);
63
- if (item instanceof RadioNodeList) {
64
- for (var field of item) {
65
- if (field.type !== 'radio') {
66
- throw new Error('Repeated field name is not supported');
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
- if (item === null) {
71
- // Create placeholder button to keep the error without contributing to the form data
72
- var button = document.createElement('button');
73
- button.name = elementName;
74
- button.hidden = true;
75
- button.dataset.conformTouched = 'true';
76
- item = button;
77
- form.appendChild(button);
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 _elementName = element.name !== '__form__' ? element.name : '';
84
- var _message = messageByName.get(_elementName);
85
- var elementShouldValidate = shouldValidate(submission, _elementName);
86
- if (elementShouldValidate) {
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 (typeof _message !== 'undefined' || elementShouldValidate) {
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(_message !== null && _message !== void 0 ? _message : '');
134
+ element.setCustomValidity(getValidationMessage(messages));
94
135
  element.dispatchEvent(invalidEvent);
95
136
  }
96
- if (elementShouldValidate && !element.validity.valid) {
97
- focus(element);
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
- * The ponyfill of `HTMLFormElement.requestSubmit()`
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 requestSubmit(form, submitter) {
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 = buttonProps.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 a command button for validation
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: 'conform/validate',
159
- value: field !== null && field !== void 0 ? 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 focus(field) {
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
- type: 'submit',
188
- value: {},
189
- error: []
200
+ intent: 'submit',
201
+ payload: {},
202
+ error: {}
190
203
  };
191
- try {
192
- var _loop = function _loop(value, _name2) {
193
- var submissionType = getSubmissionType(_name2);
194
- if (submissionType) {
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
- for (var [_name2, value] of payload.entries()) {
220
- _loop(value, _name2);
221
- }
222
- switch (submission.type) {
223
- case 'list':
224
- submission = handleList(submission);
225
- break;
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
- } catch (e) {
228
- submission.error.push(['', e instanceof Error ? e.message : 'Invalid payload received']);
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
- return submission;
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(data) {
257
+ function parseListCommand(intent) {
233
258
  try {
234
- var command = JSON.parse(data);
235
- if (typeof command.type !== 'string' || !['prepend', 'append', 'replace', 'remove', 'reorder', 'combine'].includes(command.type)) {
236
- throw new Error("Unknown list command received: ".concat(command.type));
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
- return command;
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
- throw new Error("Invalid list command: \"".concat(data, "\"; ").concat(error));
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 a command button for modifying a list
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: 'conform/list',
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
- export { focus, getFormData, getFormElement, getFormElements, getName, getPaths, getSubmissionType, handleList, hasError, isFieldElement, list, parse, parseListCommand, reportSubmission, requestCommand, requestSubmit, setValue, shouldValidate, updateList, validate };
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
@@ -2,7 +2,7 @@
2
2
  "name": "@conform-to/dom",
3
3
  "description": "A set of opinionated helpers built on top of the Constraint Validation API",
4
4
  "license": "MIT",
5
- "version": "0.5.1",
5
+ "version": "0.6.0",
6
6
  "main": "index.js",
7
7
  "module": "module/index.js",
8
8
  "repository": {