@conform-to/dom 0.2.0 → 0.3.0-pre.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
@@ -4,24 +4,22 @@
4
4
 
5
5
  ## API Reference
6
6
 
7
- - [getControlButtonProps](#getControlButtonProps)
8
7
  - [getFieldProps](#getFieldProps)
9
- - [getFieldElements](#getFieldElements)
8
+ - [getFieldsetData](#getFieldsetData)
10
9
  - [getName](#getName)
11
10
  - [getPaths](#getPaths)
12
11
  - [isFieldElement](#isFieldElement)
12
+ - [isFieldsetField](#isFieldsetField)
13
13
  - [parse](#parse)
14
14
  - [reportValidity](#reportValidity)
15
15
  - [setFieldState](#setFieldState)
16
+ - [setFieldsetError](#setFieldsetError)
16
17
  - [shouldSkipValidate](#shouldSkipValidate)
17
- - [setFieldState](#setFieldState)
18
18
  - [transform](#transform)
19
19
 
20
- ### getControlButtonProps
21
-
22
20
  ### getFieldProps
23
21
 
24
- ### getFieldElements
22
+ ### getFieldsetData
25
23
 
26
24
  ### getName
27
25
 
@@ -29,7 +27,7 @@
29
27
 
30
28
  ### isFieldElement
31
29
 
32
- ### isFieldsetElement
30
+ ### isFieldsetField
33
31
 
34
32
  ### parse
35
33
 
@@ -37,6 +35,8 @@
37
35
 
38
36
  ### setFieldState
39
37
 
38
+ ### setFieldsetError
39
+
40
40
  ### shouldSkipValidate
41
41
 
42
42
  ### transform
package/index.d.ts CHANGED
@@ -1,4 +1,21 @@
1
- export declare type Constraint = {
1
+ export declare type Primitive = null | undefined | string | number | boolean | Date;
2
+ export declare type FieldElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement;
3
+ export interface FieldConfig<Schema = unknown> extends FieldConstraint {
4
+ name: string;
5
+ defaultValue?: FieldValue<Schema>;
6
+ initialError?: FieldError<Schema>['details'];
7
+ form?: string;
8
+ }
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
+ [Key in keyof Schema]?: FieldValue<Schema[Key]>;
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
+ export declare type FieldConstraint = {
2
19
  required?: boolean;
3
20
  minLength?: number;
4
21
  maxLength?: number;
@@ -8,36 +25,18 @@ export declare type Constraint = {
8
25
  multiple?: boolean;
9
26
  pattern?: string;
10
27
  };
11
- export interface FieldProps<Type = any> extends Constraint {
12
- name: string;
13
- defaultValue?: FieldsetData<Type, string>;
14
- error?: FieldsetData<Type, string>;
15
- form?: string;
16
- }
17
- export declare type Schema<Type extends Record<string, any>> = {
18
- fields: {
19
- [Key in keyof Type]-?: Constraint;
20
- };
21
- validate?: (element: FieldsetElement) => void;
28
+ export declare type FieldsetConstraint<Schema extends Record<string, any>> = {
29
+ [Key in keyof Schema]?: FieldConstraint;
22
30
  };
23
- /**
24
- * Data structure of the form value
25
- */
26
- export declare type FieldsetData<Type, Value> = Type extends string | number | Date | boolean | undefined ? Value : Type extends Array<infer InnerType> ? Array<FieldsetData<InnerType, Value>> : Type extends Object ? {
27
- [Key in keyof Type]?: FieldsetData<Type[Key], Value>;
28
- } : unknown;
29
- /**
30
- * Element that maintains a list of fields
31
- * i.e. fieldset.elements
32
- */
33
- export declare type FieldsetElement = HTMLFormElement | HTMLFieldSetElement;
34
- /**
35
- * Element type that might be a candiate of Constraint Validation
36
- */
37
- export declare type FieldElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement;
38
- export interface FormState<T> {
39
- value: FieldsetData<T, string>;
40
- error: FieldsetData<T, string>;
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>> {
38
+ value: FieldValue<Schema>;
39
+ error: FieldError<Schema>;
41
40
  }
42
41
  export declare type Submission<T extends Record<string, unknown>> = {
43
42
  state: 'modified';
@@ -50,50 +49,48 @@ export declare type Submission<T extends Record<string, unknown>> = {
50
49
  data: T;
51
50
  form: FormState<T>;
52
51
  };
53
- export interface ControlButtonProps {
54
- type: 'submit';
55
- name: string;
56
- value: string;
57
- formNoValidate: boolean;
58
- }
59
- export interface ControlAction<T = unknown> {
60
- prepend: {
61
- defaultValue: T;
52
+ export declare function isFieldElement(element: unknown): element is FieldElement;
53
+ export declare function getPaths(name?: string): Array<string | number>;
54
+ export declare function getFormData(form: HTMLFormElement, submitter?: HTMLInputElement | HTMLButtonElement | null): FormData;
55
+ 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;
58
+ 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;
63
+ export declare function getFormElement(element: HTMLFormElement | HTMLFieldSetElement | HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement | null): HTMLFormElement | null;
64
+ export declare type ListCommand<Schema> = {
65
+ type: 'prepend';
66
+ payload: {
67
+ defaultValue: Schema;
62
68
  };
63
- append: {
64
- defaultValue: T;
69
+ } | {
70
+ type: 'append';
71
+ payload: {
72
+ defaultValue: Schema;
65
73
  };
66
- replace: {
67
- defaultValue: T;
74
+ } | {
75
+ type: 'replace';
76
+ payload: {
77
+ defaultValue: Schema;
68
78
  index: number;
69
79
  };
70
- remove: {
80
+ } | {
81
+ type: 'remove';
82
+ payload: {
71
83
  index: number;
72
84
  };
73
- reorder: {
85
+ } | {
86
+ type: 'reorder';
87
+ payload: {
74
88
  from: number;
75
89
  to: number;
76
90
  };
77
- }
78
- export declare function isFieldsetElement(element: unknown): element is FieldsetElement;
79
- export declare function isFieldElement(element: unknown): element is FieldElement;
80
- export declare function setFieldState(field: unknown, state: {
81
- touched: boolean;
82
- }): void;
83
- export declare function reportValidity(fieldset: FieldsetElement): boolean;
84
- export declare function getFieldProps<Type extends Record<string, any>>(schema: Schema<Type>, options: {
85
- name?: string;
86
- form?: string;
87
- defaultValue?: FieldsetData<Type, string>;
88
- error?: FieldsetData<Type, string>;
89
- }): {
90
- [Key in keyof Type]-?: FieldProps<Type[Key]>;
91
91
  };
92
- export declare function shouldSkipValidate(event: SubmitEvent): boolean;
93
- export declare function getPaths(name?: string): Array<string | number>;
94
- export declare function getName(paths: Array<string | number>): string;
95
- export declare function transform(entries: Array<[string, FormDataEntryValue]> | Iterable<[string, FormDataEntryValue]>): Record<string, unknown>;
96
- export declare function getControlButtonProps<Action extends keyof ControlAction, Payload extends ControlAction[Action]>(name: string, action: Action, payload: Payload): ControlButtonProps;
97
- export declare function applyControlCommand<Type, Action extends keyof ControlAction<Type>, Payload extends ControlAction<Type>[Action]>(list: Array<Type>, action: Action | string, payload: Payload): Array<Type>;
98
- export declare function parse(payload: FormData | URLSearchParams): Submission<Record<string, unknown>>;
99
- export declare function getFieldElements(fieldset: FieldsetElement, key: string): FieldElement[];
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;
package/index.js CHANGED
@@ -2,86 +2,9 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var _rollupPluginBabelHelpers = require('./_virtual/_rollupPluginBabelHelpers.js');
6
-
7
- /**
8
- * Data structure of the form value
9
- */
10
-
11
- /**
12
- * Element that maintains a list of fields
13
- * i.e. fieldset.elements
14
- */
15
-
16
- /**
17
- * Element type that might be a candiate of Constraint Validation
18
- */
19
- function isFieldsetElement(element) {
20
- return element instanceof Element && (element.tagName === 'FORM' || element.tagName === 'FIELDSET');
21
- }
22
5
  function isFieldElement(element) {
23
6
  return element instanceof Element && (element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA' || element.tagName === 'BUTTON');
24
7
  }
25
- function setFieldState(field, state) {
26
- if (isFieldsetElement(field)) {
27
- for (var _element of field.elements) {
28
- setFieldState(_element, state);
29
- }
30
-
31
- return;
32
- }
33
-
34
- if (!isFieldElement(field)) {
35
- console.warn('Only input/select/textarea/button element can be touched');
36
- return;
37
- }
38
-
39
- if (state.touched) {
40
- field.dataset.touched = 'true';
41
- } else {
42
- delete field.dataset.touched;
43
- }
44
- }
45
- function reportValidity(fieldset) {
46
- var isValid = true;
47
-
48
- for (var field of fieldset.elements) {
49
- if (isFieldElement(field) && field.dataset.touched && !field.checkValidity()) {
50
- isValid = false;
51
- }
52
- }
53
-
54
- return isValid;
55
- }
56
- function getFieldProps(schema, options) {
57
- var result = {};
58
-
59
- for (var key of Object.keys(schema.fields)) {
60
- var _options$defaultValue, _options$error;
61
-
62
- var constraint = schema.fields[key];
63
-
64
- var props = _rollupPluginBabelHelpers.objectSpread2({
65
- name: options.name ? "".concat(options.name, ".").concat(key) : key,
66
- form: options.form,
67
- defaultValue: (_options$defaultValue = options.defaultValue) === null || _options$defaultValue === void 0 ? void 0 : _options$defaultValue[key],
68
- error: (_options$error = options.error) === null || _options$error === void 0 ? void 0 : _options$error[key]
69
- }, constraint);
70
-
71
- result[key] = props;
72
- }
73
-
74
- return result;
75
- }
76
- function shouldSkipValidate(event) {
77
- var _event$submitter, _event$submitter2;
78
-
79
- if (((_event$submitter = event.submitter) === null || _event$submitter === void 0 ? void 0 : _event$submitter.tagName) === 'BUTTON' || ((_event$submitter2 = event.submitter) === null || _event$submitter2 === void 0 ? void 0 : _event$submitter2.tagName) === 'INPUT') {
80
- return event.submitter.formNoValidate;
81
- }
82
-
83
- return false;
84
- }
85
8
  function getPaths(name) {
86
9
  var pattern = /(\w+)\[(\d+)\]/;
87
10
 
@@ -99,6 +22,15 @@ function getPaths(name) {
99
22
  return [matches[1], Number(matches[2])];
100
23
  });
101
24
  }
25
+ function getFormData(form, submitter) {
26
+ var payload = new FormData(form);
27
+
28
+ if (submitter !== null && submitter !== void 0 && submitter.name) {
29
+ payload.append(submitter.name, submitter.value);
30
+ }
31
+
32
+ return payload;
33
+ }
102
34
  function getName(paths) {
103
35
  return paths.reduce((name, path) => {
104
36
  if (name === '' || path === '') {
@@ -112,179 +44,246 @@ function getName(paths) {
112
44
  return [name, path].join('.');
113
45
  }, '');
114
46
  }
115
- function transform(entries) {
116
- var result = {};
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);
117
51
 
118
- for (var [key, value] of entries) {
119
- var paths = getPaths(key);
120
- var length = paths.length;
121
- var lastIndex = length - 1;
122
- var index = -1;
123
- var pointer = result;
52
+ if (paths.length > 1) {
53
+ return null;
54
+ }
124
55
 
125
- while (pointer != null && ++index < length) {
126
- var _key = paths[index];
127
- var next = paths[index + 1];
128
- var newValue = value;
56
+ return typeof paths[0] === 'string' ? paths[0] : null;
57
+ }
58
+ function setFormError(form, errors) {
59
+ var firstErrorByName = Object.fromEntries([...errors].reverse());
129
60
 
130
- if (index != lastIndex) {
131
- var _pointer$_key;
61
+ for (var _element of form.elements) {
62
+ var _firstErrorByName$_el;
132
63
 
133
- newValue = (_pointer$_key = pointer[_key]) !== null && _pointer$_key !== void 0 ? _pointer$_key : typeof next === 'number' ? [] : {};
134
- } // if (typeof pointer[key] !== 'undefined') {
135
- // pointer[key] = Array.isArray(pointer[key])
136
- // ? pointer[key].concat(newValue)
137
- // : [pointer[key], newValue];
138
- // } else {
64
+ if (!isFieldElement(_element)) {
65
+ continue;
66
+ }
139
67
 
68
+ _element.setCustomValidity((_firstErrorByName$_el = firstErrorByName[_element.name]) !== null && _firstErrorByName$_el !== void 0 ? _firstErrorByName$_el : '');
69
+ }
70
+ }
71
+ function setValue(target, paths, valueFn) {
72
+ var length = paths.length;
73
+ var lastIndex = length - 1;
74
+ var index = -1;
75
+ var pointer = target;
76
+
77
+ while (pointer != null && ++index < length) {
78
+ var _pointer$key;
79
+
80
+ var key = paths[index];
81
+ var next = paths[index + 1];
82
+ var newValue = index != lastIndex ? (_pointer$key = pointer[key]) !== null && _pointer$key !== void 0 ? _pointer$key : typeof next === 'number' ? [] : {} : valueFn(pointer[key]);
83
+ pointer[key] = newValue;
84
+ pointer = pointer[key];
85
+ }
86
+ }
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
+ }
101
+ }
140
102
 
141
- pointer[_key] = newValue; // }
103
+ return entries;
104
+ }
105
+ function unflatten(entries) {
106
+ var result = {};
142
107
 
143
- pointer = pointer[_key];
144
- }
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
+ }
114
+
115
+ return value;
116
+ });
117
+ };
118
+
119
+ for (var [key, value] of entries) {
120
+ _loop(key, value);
145
121
  }
146
122
 
147
123
  return result;
148
124
  }
149
- function getControlButtonProps(name, action, payload) {
125
+ function createSubmission(payload) {
126
+ var value = {};
127
+
128
+ 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: {}
138
+ }
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'
148
+ }
149
+ }
150
+ };
151
+ }
152
+
150
153
  return {
151
- type: 'submit',
152
- name: '__conform__',
153
- value: [name, action, JSON.stringify(payload)].join('::'),
154
- formNoValidate: true
154
+ state: 'accepted',
155
+ data: value,
156
+ form: {
157
+ value,
158
+ error: {}
159
+ }
160
+ };
161
+ }
162
+ function createValidate(handler) {
163
+ return (form, submitter) => {
164
+ var formData = getFormData(form, submitter);
165
+
166
+ for (var _field of form.elements) {
167
+ if (isFieldElement(_field)) {
168
+ handler(_field, formData);
169
+ }
170
+ }
155
171
  };
156
172
  }
157
- function applyControlCommand(list, action, payload) {
158
- switch (action) {
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;
178
+ }
179
+
180
+ return form;
181
+ }
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
+ }];
196
+ }
197
+ function updateList(list, command) {
198
+ switch (command.type) {
159
199
  case 'prepend':
160
200
  {
161
- var {
162
- defaultValue
163
- } = payload;
164
- list.unshift(defaultValue);
201
+ list.unshift(command.payload.defaultValue);
165
202
  break;
166
203
  }
167
204
 
168
205
  case 'append':
169
206
  {
170
- var {
171
- defaultValue: _defaultValue
172
- } = payload;
173
- list.push(_defaultValue);
207
+ list.push(command.payload.defaultValue);
174
208
  break;
175
209
  }
176
210
 
177
211
  case 'replace':
178
212
  {
179
- var {
180
- defaultValue: _defaultValue2,
181
- index: _index
182
- } = payload;
183
- list.splice(_index, 1, _defaultValue2);
213
+ list.splice(command.payload.index, 1, command.payload.defaultValue);
184
214
  break;
185
215
  }
186
216
 
187
217
  case 'remove':
188
- var {
189
- index
190
- } = payload;
191
- list.splice(index, 1);
218
+ list.splice(command.payload.index, 1);
192
219
  break;
193
220
 
194
221
  case 'reorder':
195
- var {
196
- from,
197
- to
198
- } = payload;
199
- list.splice(to, 0, ...list.splice(from, 1));
222
+ list.splice(command.payload.to, 0, ...list.splice(command.payload.from, 1));
200
223
  break;
201
224
 
202
225
  default:
203
- throw new Error('Invalid action found');
226
+ throw new Error('Invalid list command');
204
227
  }
205
228
 
206
229
  return list;
207
230
  }
208
- function parse(payload) {
209
- var command = payload.get('__conform__');
231
+ function applyListCommand(payload) {
232
+ var command = payload.get(listCommandKey);
210
233
 
211
- if (command) {
212
- payload.delete('__conform__');
234
+ if (!command) {
235
+ return payload;
213
236
  }
214
237
 
215
- var value = transform(payload.entries());
238
+ payload.delete(listCommandKey);
216
239
 
217
- if (command) {
218
- try {
219
- if (command instanceof File) {
220
- throw new Error('The __conform__ key is reserved for special command and could not be used for file upload.');
221
- }
240
+ if (command instanceof File) {
241
+ throw new Error("The \"".concat(listCommandKey, "\" key could not be used for file upload"));
242
+ }
222
243
 
223
- var [name, action, json] = command.split('::');
224
- var list = value;
244
+ var result = new FormData();
245
+ var entries = [];
246
+ var [key, listCommand] = parseListCommand(command);
225
247
 
226
- for (var path of getPaths(name)) {
227
- list = list[path];
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);
253
+ }
254
+ }
228
255
 
229
- if (typeof list === 'undefined') {
230
- break;
231
- }
232
- }
256
+ var {
257
+ list
258
+ } = unflatten(entries);
233
259
 
234
- if (!Array.isArray(list)) {
235
- throw new Error('');
236
- }
260
+ if (!Array.isArray(list)) {
261
+ throw new Error('The list command can only be applied to a list');
262
+ }
237
263
 
238
- applyControlCommand(list, action, JSON.parse(json));
239
- } catch (e) {
240
- return {
241
- state: 'rejected',
242
- form: {
243
- value,
244
- error: {
245
- __conform__: e instanceof Error ? e.message : 'Something went wrong'
246
- }
247
- }
248
- };
249
- }
264
+ updateList(list, listCommand);
250
265
 
251
- return {
252
- state: 'modified',
253
- form: {
254
- value,
255
- error: {}
256
- }
257
- };
266
+ for (var [_name, _value] of flatten(list, key)) {
267
+ result.append(_name, _value);
258
268
  }
259
269
 
260
- return {
261
- state: 'accepted',
262
- data: value,
263
- form: {
264
- value,
265
- error: {}
266
- }
267
- };
268
- }
269
- function getFieldElements(fieldset, key) {
270
- var _fieldset$name;
271
-
272
- var name = getName([(_fieldset$name = fieldset.name) !== null && _fieldset$name !== void 0 ? _fieldset$name : '', key]);
273
- var item = fieldset.elements.namedItem(name);
274
- var nodes = item instanceof RadioNodeList ? Array.from(item) : item !== null ? [item] : [];
275
- return nodes.filter(isFieldElement);
270
+ return result;
276
271
  }
277
272
 
278
- exports.applyControlCommand = applyControlCommand;
279
- exports.getControlButtonProps = getControlButtonProps;
280
- exports.getFieldElements = getFieldElements;
281
- exports.getFieldProps = getFieldProps;
273
+ exports.applyListCommand = applyListCommand;
274
+ exports.createSubmission = createSubmission;
275
+ exports.createValidate = createValidate;
276
+ exports.flatten = flatten;
277
+ exports.getFormData = getFormData;
278
+ exports.getFormElement = getFormElement;
279
+ exports.getKey = getKey;
282
280
  exports.getName = getName;
283
281
  exports.getPaths = getPaths;
284
282
  exports.isFieldElement = isFieldElement;
285
- exports.isFieldsetElement = isFieldsetElement;
286
- exports.parse = parse;
287
- exports.reportValidity = reportValidity;
288
- exports.setFieldState = setFieldState;
289
- exports.shouldSkipValidate = shouldSkipValidate;
290
- exports.transform = transform;
283
+ exports.listCommandKey = listCommandKey;
284
+ exports.parseListCommand = parseListCommand;
285
+ exports.serializeListCommand = serializeListCommand;
286
+ exports.setFormError = setFormError;
287
+ exports.setValue = setValue;
288
+ exports.unflatten = unflatten;
289
+ exports.updateList = updateList;
package/module/index.js CHANGED
@@ -1,83 +1,6 @@
1
- import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.js';
2
-
3
- /**
4
- * Data structure of the form value
5
- */
6
-
7
- /**
8
- * Element that maintains a list of fields
9
- * i.e. fieldset.elements
10
- */
11
-
12
- /**
13
- * Element type that might be a candiate of Constraint Validation
14
- */
15
- function isFieldsetElement(element) {
16
- return element instanceof Element && (element.tagName === 'FORM' || element.tagName === 'FIELDSET');
17
- }
18
1
  function isFieldElement(element) {
19
2
  return element instanceof Element && (element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA' || element.tagName === 'BUTTON');
20
3
  }
21
- function setFieldState(field, state) {
22
- if (isFieldsetElement(field)) {
23
- for (var _element of field.elements) {
24
- setFieldState(_element, state);
25
- }
26
-
27
- return;
28
- }
29
-
30
- if (!isFieldElement(field)) {
31
- console.warn('Only input/select/textarea/button element can be touched');
32
- return;
33
- }
34
-
35
- if (state.touched) {
36
- field.dataset.touched = 'true';
37
- } else {
38
- delete field.dataset.touched;
39
- }
40
- }
41
- function reportValidity(fieldset) {
42
- var isValid = true;
43
-
44
- for (var field of fieldset.elements) {
45
- if (isFieldElement(field) && field.dataset.touched && !field.checkValidity()) {
46
- isValid = false;
47
- }
48
- }
49
-
50
- return isValid;
51
- }
52
- function getFieldProps(schema, options) {
53
- var result = {};
54
-
55
- for (var key of Object.keys(schema.fields)) {
56
- var _options$defaultValue, _options$error;
57
-
58
- var constraint = schema.fields[key];
59
-
60
- var props = _objectSpread2({
61
- name: options.name ? "".concat(options.name, ".").concat(key) : key,
62
- form: options.form,
63
- defaultValue: (_options$defaultValue = options.defaultValue) === null || _options$defaultValue === void 0 ? void 0 : _options$defaultValue[key],
64
- error: (_options$error = options.error) === null || _options$error === void 0 ? void 0 : _options$error[key]
65
- }, constraint);
66
-
67
- result[key] = props;
68
- }
69
-
70
- return result;
71
- }
72
- function shouldSkipValidate(event) {
73
- var _event$submitter, _event$submitter2;
74
-
75
- if (((_event$submitter = event.submitter) === null || _event$submitter === void 0 ? void 0 : _event$submitter.tagName) === 'BUTTON' || ((_event$submitter2 = event.submitter) === null || _event$submitter2 === void 0 ? void 0 : _event$submitter2.tagName) === 'INPUT') {
76
- return event.submitter.formNoValidate;
77
- }
78
-
79
- return false;
80
- }
81
4
  function getPaths(name) {
82
5
  var pattern = /(\w+)\[(\d+)\]/;
83
6
 
@@ -95,6 +18,15 @@ function getPaths(name) {
95
18
  return [matches[1], Number(matches[2])];
96
19
  });
97
20
  }
21
+ function getFormData(form, submitter) {
22
+ var payload = new FormData(form);
23
+
24
+ if (submitter !== null && submitter !== void 0 && submitter.name) {
25
+ payload.append(submitter.name, submitter.value);
26
+ }
27
+
28
+ return payload;
29
+ }
98
30
  function getName(paths) {
99
31
  return paths.reduce((name, path) => {
100
32
  if (name === '' || path === '') {
@@ -108,167 +40,230 @@ function getName(paths) {
108
40
  return [name, path].join('.');
109
41
  }, '');
110
42
  }
111
- function transform(entries) {
112
- var result = {};
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);
113
47
 
114
- for (var [key, value] of entries) {
115
- var paths = getPaths(key);
116
- var length = paths.length;
117
- var lastIndex = length - 1;
118
- var index = -1;
119
- var pointer = result;
48
+ if (paths.length > 1) {
49
+ return null;
50
+ }
120
51
 
121
- while (pointer != null && ++index < length) {
122
- var _key = paths[index];
123
- var next = paths[index + 1];
124
- var newValue = value;
52
+ return typeof paths[0] === 'string' ? paths[0] : null;
53
+ }
54
+ function setFormError(form, errors) {
55
+ var firstErrorByName = Object.fromEntries([...errors].reverse());
125
56
 
126
- if (index != lastIndex) {
127
- var _pointer$_key;
57
+ for (var _element of form.elements) {
58
+ var _firstErrorByName$_el;
128
59
 
129
- newValue = (_pointer$_key = pointer[_key]) !== null && _pointer$_key !== void 0 ? _pointer$_key : typeof next === 'number' ? [] : {};
130
- } // if (typeof pointer[key] !== 'undefined') {
131
- // pointer[key] = Array.isArray(pointer[key])
132
- // ? pointer[key].concat(newValue)
133
- // : [pointer[key], newValue];
134
- // } else {
60
+ if (!isFieldElement(_element)) {
61
+ continue;
62
+ }
135
63
 
64
+ _element.setCustomValidity((_firstErrorByName$_el = firstErrorByName[_element.name]) !== null && _firstErrorByName$_el !== void 0 ? _firstErrorByName$_el : '');
65
+ }
66
+ }
67
+ function setValue(target, paths, valueFn) {
68
+ var length = paths.length;
69
+ var lastIndex = length - 1;
70
+ var index = -1;
71
+ var pointer = target;
72
+
73
+ while (pointer != null && ++index < length) {
74
+ var _pointer$key;
75
+
76
+ var key = paths[index];
77
+ var next = paths[index + 1];
78
+ var newValue = index != lastIndex ? (_pointer$key = pointer[key]) !== null && _pointer$key !== void 0 ? _pointer$key : typeof next === 'number' ? [] : {} : valueFn(pointer[key]);
79
+ pointer[key] = newValue;
80
+ pointer = pointer[key];
81
+ }
82
+ }
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
+ }
97
+ }
136
98
 
137
- pointer[_key] = newValue; // }
99
+ return entries;
100
+ }
101
+ function unflatten(entries) {
102
+ var result = {};
138
103
 
139
- pointer = pointer[_key];
140
- }
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
+ }
110
+
111
+ return value;
112
+ });
113
+ };
114
+
115
+ for (var [key, value] of entries) {
116
+ _loop(key, value);
141
117
  }
142
118
 
143
119
  return result;
144
120
  }
145
- function getControlButtonProps(name, action, payload) {
121
+ function createSubmission(payload) {
122
+ var value = {};
123
+
124
+ 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: {}
134
+ }
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'
144
+ }
145
+ }
146
+ };
147
+ }
148
+
146
149
  return {
147
- type: 'submit',
148
- name: '__conform__',
149
- value: [name, action, JSON.stringify(payload)].join('::'),
150
- formNoValidate: true
150
+ state: 'accepted',
151
+ data: value,
152
+ form: {
153
+ value,
154
+ error: {}
155
+ }
156
+ };
157
+ }
158
+ function createValidate(handler) {
159
+ return (form, submitter) => {
160
+ var formData = getFormData(form, submitter);
161
+
162
+ for (var _field of form.elements) {
163
+ if (isFieldElement(_field)) {
164
+ handler(_field, formData);
165
+ }
166
+ }
151
167
  };
152
168
  }
153
- function applyControlCommand(list, action, payload) {
154
- switch (action) {
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;
174
+ }
175
+
176
+ return form;
177
+ }
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
+ }];
192
+ }
193
+ function updateList(list, command) {
194
+ switch (command.type) {
155
195
  case 'prepend':
156
196
  {
157
- var {
158
- defaultValue
159
- } = payload;
160
- list.unshift(defaultValue);
197
+ list.unshift(command.payload.defaultValue);
161
198
  break;
162
199
  }
163
200
 
164
201
  case 'append':
165
202
  {
166
- var {
167
- defaultValue: _defaultValue
168
- } = payload;
169
- list.push(_defaultValue);
203
+ list.push(command.payload.defaultValue);
170
204
  break;
171
205
  }
172
206
 
173
207
  case 'replace':
174
208
  {
175
- var {
176
- defaultValue: _defaultValue2,
177
- index: _index
178
- } = payload;
179
- list.splice(_index, 1, _defaultValue2);
209
+ list.splice(command.payload.index, 1, command.payload.defaultValue);
180
210
  break;
181
211
  }
182
212
 
183
213
  case 'remove':
184
- var {
185
- index
186
- } = payload;
187
- list.splice(index, 1);
214
+ list.splice(command.payload.index, 1);
188
215
  break;
189
216
 
190
217
  case 'reorder':
191
- var {
192
- from,
193
- to
194
- } = payload;
195
- list.splice(to, 0, ...list.splice(from, 1));
218
+ list.splice(command.payload.to, 0, ...list.splice(command.payload.from, 1));
196
219
  break;
197
220
 
198
221
  default:
199
- throw new Error('Invalid action found');
222
+ throw new Error('Invalid list command');
200
223
  }
201
224
 
202
225
  return list;
203
226
  }
204
- function parse(payload) {
205
- var command = payload.get('__conform__');
227
+ function applyListCommand(payload) {
228
+ var command = payload.get(listCommandKey);
206
229
 
207
- if (command) {
208
- payload.delete('__conform__');
230
+ if (!command) {
231
+ return payload;
209
232
  }
210
233
 
211
- var value = transform(payload.entries());
234
+ payload.delete(listCommandKey);
212
235
 
213
- if (command) {
214
- try {
215
- if (command instanceof File) {
216
- throw new Error('The __conform__ key is reserved for special command and could not be used for file upload.');
217
- }
236
+ if (command instanceof File) {
237
+ throw new Error("The \"".concat(listCommandKey, "\" key could not be used for file upload"));
238
+ }
218
239
 
219
- var [name, action, json] = command.split('::');
220
- var list = value;
240
+ var result = new FormData();
241
+ var entries = [];
242
+ var [key, listCommand] = parseListCommand(command);
221
243
 
222
- for (var path of getPaths(name)) {
223
- list = list[path];
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);
249
+ }
250
+ }
224
251
 
225
- if (typeof list === 'undefined') {
226
- break;
227
- }
228
- }
252
+ var {
253
+ list
254
+ } = unflatten(entries);
229
255
 
230
- if (!Array.isArray(list)) {
231
- throw new Error('');
232
- }
256
+ if (!Array.isArray(list)) {
257
+ throw new Error('The list command can only be applied to a list');
258
+ }
233
259
 
234
- applyControlCommand(list, action, JSON.parse(json));
235
- } catch (e) {
236
- return {
237
- state: 'rejected',
238
- form: {
239
- value,
240
- error: {
241
- __conform__: e instanceof Error ? e.message : 'Something went wrong'
242
- }
243
- }
244
- };
245
- }
260
+ updateList(list, listCommand);
246
261
 
247
- return {
248
- state: 'modified',
249
- form: {
250
- value,
251
- error: {}
252
- }
253
- };
262
+ for (var [_name, _value] of flatten(list, key)) {
263
+ result.append(_name, _value);
254
264
  }
255
265
 
256
- return {
257
- state: 'accepted',
258
- data: value,
259
- form: {
260
- value,
261
- error: {}
262
- }
263
- };
264
- }
265
- function getFieldElements(fieldset, key) {
266
- var _fieldset$name;
267
-
268
- var name = getName([(_fieldset$name = fieldset.name) !== null && _fieldset$name !== void 0 ? _fieldset$name : '', key]);
269
- var item = fieldset.elements.namedItem(name);
270
- var nodes = item instanceof RadioNodeList ? Array.from(item) : item !== null ? [item] : [];
271
- return nodes.filter(isFieldElement);
266
+ return result;
272
267
  }
273
268
 
274
- export { applyControlCommand, getControlButtonProps, getFieldElements, getFieldProps, getName, getPaths, isFieldElement, isFieldsetElement, parse, reportValidity, setFieldState, shouldSkipValidate, transform };
269
+ export { applyListCommand, createSubmission, createValidate, flatten, getFormData, getFormElement, getKey, getName, getPaths, isFieldElement, listCommandKey, parseListCommand, serializeListCommand, setFormError, setValue, unflatten, 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.2.0",
5
+ "version": "0.3.0-pre.0",
6
6
  "main": "index.js",
7
7
  "module": "module/index.js",
8
8
  "repository": {