@conform-to/dom 0.3.1 → 0.4.0-pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/index.d.ts +30 -38
  2. package/index.js +140 -156
  3. package/module/index.js +133 -149
  4. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -3,18 +3,12 @@ export declare type FieldElement = HTMLInputElement | HTMLSelectElement | HTMLTe
3
3
  export interface FieldConfig<Schema = unknown> extends FieldConstraint {
4
4
  name: string;
5
5
  defaultValue?: FieldValue<Schema>;
6
- initialError?: FieldError<Schema>['details'];
6
+ initialError?: Array<[string, string]>;
7
7
  form?: string;
8
8
  }
9
9
  export declare type FieldValue<Schema> = Schema extends Primitive | File ? string : Schema extends Array<infer InnerType> ? Array<FieldValue<InnerType>> : Schema extends Record<string, any> ? {
10
10
  [Key in keyof Schema]?: FieldValue<Schema[Key]>;
11
11
  } : unknown;
12
- export interface FieldError<Schema> {
13
- message?: string;
14
- details?: Schema extends Primitive | File ? never : Schema extends Array<infer InnerType> ? Array<FieldError<InnerType>> : Schema extends Record<string, any> ? {
15
- [Key in keyof Schema]?: FieldError<Schema[Key]>;
16
- } : unknown;
17
- }
18
12
  export declare type FieldConstraint = {
19
13
  required?: boolean;
20
14
  minLength?: number;
@@ -28,69 +22,67 @@ export declare type FieldConstraint = {
28
22
  export declare type FieldsetConstraint<Schema extends Record<string, any>> = {
29
23
  [Key in keyof Schema]?: FieldConstraint;
30
24
  };
31
- export declare type Schema<Shape extends Record<string, any>, Source> = {
32
- source: Source;
33
- constraint: FieldsetConstraint<Shape>;
34
- validate: (element: HTMLFormElement, submitter?: HTMLInputElement | HTMLButtonElement | null) => void;
35
- parse: (payload: FormData | URLSearchParams) => Submission<Shape>;
36
- };
37
- export interface FormState<Schema extends Record<string, any>> {
25
+ export declare type Submission<Schema = unknown> = {
26
+ type?: undefined;
38
27
  value: FieldValue<Schema>;
39
- error: FieldError<Schema>;
40
- }
41
- export declare type Submission<T extends Record<string, unknown>> = {
42
- state: 'modified';
43
- form: FormState<T>;
44
- } | {
45
- state: 'rejected';
46
- form: FormState<T>;
28
+ error: Array<[string, string]>;
47
29
  } | {
48
- state: 'accepted';
49
- data: T;
50
- form: FormState<T>;
30
+ type: string;
31
+ metadata: string;
32
+ value: FieldValue<Schema>;
33
+ error: Array<[string, string]>;
51
34
  };
52
35
  export declare function isFieldElement(element: unknown): element is FieldElement;
53
- export declare function getPaths(name?: string): Array<string | number>;
36
+ export declare function getPaths(name: string): Array<string | number>;
54
37
  export declare function getFormData(form: HTMLFormElement, submitter?: HTMLInputElement | HTMLButtonElement | null): FormData;
55
38
  export declare function getName(paths: Array<string | number>): string;
56
- export declare function getKey(fieldName: string, fieldsetName?: string): string | null;
57
- export declare function setFormError(form: HTMLFormElement, errors: Array<[string, string]>): void;
39
+ export declare function shouldValidate(submission: Submission, name: string): boolean;
40
+ export declare function hasError(error: Array<[string, string]>, name: string): boolean;
41
+ export declare function setFormError(form: HTMLFormElement, submission: Submission): void;
58
42
  export declare function setValue<T>(target: any, paths: Array<string | number>, valueFn: (prev?: T) => T): void;
59
- export declare function flatten(data: unknown, prefix?: string): Array<[string, FormDataEntryValue]>;
60
- export declare function unflatten(entries: Array<[string, FormDataEntryValue]> | Iterable<[string, FormDataEntryValue]>): Record<string, unknown>;
61
- export declare function createSubmission(payload: FormData | URLSearchParams): Submission<Record<string, unknown>>;
62
- export declare function createValidate(handler: (field: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement, formData: FormData) => void): (form: HTMLFormElement, submitter?: HTMLInputElement | HTMLButtonElement | null) => void;
43
+ export declare function requestSubmit(form: HTMLFormElement, submitter?: HTMLButtonElement | HTMLInputElement): void;
44
+ export declare function requestValidate(form: HTMLFormElement, field?: string): void;
63
45
  export declare function getFormElement(element: HTMLFormElement | HTMLFieldSetElement | HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement | null): HTMLFormElement | null;
64
- export declare type ListCommand<Schema> = {
46
+ export declare function focusFirstInvalidField(form: HTMLFormElement): void;
47
+ export declare function getSubmissionType(name: string): string | null;
48
+ export declare function parse<Schema extends Record<string, any>>(payload: FormData | URLSearchParams): Submission<Schema>;
49
+ export declare type Command = {
50
+ name: string;
51
+ value: string;
52
+ };
53
+ export declare type ListCommand<Schema = unknown> = {
65
54
  type: 'prepend';
55
+ scope: string;
66
56
  payload: {
67
57
  defaultValue: Schema;
68
58
  };
69
59
  } | {
70
60
  type: 'append';
61
+ scope: string;
71
62
  payload: {
72
63
  defaultValue: Schema;
73
64
  };
74
65
  } | {
75
66
  type: 'replace';
67
+ scope: string;
76
68
  payload: {
77
69
  defaultValue: Schema;
78
70
  index: number;
79
71
  };
80
72
  } | {
81
73
  type: 'remove';
74
+ scope: string;
82
75
  payload: {
83
76
  index: number;
84
77
  };
85
78
  } | {
86
79
  type: 'reorder';
80
+ scope: string;
87
81
  payload: {
88
82
  from: number;
89
83
  to: number;
90
84
  };
91
85
  };
92
- export declare const listCommandKey = "__conform__";
93
- export declare function serializeListCommand<Schema>(name: string, { type, payload }: ListCommand<Schema>): string;
94
- export declare function parseListCommand<Schema>(serialized: string): [string, ListCommand<Schema>];
95
- export declare function updateList<Type>(list: Array<Type>, command: ListCommand<Type>): Array<Type>;
96
- export declare function applyListCommand(payload: FormData | URLSearchParams): FormData | URLSearchParams;
86
+ export declare function parseListCommand<Schema = unknown>(data: string): ListCommand<Schema>;
87
+ export declare function updateList<Schema>(list: Array<Schema>, command: ListCommand<Schema>): Array<Schema>;
88
+ export declare function handleList<Schema>(submission: Submission<Schema>): Submission<Schema>;
package/index.js CHANGED
@@ -2,11 +2,13 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var _rollupPluginBabelHelpers = require('./_virtual/_rollupPluginBabelHelpers.js');
6
+
5
7
  function isFieldElement(element) {
6
8
  return element instanceof Element && (element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA' || element.tagName === 'BUTTON');
7
9
  }
8
10
  function getPaths(name) {
9
- var pattern = /(\w+)\[(\d+)\]/;
11
+ var pattern = /(\w*)\[(\d+)\]/;
10
12
 
11
13
  if (!name) {
12
14
  return [];
@@ -19,6 +21,10 @@ function getPaths(name) {
19
21
  return key;
20
22
  }
21
23
 
24
+ if (matches[1] === '') {
25
+ return Number(matches[2]);
26
+ }
27
+
22
28
  return [matches[1], Number(matches[2])];
23
29
  });
24
30
  }
@@ -33,39 +39,37 @@ function getFormData(form, submitter) {
33
39
  }
34
40
  function getName(paths) {
35
41
  return paths.reduce((name, path) => {
36
- if (name === '' || path === '') {
37
- return [name, path].join('');
38
- }
39
-
40
42
  if (typeof path === 'number') {
41
43
  return "".concat(name, "[").concat(path, "]");
42
44
  }
43
45
 
46
+ if (name === '' || path === '') {
47
+ return [name, path].join('');
48
+ }
49
+
44
50
  return [name, path].join('.');
45
51
  }, '');
46
52
  }
47
- function getKey(fieldName) {
48
- var fieldsetName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
49
- var name = fieldsetName === '' || fieldName.startsWith(fieldsetName) ? fieldName.slice(fieldsetName ? fieldsetName.length + 1 : 0) : '';
50
- var paths = getPaths(name);
51
-
52
- if (paths.length > 1) {
53
- return null;
54
- }
55
-
56
- return typeof paths[0] === 'string' ? paths[0] : null;
53
+ function shouldValidate(submission, name) {
54
+ return submission.type !== 'validate' || submission.metadata === name;
57
55
  }
58
- function setFormError(form, errors) {
59
- var firstErrorByName = Object.fromEntries([...errors].reverse());
56
+ function hasError(error, name) {
57
+ return typeof error.find(_ref => {
58
+ var [fieldName, message] = _ref;
59
+ return fieldName === name && message !== '';
60
+ }) !== 'undefined';
61
+ }
62
+ function setFormError(form, submission) {
63
+ var firstErrorByName = Object.fromEntries([...submission.error].reverse());
60
64
 
61
- for (var _element of form.elements) {
62
- var _firstErrorByName$_el;
65
+ for (var element of form.elements) {
66
+ if (isFieldElement(element)) {
67
+ var error = firstErrorByName[element.name];
63
68
 
64
- if (!isFieldElement(_element)) {
65
- continue;
69
+ if (typeof error !== 'undefined' || shouldValidate(submission, element.name)) {
70
+ element.setCustomValidity(error !== null && error !== void 0 ? error : '');
71
+ }
66
72
  }
67
-
68
- _element.setCustomValidity((_firstErrorByName$_el = firstErrorByName[_element.name]) !== null && _firstErrorByName$_el !== void 0 ? _firstErrorByName$_el : '');
69
73
  }
70
74
  }
71
75
  function setValue(target, paths, valueFn) {
@@ -84,115 +88,120 @@ function setValue(target, paths, valueFn) {
84
88
  pointer = pointer[key];
85
89
  }
86
90
  }
87
- function flatten(data) {
88
- var prefix = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
89
- var entries = [];
90
-
91
- if (typeof data === 'string' || typeof data === 'undefined' || data instanceof File) {
92
- entries.push([prefix, data !== null && data !== void 0 ? data : '']);
93
- } else if (Array.isArray(data)) {
94
- for (var i = 0; i < data.length; i++) {
95
- entries.push(...flatten(data[i], "".concat(prefix, "[").concat(i, "]")));
96
- }
97
- } else {
98
- for (var [key, value] of Object.entries(Object(data))) {
99
- entries.push(...flatten(value, prefix ? "".concat(prefix, ".").concat(key) : key));
100
- }
91
+ function requestSubmit(form, submitter) {
92
+ var submitEvent = new SubmitEvent('submit', {
93
+ bubbles: true,
94
+ cancelable: true,
95
+ submitter
96
+ });
97
+ form.dispatchEvent(submitEvent);
98
+ }
99
+ function requestValidate(form, field) {
100
+ var button = document.createElement('button');
101
+ button.name = 'conform/validate';
102
+ button.value = field !== null && field !== void 0 ? field : '';
103
+ button.hidden = true;
104
+ form.appendChild(button);
105
+ requestSubmit(form, button);
106
+ form.removeChild(button);
107
+ }
108
+ function getFormElement(element) {
109
+ var form = element instanceof HTMLFormElement ? element : element === null || element === void 0 ? void 0 : element.form;
110
+
111
+ if (!form) {
112
+ return null;
101
113
  }
102
114
 
103
- return entries;
115
+ return form;
104
116
  }
105
- function unflatten(entries) {
106
- var result = {};
107
-
108
- var _loop = function _loop(key, value) {
109
- var paths = getPaths(key);
110
- setValue(result, paths, prev => {
111
- if (prev) {
112
- throw new Error('Entry with the same name is not supported');
113
- }
117
+ function focusFirstInvalidField(form) {
118
+ var currentFocus = document.activeElement;
114
119
 
115
- return value;
116
- });
117
- };
120
+ if (!isFieldElement(currentFocus) || currentFocus.tagName !== 'BUTTON' || currentFocus.form !== form) {
121
+ return;
122
+ }
118
123
 
119
- for (var [key, value] of entries) {
120
- _loop(key, value);
124
+ for (var field of form.elements) {
125
+ if (isFieldElement(field)) {
126
+ // Focus on the first non button field
127
+ if (!field.validity.valid && field.dataset.conformTouched && field.tagName !== 'BUTTON') {
128
+ field.focus();
129
+ break;
130
+ }
131
+ }
132
+ }
133
+ }
134
+ function getSubmissionType(name) {
135
+ var prefix = 'conform/';
136
+
137
+ if (!name.startsWith(prefix) || name.length <= prefix.length) {
138
+ return null;
121
139
  }
122
140
 
123
- return result;
141
+ return name.slice(prefix.length);
124
142
  }
125
- function createSubmission(payload) {
126
- var value = {};
143
+ function parse(payload) {
144
+ var submission = {
145
+ value: {},
146
+ error: []
147
+ };
127
148
 
128
149
  try {
129
- var modifiedPayload = applyListCommand(payload);
130
- value = unflatten(modifiedPayload.entries());
131
-
132
- if (payload !== modifiedPayload) {
133
- return {
134
- state: 'modified',
135
- form: {
136
- value,
137
- error: {}
150
+ var _loop = function _loop(name, value) {
151
+ var submissionType = getSubmissionType(name);
152
+
153
+ if (submissionType) {
154
+ if (typeof value !== 'string') {
155
+ throw new Error('The conform command could not be used on a file input');
138
156
  }
139
- };
140
- }
141
- } catch (e) {
142
- return {
143
- state: 'rejected',
144
- form: {
145
- value,
146
- error: {
147
- message: e instanceof Error ? e.message : 'Submission failed'
157
+
158
+ if (typeof submission.type !== 'undefined') {
159
+ throw new Error('The conform command could only be set on a button');
148
160
  }
161
+
162
+ submission = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, submission), {}, {
163
+ type: submissionType,
164
+ metadata: value
165
+ });
166
+ } else {
167
+ var paths = getPaths(name);
168
+ setValue(submission.value, paths, prev => {
169
+ if (prev) {
170
+ throw new Error('Entry with the same name is not supported');
171
+ }
172
+
173
+ return value;
174
+ });
149
175
  }
150
176
  };
151
- }
152
177
 
153
- return {
154
- state: 'accepted',
155
- data: value,
156
- form: {
157
- value,
158
- error: {}
178
+ for (var [name, value] of payload.entries()) {
179
+ _loop(name, value);
159
180
  }
160
- };
161
- }
162
- function createValidate(handler) {
163
- return (form, submitter) => {
164
- var formData = getFormData(form, submitter);
165
181
 
166
- for (var _field of form.elements) {
167
- if (isFieldElement(_field)) {
168
- handler(_field, formData);
169
- }
182
+ switch (submission.type) {
183
+ case 'list':
184
+ submission = handleList(submission);
185
+ break;
170
186
  }
171
- };
172
- }
173
- function getFormElement(element) {
174
- var form = element instanceof HTMLFormElement ? element : element === null || element === void 0 ? void 0 : element.form;
175
-
176
- if (!form) {
177
- return null;
187
+ } catch (e) {
188
+ submission.error.push(['', e instanceof Error ? e.message : 'Invalid payload received']);
178
189
  }
179
190
 
180
- return form;
191
+ return submission;
181
192
  }
182
- var listCommandKey = '__conform__';
183
- function serializeListCommand(name, _ref) {
184
- var {
185
- type,
186
- payload
187
- } = _ref;
188
- return [name, type, JSON.stringify(payload)].join('::');
189
- }
190
- function parseListCommand(serialized) {
191
- var [name, type, json] = serialized.split('::');
192
- return [name, {
193
- type: type,
194
- payload: JSON.parse(json)
195
- }];
193
+ function parseListCommand(data) {
194
+ try {
195
+ var command = JSON.parse(data);
196
+
197
+ if (typeof command.type !== 'string' || !['prepend', 'append', 'replace', 'remove', 'reorder'].includes(command.type)) {
198
+ throw new Error('Unsupported list command type');
199
+ }
200
+
201
+ return command;
202
+ } catch (error) {
203
+ throw new Error("Invalid list command: \"".concat(data, "\"; ").concat(error));
204
+ }
196
205
  }
197
206
  function updateList(list, command) {
198
207
  switch (command.type) {
@@ -223,67 +232,42 @@ function updateList(list, command) {
223
232
  break;
224
233
 
225
234
  default:
226
- throw new Error('Invalid list command');
235
+ throw new Error('Unknown list command received');
227
236
  }
228
237
 
229
238
  return list;
230
239
  }
231
- function applyListCommand(payload) {
232
- var command = payload.get(listCommandKey);
233
-
234
- if (!command) {
235
- return payload;
236
- }
237
-
238
- payload.delete(listCommandKey);
239
-
240
- if (command instanceof File) {
241
- throw new Error("The \"".concat(listCommandKey, "\" key could not be used for file upload"));
240
+ function handleList(submission) {
241
+ if (submission.type !== 'list') {
242
+ return submission;
242
243
  }
243
244
 
244
- var result = new FormData();
245
- var entries = [];
246
- var [key, listCommand] = parseListCommand(command);
247
-
248
- for (var [name, value] of payload) {
249
- if (name.startsWith(key)) {
250
- entries.push([name.replace(key, 'list'), value]);
251
- } else {
252
- result.append(name, value);
245
+ var command = parseListCommand(submission.metadata);
246
+ var paths = getPaths(command.scope);
247
+ setValue(submission.value, paths, list => {
248
+ if (!Array.isArray(list)) {
249
+ throw new Error('The list command can only be applied to a list');
253
250
  }
254
- }
255
-
256
- var {
257
- list
258
- } = unflatten(entries);
259
-
260
- if (!Array.isArray(list)) {
261
- throw new Error('The list command can only be applied to a list');
262
- }
263
251
 
264
- updateList(list, listCommand);
265
-
266
- for (var [_name, _value] of flatten(list, key)) {
267
- result.append(_name, _value);
268
- }
269
-
270
- return result;
252
+ return updateList(list, command);
253
+ });
254
+ return submission;
271
255
  }
272
256
 
273
- exports.applyListCommand = applyListCommand;
274
- exports.createSubmission = createSubmission;
275
- exports.createValidate = createValidate;
276
- exports.flatten = flatten;
257
+ exports.focusFirstInvalidField = focusFirstInvalidField;
277
258
  exports.getFormData = getFormData;
278
259
  exports.getFormElement = getFormElement;
279
- exports.getKey = getKey;
280
260
  exports.getName = getName;
281
261
  exports.getPaths = getPaths;
262
+ exports.getSubmissionType = getSubmissionType;
263
+ exports.handleList = handleList;
264
+ exports.hasError = hasError;
282
265
  exports.isFieldElement = isFieldElement;
283
- exports.listCommandKey = listCommandKey;
266
+ exports.parse = parse;
284
267
  exports.parseListCommand = parseListCommand;
285
- exports.serializeListCommand = serializeListCommand;
268
+ exports.requestSubmit = requestSubmit;
269
+ exports.requestValidate = requestValidate;
286
270
  exports.setFormError = setFormError;
287
271
  exports.setValue = setValue;
288
- exports.unflatten = unflatten;
272
+ exports.shouldValidate = shouldValidate;
289
273
  exports.updateList = updateList;
package/module/index.js CHANGED
@@ -1,8 +1,10 @@
1
+ import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.js';
2
+
1
3
  function isFieldElement(element) {
2
4
  return element instanceof Element && (element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA' || element.tagName === 'BUTTON');
3
5
  }
4
6
  function getPaths(name) {
5
- var pattern = /(\w+)\[(\d+)\]/;
7
+ var pattern = /(\w*)\[(\d+)\]/;
6
8
 
7
9
  if (!name) {
8
10
  return [];
@@ -15,6 +17,10 @@ function getPaths(name) {
15
17
  return key;
16
18
  }
17
19
 
20
+ if (matches[1] === '') {
21
+ return Number(matches[2]);
22
+ }
23
+
18
24
  return [matches[1], Number(matches[2])];
19
25
  });
20
26
  }
@@ -29,39 +35,37 @@ function getFormData(form, submitter) {
29
35
  }
30
36
  function getName(paths) {
31
37
  return paths.reduce((name, path) => {
32
- if (name === '' || path === '') {
33
- return [name, path].join('');
34
- }
35
-
36
38
  if (typeof path === 'number') {
37
39
  return "".concat(name, "[").concat(path, "]");
38
40
  }
39
41
 
42
+ if (name === '' || path === '') {
43
+ return [name, path].join('');
44
+ }
45
+
40
46
  return [name, path].join('.');
41
47
  }, '');
42
48
  }
43
- function getKey(fieldName) {
44
- var fieldsetName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
45
- var name = fieldsetName === '' || fieldName.startsWith(fieldsetName) ? fieldName.slice(fieldsetName ? fieldsetName.length + 1 : 0) : '';
46
- var paths = getPaths(name);
47
-
48
- if (paths.length > 1) {
49
- return null;
50
- }
51
-
52
- return typeof paths[0] === 'string' ? paths[0] : null;
49
+ function shouldValidate(submission, name) {
50
+ return submission.type !== 'validate' || submission.metadata === name;
53
51
  }
54
- function setFormError(form, errors) {
55
- var firstErrorByName = Object.fromEntries([...errors].reverse());
52
+ function hasError(error, name) {
53
+ return typeof error.find(_ref => {
54
+ var [fieldName, message] = _ref;
55
+ return fieldName === name && message !== '';
56
+ }) !== 'undefined';
57
+ }
58
+ function setFormError(form, submission) {
59
+ var firstErrorByName = Object.fromEntries([...submission.error].reverse());
56
60
 
57
- for (var _element of form.elements) {
58
- var _firstErrorByName$_el;
61
+ for (var element of form.elements) {
62
+ if (isFieldElement(element)) {
63
+ var error = firstErrorByName[element.name];
59
64
 
60
- if (!isFieldElement(_element)) {
61
- continue;
65
+ if (typeof error !== 'undefined' || shouldValidate(submission, element.name)) {
66
+ element.setCustomValidity(error !== null && error !== void 0 ? error : '');
67
+ }
62
68
  }
63
-
64
- _element.setCustomValidity((_firstErrorByName$_el = firstErrorByName[_element.name]) !== null && _firstErrorByName$_el !== void 0 ? _firstErrorByName$_el : '');
65
69
  }
66
70
  }
67
71
  function setValue(target, paths, valueFn) {
@@ -80,115 +84,120 @@ function setValue(target, paths, valueFn) {
80
84
  pointer = pointer[key];
81
85
  }
82
86
  }
83
- function flatten(data) {
84
- var prefix = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
85
- var entries = [];
86
-
87
- if (typeof data === 'string' || typeof data === 'undefined' || data instanceof File) {
88
- entries.push([prefix, data !== null && data !== void 0 ? data : '']);
89
- } else if (Array.isArray(data)) {
90
- for (var i = 0; i < data.length; i++) {
91
- entries.push(...flatten(data[i], "".concat(prefix, "[").concat(i, "]")));
92
- }
93
- } else {
94
- for (var [key, value] of Object.entries(Object(data))) {
95
- entries.push(...flatten(value, prefix ? "".concat(prefix, ".").concat(key) : key));
96
- }
87
+ function requestSubmit(form, submitter) {
88
+ var submitEvent = new SubmitEvent('submit', {
89
+ bubbles: true,
90
+ cancelable: true,
91
+ submitter
92
+ });
93
+ form.dispatchEvent(submitEvent);
94
+ }
95
+ function requestValidate(form, field) {
96
+ var button = document.createElement('button');
97
+ button.name = 'conform/validate';
98
+ button.value = field !== null && field !== void 0 ? field : '';
99
+ button.hidden = true;
100
+ form.appendChild(button);
101
+ requestSubmit(form, button);
102
+ form.removeChild(button);
103
+ }
104
+ function getFormElement(element) {
105
+ var form = element instanceof HTMLFormElement ? element : element === null || element === void 0 ? void 0 : element.form;
106
+
107
+ if (!form) {
108
+ return null;
97
109
  }
98
110
 
99
- return entries;
111
+ return form;
100
112
  }
101
- function unflatten(entries) {
102
- var result = {};
103
-
104
- var _loop = function _loop(key, value) {
105
- var paths = getPaths(key);
106
- setValue(result, paths, prev => {
107
- if (prev) {
108
- throw new Error('Entry with the same name is not supported');
109
- }
113
+ function focusFirstInvalidField(form) {
114
+ var currentFocus = document.activeElement;
110
115
 
111
- return value;
112
- });
113
- };
116
+ if (!isFieldElement(currentFocus) || currentFocus.tagName !== 'BUTTON' || currentFocus.form !== form) {
117
+ return;
118
+ }
114
119
 
115
- for (var [key, value] of entries) {
116
- _loop(key, value);
120
+ for (var field of form.elements) {
121
+ if (isFieldElement(field)) {
122
+ // Focus on the first non button field
123
+ if (!field.validity.valid && field.dataset.conformTouched && field.tagName !== 'BUTTON') {
124
+ field.focus();
125
+ break;
126
+ }
127
+ }
128
+ }
129
+ }
130
+ function getSubmissionType(name) {
131
+ var prefix = 'conform/';
132
+
133
+ if (!name.startsWith(prefix) || name.length <= prefix.length) {
134
+ return null;
117
135
  }
118
136
 
119
- return result;
137
+ return name.slice(prefix.length);
120
138
  }
121
- function createSubmission(payload) {
122
- var value = {};
139
+ function parse(payload) {
140
+ var submission = {
141
+ value: {},
142
+ error: []
143
+ };
123
144
 
124
145
  try {
125
- var modifiedPayload = applyListCommand(payload);
126
- value = unflatten(modifiedPayload.entries());
127
-
128
- if (payload !== modifiedPayload) {
129
- return {
130
- state: 'modified',
131
- form: {
132
- value,
133
- error: {}
146
+ var _loop = function _loop(name, value) {
147
+ var submissionType = getSubmissionType(name);
148
+
149
+ if (submissionType) {
150
+ if (typeof value !== 'string') {
151
+ throw new Error('The conform command could not be used on a file input');
134
152
  }
135
- };
136
- }
137
- } catch (e) {
138
- return {
139
- state: 'rejected',
140
- form: {
141
- value,
142
- error: {
143
- message: e instanceof Error ? e.message : 'Submission failed'
153
+
154
+ if (typeof submission.type !== 'undefined') {
155
+ throw new Error('The conform command could only be set on a button');
144
156
  }
157
+
158
+ submission = _objectSpread2(_objectSpread2({}, submission), {}, {
159
+ type: submissionType,
160
+ metadata: value
161
+ });
162
+ } else {
163
+ var paths = getPaths(name);
164
+ setValue(submission.value, paths, prev => {
165
+ if (prev) {
166
+ throw new Error('Entry with the same name is not supported');
167
+ }
168
+
169
+ return value;
170
+ });
145
171
  }
146
172
  };
147
- }
148
173
 
149
- return {
150
- state: 'accepted',
151
- data: value,
152
- form: {
153
- value,
154
- error: {}
174
+ for (var [name, value] of payload.entries()) {
175
+ _loop(name, value);
155
176
  }
156
- };
157
- }
158
- function createValidate(handler) {
159
- return (form, submitter) => {
160
- var formData = getFormData(form, submitter);
161
177
 
162
- for (var _field of form.elements) {
163
- if (isFieldElement(_field)) {
164
- handler(_field, formData);
165
- }
178
+ switch (submission.type) {
179
+ case 'list':
180
+ submission = handleList(submission);
181
+ break;
166
182
  }
167
- };
168
- }
169
- function getFormElement(element) {
170
- var form = element instanceof HTMLFormElement ? element : element === null || element === void 0 ? void 0 : element.form;
171
-
172
- if (!form) {
173
- return null;
183
+ } catch (e) {
184
+ submission.error.push(['', e instanceof Error ? e.message : 'Invalid payload received']);
174
185
  }
175
186
 
176
- return form;
187
+ return submission;
177
188
  }
178
- var listCommandKey = '__conform__';
179
- function serializeListCommand(name, _ref) {
180
- var {
181
- type,
182
- payload
183
- } = _ref;
184
- return [name, type, JSON.stringify(payload)].join('::');
185
- }
186
- function parseListCommand(serialized) {
187
- var [name, type, json] = serialized.split('::');
188
- return [name, {
189
- type: type,
190
- payload: JSON.parse(json)
191
- }];
189
+ function parseListCommand(data) {
190
+ try {
191
+ var command = JSON.parse(data);
192
+
193
+ if (typeof command.type !== 'string' || !['prepend', 'append', 'replace', 'remove', 'reorder'].includes(command.type)) {
194
+ throw new Error('Unsupported list command type');
195
+ }
196
+
197
+ return command;
198
+ } catch (error) {
199
+ throw new Error("Invalid list command: \"".concat(data, "\"; ").concat(error));
200
+ }
192
201
  }
193
202
  function updateList(list, command) {
194
203
  switch (command.type) {
@@ -219,51 +228,26 @@ function updateList(list, command) {
219
228
  break;
220
229
 
221
230
  default:
222
- throw new Error('Invalid list command');
231
+ throw new Error('Unknown list command received');
223
232
  }
224
233
 
225
234
  return list;
226
235
  }
227
- function applyListCommand(payload) {
228
- var command = payload.get(listCommandKey);
229
-
230
- if (!command) {
231
- return payload;
232
- }
233
-
234
- payload.delete(listCommandKey);
235
-
236
- if (command instanceof File) {
237
- throw new Error("The \"".concat(listCommandKey, "\" key could not be used for file upload"));
236
+ function handleList(submission) {
237
+ if (submission.type !== 'list') {
238
+ return submission;
238
239
  }
239
240
 
240
- var result = new FormData();
241
- var entries = [];
242
- var [key, listCommand] = parseListCommand(command);
243
-
244
- for (var [name, value] of payload) {
245
- if (name.startsWith(key)) {
246
- entries.push([name.replace(key, 'list'), value]);
247
- } else {
248
- result.append(name, value);
241
+ var command = parseListCommand(submission.metadata);
242
+ var paths = getPaths(command.scope);
243
+ setValue(submission.value, paths, list => {
244
+ if (!Array.isArray(list)) {
245
+ throw new Error('The list command can only be applied to a list');
249
246
  }
250
- }
251
-
252
- var {
253
- list
254
- } = unflatten(entries);
255
-
256
- if (!Array.isArray(list)) {
257
- throw new Error('The list command can only be applied to a list');
258
- }
259
247
 
260
- updateList(list, listCommand);
261
-
262
- for (var [_name, _value] of flatten(list, key)) {
263
- result.append(_name, _value);
264
- }
265
-
266
- return result;
248
+ return updateList(list, command);
249
+ });
250
+ return submission;
267
251
  }
268
252
 
269
- export { applyListCommand, createSubmission, createValidate, flatten, getFormData, getFormElement, getKey, getName, getPaths, isFieldElement, listCommandKey, parseListCommand, serializeListCommand, setFormError, setValue, unflatten, updateList };
253
+ export { focusFirstInvalidField, getFormData, getFormElement, getName, getPaths, getSubmissionType, handleList, hasError, isFieldElement, parse, parseListCommand, requestSubmit, requestValidate, setFormError, setValue, shouldValidate, updateList };
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.3.1",
5
+ "version": "0.4.0-pre.1",
6
6
  "main": "index.js",
7
7
  "module": "module/index.js",
8
8
  "repository": {