@conform-to/dom 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Edmund Hung
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @conform-to/dom
2
+
3
+ > A set of opinionated helpers interacting with the DOM elements
4
+
5
+ ## API Reference
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)
19
+
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
package/index.d.ts ADDED
@@ -0,0 +1,84 @@
1
+ export declare type Constraint<Type> = (Type extends string | number | Date | undefined ? {
2
+ required?: boolean;
3
+ minLength?: number;
4
+ maxLength?: number;
5
+ min?: string;
6
+ max?: string;
7
+ step?: string;
8
+ multiple?: boolean;
9
+ pattern?: string;
10
+ } : {}) & (undefined extends Type ? {
11
+ required?: false;
12
+ } : {
13
+ required: true;
14
+ }) & (Type extends Array<any> ? {
15
+ multiple: true;
16
+ } : {
17
+ multiple?: false;
18
+ });
19
+ export interface FieldConfig<Type = any> {
20
+ name: string;
21
+ initialValue?: FieldsetData<Type, string>;
22
+ error?: FieldsetData<Type, string>;
23
+ form?: string;
24
+ constraint?: Constraint<Type>;
25
+ }
26
+ export declare type Schema<Type extends Record<string, any>> = {
27
+ fields: {
28
+ [Key in keyof Type]-?: Constraint<Type[Key]>;
29
+ };
30
+ validate?: (element: FieldsetElement) => void;
31
+ };
32
+ /**
33
+ * Data structure of the form value
34
+ */
35
+ 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 ? {
36
+ [Key in keyof Type]?: FieldsetData<Type[Key], Value>;
37
+ } : unknown;
38
+ /**
39
+ * Element that maintains a list of fields
40
+ * i.e. fieldset.elements
41
+ */
42
+ export declare type FieldsetElement = HTMLFormElement | HTMLFieldSetElement;
43
+ /**
44
+ * Element type that might be a candiate of Constraint Validation
45
+ */
46
+ export declare type FieldElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement;
47
+ export declare type FormResult<T> = {
48
+ state: 'processed';
49
+ value: FieldsetData<T, string> | null;
50
+ error: FieldsetData<T, string> | null;
51
+ } | {
52
+ state: 'rejected';
53
+ value: FieldsetData<T, string> | null;
54
+ error: FieldsetData<T, string>;
55
+ } | {
56
+ state: 'accepted';
57
+ value: T;
58
+ };
59
+ export declare function isFieldsetElement(element: unknown): element is FieldsetElement;
60
+ export declare function isFieldElement(element: unknown): element is FieldElement;
61
+ export declare function setFieldState(field: unknown, state: {
62
+ touched: boolean;
63
+ }): void;
64
+ export declare function reportValidity(fieldset: FieldsetElement): boolean;
65
+ export declare function createFieldConfig<Type extends Record<string, any>>(schema: Schema<Type>, options: {
66
+ name?: string;
67
+ form?: string;
68
+ initialValue?: FieldsetData<Type, string>;
69
+ error?: FieldsetData<Type, string>;
70
+ }): {
71
+ [Key in keyof Type]-?: FieldConfig<Type[Key]>;
72
+ };
73
+ export declare function shouldSkipValidate(event: SubmitEvent): boolean;
74
+ export declare function getPaths(name?: string): Array<string | number>;
75
+ export declare function getName(paths: Array<string | number>): string;
76
+ export declare function transform(entries: Array<[string, FormDataEntryValue]> | Iterable<[string, FormDataEntryValue]>): unknown;
77
+ export declare function createControlButton(name: string, action: 'prepend' | 'append' | 'remove', data: any): {
78
+ type: 'submit';
79
+ name: string;
80
+ value: string;
81
+ formNoValidate: boolean;
82
+ };
83
+ export declare function parse(payload: FormData | URLSearchParams): FormResult<unknown>;
84
+ export declare function getFieldElements(fieldset: FieldsetElement, key: string): FieldElement[];
package/index.js ADDED
@@ -0,0 +1,253 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
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
+ function isFieldElement(element) {
21
+ return element instanceof Element && (element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA' || element.tagName === 'BUTTON');
22
+ }
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
+ // @ts-expect-error
67
+ constraint
68
+ };
69
+ result[key] = config;
70
+ }
71
+
72
+ return result;
73
+ }
74
+ function shouldSkipValidate(event) {
75
+ var _event$submitter, _event$submitter2;
76
+
77
+ 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') {
78
+ return event.submitter.formNoValidate;
79
+ }
80
+
81
+ return false;
82
+ }
83
+ function getPaths(name) {
84
+ var pattern = /(\w+)\[(\d+)\]/;
85
+
86
+ if (!name) {
87
+ return [];
88
+ }
89
+
90
+ return name.split('.').flatMap(key => {
91
+ var matches = pattern.exec(key);
92
+
93
+ if (!matches) {
94
+ return key;
95
+ }
96
+
97
+ return [matches[1], Number(matches[2])];
98
+ });
99
+ }
100
+ function getName(paths) {
101
+ return paths.reduce((name, path) => {
102
+ if (name === '' || path === '') {
103
+ return [name, path].join('');
104
+ }
105
+
106
+ if (typeof path === 'number') {
107
+ return "".concat(name, "[").concat(path, "]");
108
+ }
109
+
110
+ return [name, path].join('.');
111
+ }, '');
112
+ }
113
+ function transform(entries) {
114
+ var result = {};
115
+
116
+ for (var [key, value] of entries) {
117
+ var paths = getPaths(key);
118
+ var length = paths.length;
119
+ var lastIndex = length - 1;
120
+ var index = -1;
121
+ var pointer = result;
122
+
123
+ while (pointer != null && ++index < length) {
124
+ var _key = paths[index];
125
+ var next = paths[index + 1];
126
+ var newValue = value;
127
+
128
+ if (index != lastIndex) {
129
+ var _pointer$_key;
130
+
131
+ newValue = (_pointer$_key = pointer[_key]) !== null && _pointer$_key !== void 0 ? _pointer$_key : typeof next === 'number' ? [] : {};
132
+ } // if (typeof pointer[key] !== 'undefined') {
133
+ // pointer[key] = Array.isArray(pointer[key])
134
+ // ? pointer[key].concat(newValue)
135
+ // : [pointer[key], newValue];
136
+ // } else {
137
+
138
+
139
+ pointer[_key] = newValue; // }
140
+
141
+ pointer = pointer[_key];
142
+ }
143
+ }
144
+
145
+ return result;
146
+ }
147
+ function createControlButton(name, action, data) {
148
+ return {
149
+ type: 'submit',
150
+ name: '__conform__',
151
+ value: [name, action, JSON.stringify(data)].join('::'),
152
+ formNoValidate: true
153
+ };
154
+ }
155
+ function parse(payload) {
156
+ var command = payload.get('__conform__');
157
+
158
+ if (command) {
159
+ payload.delete('__conform__');
160
+ }
161
+
162
+ var value = transform(payload.entries());
163
+
164
+ if (command) {
165
+ try {
166
+ if (command instanceof File) {
167
+ throw new Error('The __conform__ key is reserved for special command and could not be used for file upload.');
168
+ }
169
+
170
+ var [name, action, json] = command.split('::');
171
+ var list = value;
172
+
173
+ for (var path of getPaths(name)) {
174
+ list = list[path];
175
+
176
+ if (typeof list === 'undefined') {
177
+ break;
178
+ }
179
+ }
180
+
181
+ if (!Array.isArray(list)) {
182
+ throw new Error('');
183
+ }
184
+
185
+ switch (action) {
186
+ case 'prepend':
187
+ {
188
+ var initialValue = JSON.parse(json);
189
+ list.unshift(initialValue);
190
+ break;
191
+ }
192
+
193
+ case 'append':
194
+ {
195
+ var _initialValue = JSON.parse(json);
196
+
197
+ list.push(_initialValue);
198
+ break;
199
+ }
200
+
201
+ case 'remove':
202
+ var {
203
+ index
204
+ } = JSON.parse(json);
205
+ list.splice(index, 1);
206
+ break;
207
+
208
+ default:
209
+ throw new Error('Invalid action found; Only `prepend`, `append` and `remove` is accepted');
210
+ }
211
+ } catch (e) {
212
+ return {
213
+ state: 'rejected',
214
+ value,
215
+ error: {
216
+ __conform__: e instanceof Error ? e.message : 'Something went wrong'
217
+ }
218
+ };
219
+ }
220
+
221
+ return {
222
+ state: 'processed',
223
+ value,
224
+ error: null
225
+ };
226
+ }
227
+
228
+ return {
229
+ state: 'accepted',
230
+ value
231
+ };
232
+ }
233
+ function getFieldElements(fieldset, key) {
234
+ var _fieldset$name;
235
+
236
+ var name = getName([(_fieldset$name = fieldset.name) !== null && _fieldset$name !== void 0 ? _fieldset$name : '', key]);
237
+ var item = fieldset.elements.namedItem(name);
238
+ var nodes = item instanceof RadioNodeList ? Array.from(item) : item !== null ? [item] : [];
239
+ return nodes.filter(isFieldElement);
240
+ }
241
+
242
+ exports.createControlButton = createControlButton;
243
+ exports.createFieldConfig = createFieldConfig;
244
+ exports.getFieldElements = getFieldElements;
245
+ exports.getName = getName;
246
+ exports.getPaths = getPaths;
247
+ exports.isFieldElement = isFieldElement;
248
+ exports.isFieldsetElement = isFieldsetElement;
249
+ exports.parse = parse;
250
+ exports.reportValidity = reportValidity;
251
+ exports.setFieldState = setFieldState;
252
+ exports.shouldSkipValidate = shouldSkipValidate;
253
+ exports.transform = transform;
@@ -0,0 +1,238 @@
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
+ function isFieldElement(element) {
17
+ return element instanceof Element && (element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA' || element.tagName === 'BUTTON');
18
+ }
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
+ // @ts-expect-error
63
+ constraint
64
+ };
65
+ result[key] = config;
66
+ }
67
+
68
+ return result;
69
+ }
70
+ function shouldSkipValidate(event) {
71
+ var _event$submitter, _event$submitter2;
72
+
73
+ 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') {
74
+ return event.submitter.formNoValidate;
75
+ }
76
+
77
+ return false;
78
+ }
79
+ function getPaths(name) {
80
+ var pattern = /(\w+)\[(\d+)\]/;
81
+
82
+ if (!name) {
83
+ return [];
84
+ }
85
+
86
+ return name.split('.').flatMap(key => {
87
+ var matches = pattern.exec(key);
88
+
89
+ if (!matches) {
90
+ return key;
91
+ }
92
+
93
+ return [matches[1], Number(matches[2])];
94
+ });
95
+ }
96
+ function getName(paths) {
97
+ return paths.reduce((name, path) => {
98
+ if (name === '' || path === '') {
99
+ return [name, path].join('');
100
+ }
101
+
102
+ if (typeof path === 'number') {
103
+ return "".concat(name, "[").concat(path, "]");
104
+ }
105
+
106
+ return [name, path].join('.');
107
+ }, '');
108
+ }
109
+ function transform(entries) {
110
+ var result = {};
111
+
112
+ for (var [key, value] of entries) {
113
+ var paths = getPaths(key);
114
+ var length = paths.length;
115
+ var lastIndex = length - 1;
116
+ var index = -1;
117
+ var pointer = result;
118
+
119
+ while (pointer != null && ++index < length) {
120
+ var _key = paths[index];
121
+ var next = paths[index + 1];
122
+ var newValue = value;
123
+
124
+ if (index != lastIndex) {
125
+ var _pointer$_key;
126
+
127
+ newValue = (_pointer$_key = pointer[_key]) !== null && _pointer$_key !== void 0 ? _pointer$_key : typeof next === 'number' ? [] : {};
128
+ } // if (typeof pointer[key] !== 'undefined') {
129
+ // pointer[key] = Array.isArray(pointer[key])
130
+ // ? pointer[key].concat(newValue)
131
+ // : [pointer[key], newValue];
132
+ // } else {
133
+
134
+
135
+ pointer[_key] = newValue; // }
136
+
137
+ pointer = pointer[_key];
138
+ }
139
+ }
140
+
141
+ return result;
142
+ }
143
+ function createControlButton(name, action, data) {
144
+ return {
145
+ type: 'submit',
146
+ name: '__conform__',
147
+ value: [name, action, JSON.stringify(data)].join('::'),
148
+ formNoValidate: true
149
+ };
150
+ }
151
+ function parse(payload) {
152
+ var command = payload.get('__conform__');
153
+
154
+ if (command) {
155
+ payload.delete('__conform__');
156
+ }
157
+
158
+ var value = transform(payload.entries());
159
+
160
+ if (command) {
161
+ try {
162
+ if (command instanceof File) {
163
+ throw new Error('The __conform__ key is reserved for special command and could not be used for file upload.');
164
+ }
165
+
166
+ var [name, action, json] = command.split('::');
167
+ var list = value;
168
+
169
+ for (var path of getPaths(name)) {
170
+ list = list[path];
171
+
172
+ if (typeof list === 'undefined') {
173
+ break;
174
+ }
175
+ }
176
+
177
+ if (!Array.isArray(list)) {
178
+ throw new Error('');
179
+ }
180
+
181
+ switch (action) {
182
+ case 'prepend':
183
+ {
184
+ var initialValue = JSON.parse(json);
185
+ list.unshift(initialValue);
186
+ break;
187
+ }
188
+
189
+ case 'append':
190
+ {
191
+ var _initialValue = JSON.parse(json);
192
+
193
+ list.push(_initialValue);
194
+ break;
195
+ }
196
+
197
+ case 'remove':
198
+ var {
199
+ index
200
+ } = JSON.parse(json);
201
+ list.splice(index, 1);
202
+ break;
203
+
204
+ default:
205
+ throw new Error('Invalid action found; Only `prepend`, `append` and `remove` is accepted');
206
+ }
207
+ } catch (e) {
208
+ return {
209
+ state: 'rejected',
210
+ value,
211
+ error: {
212
+ __conform__: e instanceof Error ? e.message : 'Something went wrong'
213
+ }
214
+ };
215
+ }
216
+
217
+ return {
218
+ state: 'processed',
219
+ value,
220
+ error: null
221
+ };
222
+ }
223
+
224
+ return {
225
+ state: 'accepted',
226
+ value
227
+ };
228
+ }
229
+ function getFieldElements(fieldset, key) {
230
+ var _fieldset$name;
231
+
232
+ var name = getName([(_fieldset$name = fieldset.name) !== null && _fieldset$name !== void 0 ? _fieldset$name : '', key]);
233
+ var item = fieldset.elements.namedItem(name);
234
+ var nodes = item instanceof RadioNodeList ? Array.from(item) : item !== null ? [item] : [];
235
+ return nodes.filter(isFieldElement);
236
+ }
237
+
238
+ export { createControlButton, createFieldConfig, getFieldElements, getName, getPaths, isFieldElement, isFieldsetElement, parse, reportValidity, setFieldState, shouldSkipValidate, transform };
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@conform-to/dom",
3
+ "description": "A set of opinionated helpers built on top of the Constraint Validation API",
4
+ "license": "MIT",
5
+ "version": "0.1.0",
6
+ "main": "index.js",
7
+ "module": "module/index.js",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/edmundhung/conform",
11
+ "directory": "packages/conform-dom"
12
+ },
13
+ "author": {
14
+ "name": "Edmund Hung",
15
+ "email": "me@edmund.dev",
16
+ "url": "https://edmund.dev"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/edmundhung/conform/issues"
20
+ },
21
+ "keywords": [
22
+ "constraint-validation",
23
+ "form",
24
+ "form-validation",
25
+ "validation",
26
+ "dom"
27
+ ],
28
+ "sideEffects": false
29
+ }