@keysdown/form-wrapper 0.0.2
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 +21 -0
- package/README.md +529 -0
- package/package.json +23 -0
- package/src/core/Errors.ts +21 -0
- package/src/core/Form.ts +197 -0
- package/src/core/Messages.ts +5 -0
- package/src/core/Rules.ts +4 -0
- package/src/core/Validation.ts +11 -0
- package/src/main.ts +9 -0
- package/src/types/collections.ts +3 -0
- package/src/types/fields.ts +19 -0
- package/src/types/messages.ts +3 -0
- package/src/types/rules.ts +3 -0
- package/src/types/validations.ts +4 -0
- package/src/types/values.ts +3 -0
- package/src/utils/collections.ts +78 -0
- package/src/utils/fields.ts +23 -0
- package/src/utils/helpers.ts +31 -0
- package/src/utils/validations.ts +17 -0
- package/tsconfig.json +23 -0
- package/vite.config.js +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Keys Down
|
|
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,529 @@
|
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
Form Wrapper (Beta)
|
|
3
|
+
</h1>
|
|
4
|
+
|
|
5
|
+
> A package that allows you to easily manage forms, with Form Wrapper it is possible to perform validations with error messages, in addition to managing the state of the forms.
|
|
6
|
+
|
|
7
|
+
> This package is currently in Beta and is being tested in live applications, feel free to help in the development and maturation of the package.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```shell
|
|
12
|
+
npm install @keysdown/form-wrapper --save
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Basic example
|
|
16
|
+
|
|
17
|
+
Basic example using Vue.
|
|
18
|
+
|
|
19
|
+
```vue
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<form @submit.prevent="submit">
|
|
23
|
+
<input type="text" v-model="form.first_name"/>
|
|
24
|
+
<input type="text" v-model="form.last_name"/>
|
|
25
|
+
<input type="text" v-model="form.username"/>
|
|
26
|
+
<button type="submit" :disabled="form.awaiting"/>
|
|
27
|
+
</form>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import axios from 'axios'
|
|
32
|
+
import {ref} from 'vue'
|
|
33
|
+
import Form from '@keysdown/form-wrapper'
|
|
34
|
+
|
|
35
|
+
const form = ref(Form({
|
|
36
|
+
first_name: null,
|
|
37
|
+
last_name: null,
|
|
38
|
+
username: null
|
|
39
|
+
}))
|
|
40
|
+
|
|
41
|
+
const submit = () => {
|
|
42
|
+
form.value.setAwaiting(true)
|
|
43
|
+
|
|
44
|
+
axios.post('some-api', form.value.values())
|
|
45
|
+
.then(({data}) => {
|
|
46
|
+
form.value.reset()
|
|
47
|
+
|
|
48
|
+
// ...
|
|
49
|
+
})
|
|
50
|
+
.catch(({errors}) => {
|
|
51
|
+
form.value.errors.fill(errors)
|
|
52
|
+
|
|
53
|
+
form.value.setAwaiting(false)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Basic example with validation
|
|
60
|
+
|
|
61
|
+
Basic example with validation using Vue.
|
|
62
|
+
|
|
63
|
+
```vue
|
|
64
|
+
<template>
|
|
65
|
+
<form @submit.prevent="submit">
|
|
66
|
+
<input type="text" v-model="form.first_name"/>
|
|
67
|
+
<input type="text" v-model="form.last_name"/>
|
|
68
|
+
<input type="text" v-model="form.username"/>
|
|
69
|
+
<button type="submit" :disabled="form.awaiting"/>
|
|
70
|
+
</form>
|
|
71
|
+
</template>
|
|
72
|
+
|
|
73
|
+
<script setup lang="ts">
|
|
74
|
+
import axios from 'axios'
|
|
75
|
+
import {ref} from 'vue'
|
|
76
|
+
import Form from '@keysdown/form-wrapper'
|
|
77
|
+
|
|
78
|
+
const form = ref(Form({
|
|
79
|
+
first_name: {
|
|
80
|
+
value: null,
|
|
81
|
+
rules: ['required'],
|
|
82
|
+
messages: {
|
|
83
|
+
required: 'The first name field is required.'
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
last_name: {
|
|
87
|
+
value: null,
|
|
88
|
+
rules: ['required'],
|
|
89
|
+
messages: {
|
|
90
|
+
required: 'The last name field is required.'
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
username: {
|
|
94
|
+
value: null,
|
|
95
|
+
rules: ['required', 'min:6'],
|
|
96
|
+
messages: {
|
|
97
|
+
required: 'The username field is required.',
|
|
98
|
+
min: 'The username field must have at least 6 characters'
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}))
|
|
102
|
+
|
|
103
|
+
const submit = () => {
|
|
104
|
+
form.value.validate()
|
|
105
|
+
.then((form) => {
|
|
106
|
+
form.setAwaiting(true)
|
|
107
|
+
|
|
108
|
+
axios.post('some-api', form.values())
|
|
109
|
+
.then(({data}) => {
|
|
110
|
+
form.reset()
|
|
111
|
+
|
|
112
|
+
// ...
|
|
113
|
+
})
|
|
114
|
+
.catch(({errors}) => {
|
|
115
|
+
form.errors.fill(errors)
|
|
116
|
+
|
|
117
|
+
form.setAwaiting(false)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
.catch((form) => {
|
|
121
|
+
console.log('error')
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
</script>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
## Form methods
|
|
129
|
+
|
|
130
|
+
### addField(field, value)
|
|
131
|
+
|
|
132
|
+
Method used to add a single field to the form.
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
form.addField('username', null)
|
|
136
|
+
|
|
137
|
+
form.addField('username', {
|
|
138
|
+
validation: {
|
|
139
|
+
rules: ['required'],
|
|
140
|
+
messages: {
|
|
141
|
+
required: 'The username field is required.'
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### addFields(fields)
|
|
148
|
+
|
|
149
|
+
Method used to add multiple fields to the form.
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
form.addFields({
|
|
153
|
+
full_name: null,
|
|
154
|
+
username: null
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
form.addFields({
|
|
158
|
+
full_name: {
|
|
159
|
+
validation: {
|
|
160
|
+
rules: ['required'],
|
|
161
|
+
messages: {
|
|
162
|
+
required: 'The full name field is required.'
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
username: {
|
|
167
|
+
validation: {
|
|
168
|
+
rules: ['required'],
|
|
169
|
+
messages: {
|
|
170
|
+
required: 'The username field is required.'
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### fill(data, updateOriginalValues = false)
|
|
178
|
+
|
|
179
|
+
Method used when you want to fill in several form fields at once.
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
const form = Form({
|
|
183
|
+
username: null
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
console.log(form.values()) // {username: null}
|
|
187
|
+
|
|
188
|
+
form.fill({
|
|
189
|
+
username: 'keysdown'
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
console.log(form.values()) // {username: 'keysdown'}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### removeField(field)
|
|
196
|
+
|
|
197
|
+
Method used when you want to remove a field from the form.
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
const form = Form({
|
|
201
|
+
username: null
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
console.log(form.values()) // {username: null}
|
|
205
|
+
|
|
206
|
+
form.removeField('username')
|
|
207
|
+
|
|
208
|
+
console.log(form.values()) // {}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### removeFields(fields)
|
|
212
|
+
|
|
213
|
+
Method used to remove multiple fields to the form.
|
|
214
|
+
|
|
215
|
+
```js
|
|
216
|
+
const form = Form({
|
|
217
|
+
username: null
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
console.log(form.values()) // {username: null}
|
|
221
|
+
|
|
222
|
+
form.removeFields(['username'])
|
|
223
|
+
|
|
224
|
+
console.log(form.values()) // {}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### reset()
|
|
228
|
+
|
|
229
|
+
Method used to reset all form values to original state.
|
|
230
|
+
|
|
231
|
+
```js
|
|
232
|
+
const form = Form({
|
|
233
|
+
username: null
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
form.username = 'keysdown'
|
|
237
|
+
|
|
238
|
+
console.log(form.values()) // {username: 'keysdown'}
|
|
239
|
+
|
|
240
|
+
form.reset()
|
|
241
|
+
|
|
242
|
+
console.log(form.values()) // {username: null}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### setAwaiting(awaiting = true)
|
|
246
|
+
|
|
247
|
+
Method used to change the form loading state.
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
const form = Form({
|
|
251
|
+
username: null
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
console.log(form.awaiting) // false
|
|
255
|
+
|
|
256
|
+
form.setAwaiting()
|
|
257
|
+
|
|
258
|
+
console.log(form.awaiting) // true
|
|
259
|
+
|
|
260
|
+
form.setAwaiting(false)
|
|
261
|
+
|
|
262
|
+
console.log(form.awaiting) // false
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### validate(field = null)
|
|
266
|
+
|
|
267
|
+
Method used to validate the entire form or a specific field.
|
|
268
|
+
|
|
269
|
+
```js
|
|
270
|
+
const form = Form({
|
|
271
|
+
username: {
|
|
272
|
+
value: null,
|
|
273
|
+
rules: ['required'],
|
|
274
|
+
messages: {
|
|
275
|
+
required: 'The username field is required.'
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
form.validate('username')
|
|
281
|
+
.then(() => {
|
|
282
|
+
//...
|
|
283
|
+
})
|
|
284
|
+
.catch(() => {
|
|
285
|
+
//...
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
form.validate()
|
|
289
|
+
.then((form) => {
|
|
290
|
+
form.setAwaiting(true)
|
|
291
|
+
|
|
292
|
+
// ...
|
|
293
|
+
})
|
|
294
|
+
.catch((form) => {
|
|
295
|
+
console.log('error')
|
|
296
|
+
})
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### validateField(field)
|
|
300
|
+
|
|
301
|
+
Method used to validate a specific field.
|
|
302
|
+
|
|
303
|
+
```js
|
|
304
|
+
const form = Form({
|
|
305
|
+
username: {
|
|
306
|
+
value: null,
|
|
307
|
+
rules: ['required'],
|
|
308
|
+
messages: {
|
|
309
|
+
required: 'The username field is required.'
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
form.validateField('username')
|
|
315
|
+
.then(() => {
|
|
316
|
+
//...
|
|
317
|
+
})
|
|
318
|
+
.catch(() => {
|
|
319
|
+
//...
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
// Same as:
|
|
323
|
+
|
|
324
|
+
form.validate('username')
|
|
325
|
+
.then(() => {
|
|
326
|
+
//...
|
|
327
|
+
})
|
|
328
|
+
.catch(() => {
|
|
329
|
+
//...
|
|
330
|
+
})
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### validateForm()
|
|
334
|
+
|
|
335
|
+
Method used to validate the entire form.
|
|
336
|
+
|
|
337
|
+
```js
|
|
338
|
+
const form = Form({
|
|
339
|
+
username: {
|
|
340
|
+
value: null,
|
|
341
|
+
rules: ['required'],
|
|
342
|
+
messages: {
|
|
343
|
+
required: 'The username field is required.'
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
form.validateForm()
|
|
349
|
+
.then((form) => {
|
|
350
|
+
//...
|
|
351
|
+
})
|
|
352
|
+
.catch((form) => {
|
|
353
|
+
//...
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
// Same as:
|
|
357
|
+
|
|
358
|
+
form.validate()
|
|
359
|
+
.then((form) => {
|
|
360
|
+
form.setAwaiting(true)
|
|
361
|
+
|
|
362
|
+
// ...
|
|
363
|
+
})
|
|
364
|
+
.catch((form) => {
|
|
365
|
+
console.log('error')
|
|
366
|
+
})
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### values()
|
|
370
|
+
|
|
371
|
+
Method used to access all form values in json format.
|
|
372
|
+
|
|
373
|
+
```js
|
|
374
|
+
const form = Form({
|
|
375
|
+
username: null
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
form.username = 'keysdown'
|
|
379
|
+
|
|
380
|
+
axios.post('some-api', form.values())
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### valuesAsFormData()
|
|
384
|
+
|
|
385
|
+
Method used to access all form values as form data.
|
|
386
|
+
|
|
387
|
+
```js
|
|
388
|
+
const form = Form({
|
|
389
|
+
username: null
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
form.username = 'keysdown'
|
|
393
|
+
|
|
394
|
+
axios.post('some-api', form.valuesAsFormData(), {
|
|
395
|
+
headers: {
|
|
396
|
+
'Content-Type': 'multipart/form-data'
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
## Validation
|
|
402
|
+
|
|
403
|
+
There is a property in the form object dedicated to validations, you can access everything related to validations through the `validation` property:
|
|
404
|
+
|
|
405
|
+
```js
|
|
406
|
+
form.validation.errors.all()
|
|
407
|
+
|
|
408
|
+
form.validation.messages.all()
|
|
409
|
+
|
|
410
|
+
form.validation.rules.all()
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
You can also use the following shortcuts:
|
|
414
|
+
|
|
415
|
+
```js
|
|
416
|
+
form.errors.all()
|
|
417
|
+
|
|
418
|
+
form.messages.all()
|
|
419
|
+
|
|
420
|
+
form.rules.all()
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Validation collections
|
|
424
|
+
|
|
425
|
+
Both errors, messages and rules work using collections, the methods for managing collections are as follows:
|
|
426
|
+
|
|
427
|
+
#### all()
|
|
428
|
+
|
|
429
|
+
Returns all items in the collection.
|
|
430
|
+
|
|
431
|
+
```js
|
|
432
|
+
form.errors.all()
|
|
433
|
+
form.messages.all()
|
|
434
|
+
form.rules.all()
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
#### first(key)
|
|
438
|
+
|
|
439
|
+
Returns the first item in the collection.
|
|
440
|
+
|
|
441
|
+
```js
|
|
442
|
+
form.errors.first('username')
|
|
443
|
+
form.messages.first('username')
|
|
444
|
+
form.rules.first('username')
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
#### any()
|
|
448
|
+
|
|
449
|
+
Returns true if the collection has any items and false if it is empty.
|
|
450
|
+
|
|
451
|
+
```js
|
|
452
|
+
form.errors.any()
|
|
453
|
+
form.messages.any()
|
|
454
|
+
form.rules.any()
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
#### fill(items)
|
|
458
|
+
|
|
459
|
+
Inserts values into a collection, replacing any previous values.
|
|
460
|
+
|
|
461
|
+
```js
|
|
462
|
+
form.errors.fill({
|
|
463
|
+
username: [
|
|
464
|
+
'The username field is required.'
|
|
465
|
+
]
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
form.messages.fill({
|
|
469
|
+
username: {
|
|
470
|
+
required: 'The username field is required.'
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
form.rules.fill({
|
|
475
|
+
username: ['required']
|
|
476
|
+
})
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
#### push(key, data)
|
|
480
|
+
|
|
481
|
+
Inserts a single value into an item in a collection.
|
|
482
|
+
|
|
483
|
+
```js
|
|
484
|
+
form.errors.push('username', 'The username field is required.')
|
|
485
|
+
|
|
486
|
+
form.messages.push('username', {
|
|
487
|
+
required: 'The username field is required.'
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
form.rules.push('username', 'required')
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
#### has(key)
|
|
494
|
+
|
|
495
|
+
Checking if the collection has an item with the key.
|
|
496
|
+
|
|
497
|
+
```js
|
|
498
|
+
form.errors.has('username')
|
|
499
|
+
form.messages.has('username')
|
|
500
|
+
form.rules.has('username')
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
#### unset(key)
|
|
504
|
+
|
|
505
|
+
Remove an item from the collection.
|
|
506
|
+
|
|
507
|
+
```js
|
|
508
|
+
form.errors.unset('username')
|
|
509
|
+
form.messages.unset('username')
|
|
510
|
+
form.rules.unset('username')
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
#### clear(key)
|
|
514
|
+
|
|
515
|
+
Clears the entire collection, making it empty.
|
|
516
|
+
|
|
517
|
+
```js
|
|
518
|
+
form.errors.clear()
|
|
519
|
+
form.messages.clear()
|
|
520
|
+
form.rules.clear()
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Validation rules
|
|
524
|
+
|
|
525
|
+
There are some predefined validation rules that you can use in the form:
|
|
526
|
+
|
|
527
|
+
#### required
|
|
528
|
+
|
|
529
|
+
Used when a field is required
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@keysdown/form-wrapper",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "JavaScript wrapper for forms",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/form-wrapper.umd.js",
|
|
7
|
+
"module": "./dist/form-wrapper.es.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/form-wrapper.es.js",
|
|
11
|
+
"require": "./dist/form-wrapper.umd.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "vite",
|
|
16
|
+
"build": "tsc && vite build",
|
|
17
|
+
"preview": "vite preview"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"typescript": "^5.5.3",
|
|
21
|
+
"vite": "^5.4.1"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {Collection} from '../utils/collections'
|
|
2
|
+
|
|
3
|
+
export class Errors extends Collection<string[]> {
|
|
4
|
+
public push(
|
|
5
|
+
item: string,
|
|
6
|
+
data: any,
|
|
7
|
+
): this {
|
|
8
|
+
const currentItem = this.get(item) || []
|
|
9
|
+
|
|
10
|
+
this.collection = {
|
|
11
|
+
...this.collection,
|
|
12
|
+
[item]: [...currentItem, data]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return this
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public get(key: string): string[] | null {
|
|
19
|
+
return super.get(key, [])
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/core/Form.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {Field, FieldDeclaration, Fields, RawFields} from '../types/fields.ts'
|
|
2
|
+
import {generateFieldDeclaration} from '../utils/fields.ts'
|
|
3
|
+
import {Values} from '../types/values.ts'
|
|
4
|
+
import {Validation} from './Validation.ts'
|
|
5
|
+
import {Rule} from '../utils/validations.ts'
|
|
6
|
+
import {objectToFormData} from '../utils/helpers.ts'
|
|
7
|
+
import {ErrorMessage} from '../types/messages.ts'
|
|
8
|
+
|
|
9
|
+
export class Form {
|
|
10
|
+
[key: string]: any
|
|
11
|
+
|
|
12
|
+
public awaiting: boolean = false
|
|
13
|
+
|
|
14
|
+
public originalValues: Values = {}
|
|
15
|
+
|
|
16
|
+
public validation: Validation = new Validation()
|
|
17
|
+
|
|
18
|
+
public constructor(
|
|
19
|
+
fields: Fields | RawFields
|
|
20
|
+
) {
|
|
21
|
+
this.addFields(fields)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public addField(
|
|
25
|
+
field: string,
|
|
26
|
+
value: Field
|
|
27
|
+
): this {
|
|
28
|
+
if (typeof value === 'object' && 'value' in value) {
|
|
29
|
+
const fieldDeclaration: FieldDeclaration = generateFieldDeclaration(value)
|
|
30
|
+
|
|
31
|
+
this[field] = fieldDeclaration.value
|
|
32
|
+
|
|
33
|
+
this.originalValues[field] = fieldDeclaration.value
|
|
34
|
+
|
|
35
|
+
this.validation.messages.push(field, fieldDeclaration.validation.messages)
|
|
36
|
+
|
|
37
|
+
this.validation.rules.push(field, fieldDeclaration.validation.rules)
|
|
38
|
+
} else {
|
|
39
|
+
this[field] = value
|
|
40
|
+
|
|
41
|
+
this.originalValues[field] = value
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return this
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public addFields(
|
|
48
|
+
fields: Fields | RawFields
|
|
49
|
+
): this {
|
|
50
|
+
Object.keys(fields).forEach((field: string) => {
|
|
51
|
+
this.addField(field, fields[field])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return this
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public get errors() {
|
|
58
|
+
return this.validation.errors
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public fill(
|
|
62
|
+
data: { [key: string]: any },
|
|
63
|
+
updateOriginalValues: boolean = false
|
|
64
|
+
): this {
|
|
65
|
+
Object.keys(data).forEach((field: string) => {
|
|
66
|
+
let value = data[field]
|
|
67
|
+
|
|
68
|
+
if (updateOriginalValues) {
|
|
69
|
+
this.originalValues[field] = value
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this[field] = value
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return this
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public get messages() {
|
|
79
|
+
return this.validation.messages
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public removeField(
|
|
83
|
+
field: string
|
|
84
|
+
): this {
|
|
85
|
+
delete this[field]
|
|
86
|
+
|
|
87
|
+
delete this.originalValues[field]
|
|
88
|
+
|
|
89
|
+
this.validation.messages.unset(field)
|
|
90
|
+
|
|
91
|
+
this.validation.rules.unset(field)
|
|
92
|
+
|
|
93
|
+
return this
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public removeFields(
|
|
97
|
+
fields: string[]
|
|
98
|
+
): this {
|
|
99
|
+
fields.forEach((field: string) => {
|
|
100
|
+
this.removeField(field)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
return this
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public reset(): this {
|
|
107
|
+
this.validation.errors.clear()
|
|
108
|
+
|
|
109
|
+
Object.keys(this.originalValues).forEach((field: string) => {
|
|
110
|
+
this[field] = this.originalValues[field]
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
return this
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public get rules() {
|
|
117
|
+
return this.validation.rules
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public setAwaiting(
|
|
121
|
+
awaiting: boolean = true
|
|
122
|
+
): this {
|
|
123
|
+
this.awaiting = awaiting
|
|
124
|
+
|
|
125
|
+
return this
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public validate(
|
|
129
|
+
field: string | null = null
|
|
130
|
+
): Promise<this | void> {
|
|
131
|
+
return field ? this.validateField(field) : this.validateForm()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public validateField(
|
|
135
|
+
field: string
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
this.validation.errors.unset(field)
|
|
138
|
+
|
|
139
|
+
const rules = this.validation.rules.get(field)
|
|
140
|
+
|
|
141
|
+
if (rules && rules.length > 0) {
|
|
142
|
+
const validations = rules.map((
|
|
143
|
+
rule: string
|
|
144
|
+
) => {
|
|
145
|
+
const ruleParts = rule.split(':')
|
|
146
|
+
|
|
147
|
+
const ruleName: string = ruleParts[0]
|
|
148
|
+
const ruleAttributes: string[] = ruleParts.length === 2 ? ruleParts[1].split(',') : []
|
|
149
|
+
|
|
150
|
+
if (ruleName in Rule) {
|
|
151
|
+
return Rule[ruleName](this[field], ruleAttributes)
|
|
152
|
+
.catch((error?: string) => {
|
|
153
|
+
const errorMessage: ErrorMessage | null = this.validation.messages.get(field)
|
|
154
|
+
|
|
155
|
+
if (errorMessage && ruleName in errorMessage) {
|
|
156
|
+
this.validation.errors.push(field, errorMessage[ruleName])
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return Promise.reject(error)
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return Promise.reject(new Error(`There is no validation rule called "${rule}"`))
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return Promise.all(validations).then(() => {
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return Promise.resolve()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public validateForm(): Promise<this> {
|
|
175
|
+
const validations = Object.keys(this.originalValues).map(
|
|
176
|
+
(field: string) => this.validateField(field)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return Promise.all(validations)
|
|
180
|
+
.then(() => Promise.resolve(this))
|
|
181
|
+
.catch(() => Promise.reject(this))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public values(): Values {
|
|
185
|
+
const values: Values = {}
|
|
186
|
+
|
|
187
|
+
Object.keys(this.originalValues).forEach((field: string): void => {
|
|
188
|
+
values[field] = this[field]
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
return values
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public valuesAsFormData() {
|
|
195
|
+
return objectToFormData(this.values())
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {Messages} from './Messages.ts'
|
|
2
|
+
import {Rules} from './Rules.ts'
|
|
3
|
+
import {Errors} from './Errors.ts'
|
|
4
|
+
|
|
5
|
+
export class Validation {
|
|
6
|
+
public errors: Errors = new Errors()
|
|
7
|
+
|
|
8
|
+
public messages: Messages = new Messages()
|
|
9
|
+
|
|
10
|
+
public rules: Rules = new Rules()
|
|
11
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Validation } from './validations'
|
|
2
|
+
|
|
3
|
+
export interface Field {
|
|
4
|
+
validation?: object,
|
|
5
|
+
value: any
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface FieldDeclaration {
|
|
9
|
+
validation: Validation,
|
|
10
|
+
value: any
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Fields {
|
|
14
|
+
[key: string]: Field
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RawFields {
|
|
18
|
+
[key: string]: any
|
|
19
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {Items} from '../types/collections'
|
|
2
|
+
|
|
3
|
+
export class Collection<T> {
|
|
4
|
+
public collection: Items<T> = {}
|
|
5
|
+
|
|
6
|
+
public all(): Items<T> {
|
|
7
|
+
return this.collection
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public first(
|
|
11
|
+
key: string
|
|
12
|
+
): T | null {
|
|
13
|
+
const data = this.get(key)
|
|
14
|
+
|
|
15
|
+
if (data) {
|
|
16
|
+
return Array.isArray(data) ? data[0] : data
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public any(): boolean {
|
|
23
|
+
return Object.keys(this.collection).length > 0
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public fill(
|
|
27
|
+
items: Items<T>
|
|
28
|
+
): this {
|
|
29
|
+
this.collection = items
|
|
30
|
+
|
|
31
|
+
return this
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public push(
|
|
35
|
+
key: string,
|
|
36
|
+
data: any
|
|
37
|
+
): this {
|
|
38
|
+
this.collection = {
|
|
39
|
+
...this.collection,
|
|
40
|
+
[key]: data
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return this
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public has(
|
|
47
|
+
key: string
|
|
48
|
+
): boolean {
|
|
49
|
+
return this.collection.hasOwnProperty(key)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public get(
|
|
53
|
+
item: string,
|
|
54
|
+
defaultValue: T | null = null
|
|
55
|
+
): T | null {
|
|
56
|
+
if (!this.has(item)) {
|
|
57
|
+
return defaultValue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return this.collection[item]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public unset(
|
|
64
|
+
key: string
|
|
65
|
+
): this {
|
|
66
|
+
if (this.has(key)) {
|
|
67
|
+
delete this.collection[key]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return this
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public clear(): this {
|
|
74
|
+
this.collection = {}
|
|
75
|
+
|
|
76
|
+
return this
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {FieldDeclaration} from '../types/fields'
|
|
2
|
+
|
|
3
|
+
const generateValidationRules = (
|
|
4
|
+
rules: string | string[]
|
|
5
|
+
): string[] => {
|
|
6
|
+
return typeof rules === 'string' ?
|
|
7
|
+
rules.split('|') :
|
|
8
|
+
rules
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const generateFieldDeclaration = (
|
|
12
|
+
value: any
|
|
13
|
+
): FieldDeclaration => {
|
|
14
|
+
return {
|
|
15
|
+
validation: {
|
|
16
|
+
rules: value.validation?.rules ?
|
|
17
|
+
generateValidationRules(value.validation.rules) :
|
|
18
|
+
[],
|
|
19
|
+
messages: value.validation?.messages || {}
|
|
20
|
+
},
|
|
21
|
+
value: value.value || null
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const isObject = (value: any): boolean => {
|
|
2
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export const objectToFormData = (
|
|
6
|
+
values: Record<string, any>,
|
|
7
|
+
context?: FormData,
|
|
8
|
+
namespace: string | null = null
|
|
9
|
+
): FormData => {
|
|
10
|
+
const formData: FormData = context || new FormData()
|
|
11
|
+
|
|
12
|
+
Object.keys(values).forEach((key: string): void => {
|
|
13
|
+
const value = values[key]
|
|
14
|
+
|
|
15
|
+
key = namespace ? `${namespace}[${key}]` : key
|
|
16
|
+
|
|
17
|
+
if ([undefined, null].indexOf(value) > -1) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if ((isObject(value) && !(value instanceof File)) || Array.isArray(value)) {
|
|
22
|
+
objectToFormData(value, formData, key)
|
|
23
|
+
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
formData.append(key, value)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
return formData
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {Rules} from '../types/rules.ts'
|
|
2
|
+
|
|
3
|
+
export const required = (
|
|
4
|
+
value: string
|
|
5
|
+
): Promise<any> => new Promise((resolve, reject) => {
|
|
6
|
+
if (value === undefined || value === null) {
|
|
7
|
+
reject()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let str = String(value).replace(/\s/g, "");
|
|
11
|
+
|
|
12
|
+
str.length > 0 ? resolve(value) : reject()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const Rule: Rules = {
|
|
16
|
+
required
|
|
17
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
|
|
16
|
+
/* Linting */
|
|
17
|
+
"strict": true,
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true
|
|
21
|
+
},
|
|
22
|
+
"include": ["src"]
|
|
23
|
+
}
|
package/vite.config.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {resolve} from 'path'
|
|
2
|
+
import {defineConfig} from 'vite'
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
build: {
|
|
6
|
+
lib: {
|
|
7
|
+
entry: resolve(__dirname, 'src/main.ts'),
|
|
8
|
+
name: 'FormWrapper',
|
|
9
|
+
fileName: (format) => `form-wrapper.${format}.js`
|
|
10
|
+
},
|
|
11
|
+
rollupOptions: {
|
|
12
|
+
output: {
|
|
13
|
+
exports: 'named'
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
})
|