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