@steveesamson/microform 0.0.11 → 1.0.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 +307 -140
- package/dist/form-action.d.ts +3 -2
- package/dist/form-action.js +41 -8
- package/dist/form-validators.d.ts +23 -2
- package/dist/form-validators.js +135 -162
- package/dist/index.d.ts +2 -3
- package/dist/index.js +2 -64
- package/dist/index.svelte.js +79 -0
- package/dist/internal.d.ts +3 -2
- package/dist/types.d.ts +25 -10
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +21 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -25,18 +25,23 @@ Once you've added `microform` to your project, use it as shown below, in your vi
|
|
|
25
25
|
### In the view Script
|
|
26
26
|
|
|
27
27
|
```ts
|
|
28
|
-
<script
|
|
28
|
+
<script>
|
|
29
29
|
import uForm from "@steveesamson/microform";
|
|
30
30
|
// default form data, probably passed as props
|
|
31
|
-
|
|
31
|
+
let defaultData:any = $props();
|
|
32
32
|
|
|
33
33
|
// Instatiate microform
|
|
34
|
-
const { form, values, errors, submit,
|
|
34
|
+
const { form, values, errors, submit, sanity } = uForm({
|
|
35
35
|
// Set default form data
|
|
36
36
|
data:{...defaultData},
|
|
37
37
|
// Set a global event for validation, can be overriden on a each field.
|
|
38
38
|
// Possible values: blur, change, input, keyup
|
|
39
|
-
|
|
39
|
+
options:{
|
|
40
|
+
// Default event that triggers validation
|
|
41
|
+
validateEvent:'blur',
|
|
42
|
+
// Configure validators here
|
|
43
|
+
validators:{}
|
|
44
|
+
}
|
|
40
45
|
});
|
|
41
46
|
|
|
42
47
|
// Submit handler
|
|
@@ -51,11 +56,11 @@ const onSubmit = (data:unknown) => {
|
|
|
51
56
|
|
|
52
57
|
On the instantiation of `microform`, we have access to:
|
|
53
58
|
|
|
54
|
-
- `values`, a `FormValues`, which
|
|
55
|
-
- `errors`, a `FormErrors`, which
|
|
59
|
+
- `values`, a `FormValues`, which represents form data.
|
|
60
|
+
- `errors`, a `FormErrors`, which represents form errors.
|
|
56
61
|
- `form`, which is a `svelte action` that actually does the `microform` magic.
|
|
57
62
|
- `submit`, which is another `svelte action` to handle form submission.
|
|
58
|
-
- `
|
|
63
|
+
- `sanity`, a `FormSanity`, which tells us if a form is clean/without errors by it's `ok` property.
|
|
59
64
|
- `reset`, a function to reset form
|
|
60
65
|
- `onsubmit`, a function to handle form submission.
|
|
61
66
|
|
|
@@ -63,91 +68,162 @@ On the instantiation of `microform`, we have access to:
|
|
|
63
68
|
|
|
64
69
|
```html
|
|
65
70
|
<form use:submit={onSubmit}>
|
|
66
|
-
<label for=
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
71
|
+
<label for="fullname">
|
|
72
|
+
Name:
|
|
73
|
+
<input
|
|
74
|
+
type="text"
|
|
75
|
+
name="fullname"
|
|
76
|
+
id="fullname"
|
|
77
|
+
use:form={{ validations: ['required'] }}
|
|
78
|
+
/>
|
|
79
|
+
{#if errors.fullname}
|
|
80
|
+
<small>{errors.fullname}</small>
|
|
81
|
+
{/if}
|
|
82
|
+
</label>
|
|
83
|
+
<label for="dob">
|
|
84
|
+
DOB:
|
|
85
|
+
<input
|
|
86
|
+
type="date"
|
|
87
|
+
name="dob"
|
|
88
|
+
id="dob"
|
|
89
|
+
use:form={{ validations: ['required'] }}
|
|
90
|
+
/>
|
|
91
|
+
{#if errors.dob}
|
|
92
|
+
<small>{errors.dob}</small>
|
|
93
|
+
{/if}
|
|
94
|
+
</label>
|
|
95
|
+
<label for="supper_time">
|
|
96
|
+
Supper time:
|
|
97
|
+
<input
|
|
98
|
+
type="time"
|
|
99
|
+
name="supper_time"
|
|
100
|
+
id="supper_time"
|
|
101
|
+
use:form={{ validations: ['required'] }}
|
|
102
|
+
/>
|
|
103
|
+
{#if errors.supper_time}
|
|
104
|
+
<small>{errors.supper_time}</small>
|
|
105
|
+
{/if}
|
|
106
|
+
</label>
|
|
107
|
+
<label for="password">
|
|
108
|
+
Password:
|
|
109
|
+
<input
|
|
110
|
+
type="password"
|
|
111
|
+
name="password"
|
|
112
|
+
id="password"
|
|
113
|
+
use:form={{ validations: ['required'] }}
|
|
114
|
+
/>
|
|
115
|
+
{#if errors.password}
|
|
116
|
+
<small>{errors.password}</small>
|
|
117
|
+
{/if}
|
|
118
|
+
</label>
|
|
119
|
+
<label for="gender">
|
|
120
|
+
Gender:
|
|
121
|
+
<select
|
|
122
|
+
name="gender"
|
|
123
|
+
id="gender"
|
|
124
|
+
use:form={{ validateEvent: 'change', validations: ['required'] }}
|
|
125
|
+
>
|
|
126
|
+
<option value="">Select gender</option>
|
|
127
|
+
<option value="M">Male</option>
|
|
128
|
+
<option value="F">Female</option>
|
|
129
|
+
</select>
|
|
130
|
+
{#if errors.gender}
|
|
131
|
+
<small>{errors.gender}</small>
|
|
132
|
+
{/if}
|
|
133
|
+
</label>
|
|
134
|
+
<label for="email">
|
|
135
|
+
Email:
|
|
136
|
+
<input
|
|
137
|
+
type="text"
|
|
138
|
+
name="email"
|
|
139
|
+
id="email"
|
|
140
|
+
use:form={{ validations: ['required', 'email'] }}
|
|
141
|
+
/>
|
|
142
|
+
{#if errors.email}
|
|
143
|
+
<small>{errors.email}</small>
|
|
144
|
+
{/if}
|
|
145
|
+
</label>
|
|
146
|
+
<label for="resume">
|
|
147
|
+
Resume:
|
|
148
|
+
<input
|
|
149
|
+
type="file"
|
|
150
|
+
name="resume"
|
|
151
|
+
id="resume"
|
|
152
|
+
use:form={{
|
|
153
|
+
validateEvent:'change',
|
|
154
|
+
validations: ['required', 'file-size-mb:3']
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
{#if errors.resume}
|
|
158
|
+
<small>{errors.resume}</small>
|
|
159
|
+
{/if}
|
|
160
|
+
</label>
|
|
161
|
+
<label for="comment">
|
|
162
|
+
Comment:
|
|
163
|
+
<textarea
|
|
164
|
+
name="comment"
|
|
165
|
+
id="comment"
|
|
166
|
+
use:form={{ validations: ['required'] }}
|
|
167
|
+
></textarea>
|
|
168
|
+
{#if errors.comment}
|
|
169
|
+
<small>{errors.comment}</small>
|
|
170
|
+
{/if}
|
|
171
|
+
</label>
|
|
172
|
+
<label for="beverage">
|
|
173
|
+
Beverage:
|
|
174
|
+
{#each items as item (item.value)}
|
|
175
|
+
<span>
|
|
176
|
+
<input
|
|
177
|
+
type="radio"
|
|
178
|
+
name="beverage"
|
|
179
|
+
value={item.value}
|
|
180
|
+
use:form={{ validateEvent: 'input', validations: ['required'] }}
|
|
181
|
+
bind:group={values.beverage}
|
|
182
|
+
/>
|
|
183
|
+
{item.label}
|
|
184
|
+
</span>
|
|
185
|
+
{/each}
|
|
186
|
+
{#if errors.beverage}
|
|
187
|
+
<small>{errors.beverage}</small>
|
|
188
|
+
{/if}
|
|
189
|
+
</label>
|
|
190
|
+
<label for="favfoods">
|
|
191
|
+
Fav Foods:
|
|
192
|
+
{#each foods as item (item.value)}
|
|
193
|
+
<span>
|
|
194
|
+
<input
|
|
195
|
+
type="checkbox"
|
|
196
|
+
name="favfoods"
|
|
197
|
+
value={item.value}
|
|
198
|
+
use:form={{ validateEvent: 'change', validations: ['required'] }}
|
|
199
|
+
bind:group={values['favfoods']}
|
|
200
|
+
/>
|
|
201
|
+
{item.label}
|
|
202
|
+
</span>
|
|
203
|
+
{/each}
|
|
204
|
+
{#if errors.favfoods}
|
|
205
|
+
<small>{errors.favfoods}</small>
|
|
206
|
+
{/if}
|
|
207
|
+
</label>
|
|
208
|
+
<label for="story">
|
|
209
|
+
Story:
|
|
210
|
+
<div
|
|
211
|
+
contenteditable="true"
|
|
212
|
+
use:form={{
|
|
213
|
+
validateEvent: 'input',
|
|
214
|
+
validations: ['required'],
|
|
215
|
+
name: 'story',
|
|
216
|
+
html: true
|
|
217
|
+
}}
|
|
218
|
+
></div>
|
|
219
|
+
{#if errors.story}
|
|
220
|
+
<small>{errors.story}</small>
|
|
221
|
+
{/if}
|
|
222
|
+
</label>
|
|
147
223
|
|
|
148
224
|
<button
|
|
149
225
|
type='submit'
|
|
150
|
-
disabled={
|
|
226
|
+
disabled={!sanity.ok}>
|
|
151
227
|
Submit form
|
|
152
228
|
</button>
|
|
153
229
|
</form>
|
|
@@ -159,9 +235,16 @@ While the above example uses the `submit` action of `microform`, form could also
|
|
|
159
235
|
<form>
|
|
160
236
|
<label for="password">
|
|
161
237
|
Password:
|
|
162
|
-
<input
|
|
163
|
-
|
|
164
|
-
|
|
238
|
+
<input
|
|
239
|
+
type="text"
|
|
240
|
+
name="password"
|
|
241
|
+
id="password"
|
|
242
|
+
use:form={{
|
|
243
|
+
validations: ['required']
|
|
244
|
+
}}
|
|
245
|
+
/>
|
|
246
|
+
{#if errors.password}
|
|
247
|
+
<small>{errors.password}</small>
|
|
165
248
|
{/if}
|
|
166
249
|
</label>
|
|
167
250
|
<label for="confirm_password">
|
|
@@ -170,15 +253,16 @@ While the above example uses the `submit` action of `microform`, form could also
|
|
|
170
253
|
type="text"
|
|
171
254
|
name="confirm_password"
|
|
172
255
|
id="confirm_password"
|
|
173
|
-
use:form
|
|
174
|
-
|
|
256
|
+
use:form={{
|
|
257
|
+
validations: ['required','match:password'],
|
|
258
|
+
}}
|
|
175
259
|
/>
|
|
176
|
-
{#if
|
|
177
|
-
<small>{
|
|
260
|
+
{#if errors.confirm_password}
|
|
261
|
+
<small>{errors.confirm_password}</small>
|
|
178
262
|
{/if}
|
|
179
263
|
</label>
|
|
180
264
|
|
|
181
|
-
<button type="button" disabled="{
|
|
265
|
+
<button type="button" disabled="{!sanity.ok}" onclick="{onsubmit(onSubmit)}">Submit form</button>
|
|
182
266
|
</form>
|
|
183
267
|
```
|
|
184
268
|
|
|
@@ -186,12 +270,12 @@ While the above example uses the `submit` action of `microform`, form could also
|
|
|
186
270
|
|
|
187
271
|
`microform` performs its magic by relying the `form` action. The `form` action can optionally accept the following:
|
|
188
272
|
|
|
189
|
-
```
|
|
273
|
+
```ts
|
|
190
274
|
<input
|
|
191
275
|
use:form={{
|
|
192
276
|
// an optional list of validations
|
|
193
277
|
// default is '' - no validations
|
|
194
|
-
validations: 'email
|
|
278
|
+
validations: ['email', 'len:20'],
|
|
195
279
|
// an optional string of
|
|
196
280
|
// any of blur, change, input, keyup.
|
|
197
281
|
// default is blur
|
|
@@ -214,46 +298,36 @@ You need not bind the `values` to your fields except when there is a definite ne
|
|
|
214
298
|
|
|
215
299
|
```html
|
|
216
300
|
<input
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
id="email_account"
|
|
220
|
-
value="{$values.email_account}"
|
|
221
|
-
data-validations="required|email"
|
|
222
|
-
use:form
|
|
301
|
+
...
|
|
302
|
+
value="{values.email_account}"
|
|
223
303
|
/>
|
|
224
304
|
```
|
|
225
305
|
|
|
226
306
|
### 1. Validations
|
|
227
307
|
|
|
228
|
-
|
|
308
|
+
`validations` is an array of validations to check field values against for correctnes. Uses `validations` props on `form` action. For instance, the following:
|
|
229
309
|
|
|
230
310
|
```html
|
|
231
|
-
<input
|
|
232
|
-
type='text'
|
|
233
|
-
name='email_account'
|
|
234
|
-
id='email_account'
|
|
235
|
-
data-validations='required|email'
|
|
236
|
-
use:form/>
|
|
237
|
-
|
|
238
311
|
<input
|
|
239
312
|
type='text'
|
|
240
313
|
name='email_account'
|
|
241
314
|
id='email_account'
|
|
242
315
|
use:form={{
|
|
243
|
-
validations:'required
|
|
244
|
-
}}
|
|
316
|
+
validations:[ 'required', 'email' ]
|
|
317
|
+
}}
|
|
318
|
+
/>
|
|
245
319
|
```
|
|
246
320
|
|
|
247
321
|
### 2. Validation Event
|
|
248
322
|
|
|
249
|
-
Validation event can be changed/specified on a per-field basis. For instance, in our example, the global `validateEvent` was set to `blur` but we changed it on the select field to `change` like so:
|
|
323
|
+
Validation event is to configure the event that should trigger validations. It can be changed/specified on a per-field basis. For instance, in our example, the global `validateEvent` was set to `blur` but we changed it on the select field to `change` like so:
|
|
250
324
|
|
|
251
325
|
```html
|
|
252
326
|
<select
|
|
253
327
|
name='gender'
|
|
254
328
|
id='gender'
|
|
255
329
|
use:form={{
|
|
256
|
-
validations:'required',
|
|
330
|
+
validations:['required'],
|
|
257
331
|
validateEvent:'change'
|
|
258
332
|
}}>
|
|
259
333
|
<options value=''>Gender please</option>
|
|
@@ -274,13 +348,13 @@ Validation event can be changed/specified on a per-field basis. For instance, in
|
|
|
274
348
|
contenteditable="true"
|
|
275
349
|
use:form={{
|
|
276
350
|
validateEvent: 'input',
|
|
277
|
-
validations: 'required',
|
|
351
|
+
validations: ['required'],
|
|
278
352
|
name: 'story',
|
|
279
353
|
html: true
|
|
280
354
|
}}
|
|
281
|
-
|
|
282
|
-
{#if
|
|
283
|
-
<small>{
|
|
355
|
+
></div>
|
|
356
|
+
{#if errors.story}
|
|
357
|
+
<small>{errors.story}</small>
|
|
284
358
|
{/if}
|
|
285
359
|
</label>
|
|
286
360
|
</form>
|
|
@@ -290,35 +364,128 @@ Validation event can be changed/specified on a per-field basis. For instance, in
|
|
|
290
364
|
|
|
291
365
|
`microform` provides a set of usable validations out-of-box. The following is a list of provided validations:
|
|
292
366
|
|
|
293
|
-
- `required`: Usage, `validations
|
|
294
|
-
- `email`: Usage, `validations
|
|
295
|
-
- `url`: Usage, `validations
|
|
296
|
-
- `ip`: Usage, `validations
|
|
297
|
-
- `
|
|
298
|
-
- `number`: Usage, `validations
|
|
299
|
-
- `integer`: Usage, `validations
|
|
300
|
-
- `alpha`: Usage, `validations
|
|
301
|
-
- `alphanum`: Usage, `validations
|
|
302
|
-
- `match
|
|
303
|
-
- `
|
|
304
|
-
- `
|
|
305
|
-
- `max
|
|
306
|
-
- `
|
|
367
|
+
- `required`: Usage, `validations:['required']`
|
|
368
|
+
- `email`: Usage, `validations:['email']`
|
|
369
|
+
- `url`: Usage, `validations:['url']`
|
|
370
|
+
- `ip`: Usage, `validations:['ip']`
|
|
371
|
+
- `len:<number>`: Usage, `validations:['len:40']`
|
|
372
|
+
- `number`: Usage, `validations:['number']`
|
|
373
|
+
- `integer`: Usage, `validations:['integer']`
|
|
374
|
+
- `alpha`: Usage, `validations:['alpha']` - only alphabets
|
|
375
|
+
- `alphanum`: Usage, `validations:['alphanum']` - alphanumeric
|
|
376
|
+
- `match:<input-id>`: Usage, `validations:['match:<id-of-field-to-match>']`. For instance, this is examplified in our example with `password` and `confirm_password` fields
|
|
377
|
+
- `minlen:<number>`: Usage, `validations:['minlen:6']`
|
|
378
|
+
- `maxlen:<number>`: Usage, `validations:['maxlen:15']`
|
|
379
|
+
- `max:<number>`: Usage, `validations:['max:25']`
|
|
380
|
+
- `file-size-mb:<number>`: Usage, `validations:['file-size-mb:30']` - for file upload
|
|
307
381
|
|
|
308
382
|
Every validation listed above also comes with a very good default error message.
|
|
309
383
|
|
|
310
|
-
Finally, the validations can be combined to form a complex graph of validations based on use cases by
|
|
384
|
+
Finally, the validations can be combined to form a complex graph of validations based on use cases by combining them in a single array of validations. For instance, a required field that also should be an email field could be configured thus:
|
|
311
385
|
|
|
312
386
|
```html
|
|
313
387
|
<input
|
|
314
388
|
type="text"
|
|
315
389
|
name="email_account"
|
|
316
390
|
id="email_account"
|
|
317
|
-
|
|
318
|
-
|
|
391
|
+
use:form={{
|
|
392
|
+
validations:['required','email']
|
|
393
|
+
}}
|
|
319
394
|
/>
|
|
320
395
|
```
|
|
321
396
|
|
|
322
|
-
#
|
|
397
|
+
# Overriding validators
|
|
398
|
+
Validators could be overriden to provide custom validation and/or messages besides the default ones. For instance, let us overide the `len` validation rule. Every non-empty string/message returned from a validator's call becomes the error to be displayed to the user; and it shows up in the `errors` keyed by the field name.
|
|
399
|
+
|
|
400
|
+
### Approach 1
|
|
401
|
+
```ts
|
|
402
|
+
<script>
|
|
403
|
+
import uForm, { type FieldProps } from "@steveesamson/microform";
|
|
404
|
+
// default form data, probably passed as props
|
|
405
|
+
export let defaultData:any = {};
|
|
406
|
+
|
|
407
|
+
// Instatiate microform
|
|
408
|
+
const { form, values, errors, submit, sanity } = uForm({
|
|
409
|
+
// Set default form data
|
|
410
|
+
data:{...defaultData},
|
|
411
|
+
// Set a global event for validation, can be overriden on a each field.
|
|
412
|
+
// Possible values: blur, change, input, keyup
|
|
413
|
+
options:{
|
|
414
|
+
// Default event that triggers validation
|
|
415
|
+
validateEvent:'blur',
|
|
416
|
+
// Configure validators here
|
|
417
|
+
validators:{
|
|
418
|
+
len:({name, label, parts}:FieldProps) =>{
|
|
419
|
+
if (!parts || parts.length < 2) {
|
|
420
|
+
return `${label}: length validation requires length.`;
|
|
421
|
+
}
|
|
422
|
+
const extra = parts[1].trim();
|
|
423
|
+
return !!value && value.length !== parseInt(parts[1], 10) ? `${label} must exactly be ${extra} characters long.` : "";
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
</script>
|
|
429
|
+
```
|
|
430
|
+
Instead of using the literal validation keys like `len`, `required` etc., while overriding validators, the exported key namee could be used. The key names are:
|
|
431
|
+
- IS_REQUIRED = `required`
|
|
432
|
+
- IS_EMAIL = `email`
|
|
433
|
+
- IS_URL = `url`
|
|
434
|
+
- IS_IP = `ip`
|
|
435
|
+
- IS_INTEGER = `integer`
|
|
436
|
+
- IS_NUMBER = `number`
|
|
437
|
+
- IS_ALPHA = `alpha`
|
|
438
|
+
- IS_ALPHANUM = `alphanum`
|
|
439
|
+
- IS_MIN_LEN = `minlen`
|
|
440
|
+
- IS_MAX_LEN = `maxlen`
|
|
441
|
+
- IS_LEN = `len`
|
|
442
|
+
- IS_MIN = `min`
|
|
443
|
+
- IS_MAX = `max`
|
|
444
|
+
- IT_MATCHES = `match`
|
|
445
|
+
- IS_FILE_SIZE_MB = `file-size-mb`
|
|
446
|
+
|
|
447
|
+
Therefore, the following is equivalent to the configuration in `Approach 1`:
|
|
448
|
+
|
|
449
|
+
### Approach 2
|
|
450
|
+
```ts
|
|
451
|
+
<script>
|
|
452
|
+
import uForm, { type FieldProps, IS_LEN } from "@steveesamson/microform";
|
|
453
|
+
// default form data, probably passed as props
|
|
454
|
+
export let defaultData:any = {};
|
|
455
|
+
|
|
456
|
+
// Instatiate microform
|
|
457
|
+
const { form, values, errors, submit, sanity } = uForm({
|
|
458
|
+
// Set default form data
|
|
459
|
+
data:{...defaultData},
|
|
460
|
+
// Set a global event for validation, can be overriden on a each field.
|
|
461
|
+
// Possible values: blur, change, input, keyup
|
|
462
|
+
options:{
|
|
463
|
+
// Default event that triggers validation
|
|
464
|
+
validateEvent:'blur',
|
|
465
|
+
// Configure validators here
|
|
466
|
+
validators:{
|
|
467
|
+
[IS_LEN]:({name, label, parts}:FieldProps) =>{
|
|
468
|
+
if (!parts || parts.length < 2) {
|
|
469
|
+
return `${label}: length validation requires length.`;
|
|
470
|
+
}
|
|
471
|
+
const extra = parts[1].trim();
|
|
472
|
+
return !!value && value.length !== parseInt(parts[1], 10) ? `${label} must exactly be ${extra} characters long.` : "";
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
</script>
|
|
478
|
+
```
|
|
323
479
|
|
|
324
|
-
|
|
480
|
+
The validation will be used on the view as below:
|
|
481
|
+
|
|
482
|
+
```html
|
|
483
|
+
<input
|
|
484
|
+
type="text"
|
|
485
|
+
name="comment"
|
|
486
|
+
id="comment"
|
|
487
|
+
use:form={{
|
|
488
|
+
validations:['required','len:30']
|
|
489
|
+
}}
|
|
490
|
+
/>
|
|
491
|
+
```
|
package/dist/form-action.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// <reference types="svelte" />
|
|
2
2
|
import { type Writable } from "svelte/store";
|
|
3
|
-
import type {
|
|
4
|
-
|
|
3
|
+
import type { FormOptions, FormAction } from "./types.js";
|
|
4
|
+
import type { Params } from "./internal.js";
|
|
5
|
+
export declare const formAction: (values: Writable<Params>, errors: Writable<Params>, unfits: Writable<Params>, isdirty: Writable<boolean>, options: FormOptions, validationMap: Params) => FormAction;
|
package/dist/form-action.js
CHANGED
|
@@ -7,22 +7,34 @@ const isField = (node) => {
|
|
|
7
7
|
node instanceof HTMLTextAreaElement;
|
|
8
8
|
};
|
|
9
9
|
const isExcluded = (node) => {
|
|
10
|
-
return node instanceof HTMLInputElement && ['radio', 'checkbox'
|
|
10
|
+
return node instanceof HTMLInputElement && ['radio', 'checkbox'].includes(node.type.toLowerCase());
|
|
11
11
|
};
|
|
12
|
-
const
|
|
13
|
-
|
|
12
|
+
const isCheckbox = (node) => {
|
|
13
|
+
return node instanceof HTMLInputElement && ['checkbox'].includes(node.type.toLowerCase());
|
|
14
|
+
};
|
|
15
|
+
const isRadio = (node) => {
|
|
16
|
+
return node instanceof HTMLInputElement && ['radio'].includes(node.type.toLowerCase());
|
|
17
|
+
};
|
|
18
|
+
const checkFormFitness = (values, validationMap, validate) => {
|
|
14
19
|
const _values = get(values);
|
|
15
20
|
for (const [name, { validations }] of Object.entries(validationMap)) {
|
|
16
21
|
validate({ name, value: _values[name], validations, });
|
|
17
22
|
}
|
|
18
23
|
};
|
|
19
24
|
export const formAction = (values, errors, unfits, isdirty, options, validationMap) => {
|
|
20
|
-
const
|
|
25
|
+
const { validators: customValidators } = options;
|
|
26
|
+
const { validate, validators } = useValidator(errors, values);
|
|
27
|
+
// override
|
|
28
|
+
if (customValidators) {
|
|
29
|
+
for (const [key, val] of Object.entries(customValidators)) {
|
|
30
|
+
validators[key] = val;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
21
33
|
return (node, eventProps) => {
|
|
22
34
|
const nodeName = isField(node) ? node.name : '';
|
|
23
|
-
const { name: dsname = nodeName
|
|
24
|
-
const { name = dsname, validations =
|
|
25
|
-
validationMap[name] = { validations, html, nodeRef:
|
|
35
|
+
const { name: dsname = nodeName } = node.dataset || {};
|
|
36
|
+
const { name = dsname, validations = [], validateEvent = options.validateEvent = 'blur', html = false } = eventProps || {};
|
|
37
|
+
validationMap[name] = { validations, html, nodeRef: node };
|
|
26
38
|
const storedValue = get(values)[name] || '';
|
|
27
39
|
let defValue = storedValue;
|
|
28
40
|
if (isField(node) && !isExcluded(node)) {
|
|
@@ -55,7 +67,28 @@ export const formAction = (values, errors, unfits, isdirty, options, validationM
|
|
|
55
67
|
return { ...data, [name]: htm, [`${name}-text`]: text };
|
|
56
68
|
});
|
|
57
69
|
}
|
|
58
|
-
|
|
70
|
+
else if (isCheckbox(node)) {
|
|
71
|
+
const { checked, value: val } = node;
|
|
72
|
+
const { [name]: fieldValue } = get(values);
|
|
73
|
+
let current = fieldValue;
|
|
74
|
+
if (checked) {
|
|
75
|
+
current.push(val);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
current = current.filter((next) => next !== val);
|
|
79
|
+
}
|
|
80
|
+
values.update((data) => {
|
|
81
|
+
return { ...data, [name]: [...new Set(current)] };
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
else if (isRadio(node)) {
|
|
85
|
+
const { value: fvalue } = node;
|
|
86
|
+
values.update((data) => {
|
|
87
|
+
return { ...data, [name]: fvalue };
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const { validate: validateUnfit } = useValidator(unfits, values, validators);
|
|
91
|
+
checkFormFitness(values, validationMap, validateUnfit);
|
|
59
92
|
isdirty.set(true);
|
|
60
93
|
};
|
|
61
94
|
node.addEventListener(validateEvent, updateNode);
|
|
@@ -1,2 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/// <reference types="svelte" />
|
|
2
|
+
import { type Writable } from "svelte/store";
|
|
3
|
+
import type { ValidateArgs, ValidatorType, ValidatorMap } from "./types.js";
|
|
4
|
+
import type { Params } from "./internal.js";
|
|
5
|
+
export declare const IS_REQUIRED = "required";
|
|
6
|
+
export declare const IS_EMAIL = "email";
|
|
7
|
+
export declare const IS_URL = "url";
|
|
8
|
+
export declare const IS_IP = "ip";
|
|
9
|
+
export declare const IS_INTEGER = "integer";
|
|
10
|
+
export declare const IS_NUMBER = "number";
|
|
11
|
+
export declare const IS_ALPHA = "alpha";
|
|
12
|
+
export declare const IS_ALPHANUM = "alphanum";
|
|
13
|
+
export declare const IS_MIN_LEN = "minlen";
|
|
14
|
+
export declare const IS_MAX_LEN = "maxlen";
|
|
15
|
+
export declare const IS_LEN = "len";
|
|
16
|
+
export declare const IS_MIN = "min";
|
|
17
|
+
export declare const IS_MAX = "max";
|
|
18
|
+
export declare const IT_MATCHES = "match";
|
|
19
|
+
export declare const IS_FILE_SIZE_MB = "file-size-mb";
|
|
20
|
+
export declare const useValidator: (errors: Writable<Params>, values: Writable<Params>, validators?: ValidatorMap<ValidatorType>) => {
|
|
21
|
+
validate: ({ name, value, validations, node }: ValidateArgs) => Promise<void>;
|
|
22
|
+
validators: ValidatorMap<ValidatorType>;
|
|
23
|
+
};
|
package/dist/form-validators.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { get } from "svelte/store";
|
|
2
|
-
import { makeName } from "./utils.js";
|
|
2
|
+
import { makeName, isValidFileSize } from "./utils.js";
|
|
3
3
|
const regexes = {
|
|
4
4
|
number: /^[-+]?[0-9]+(\.[0-9]+)?$/g,
|
|
5
5
|
alpha: /^[A-Z\s]+$/gi,
|
|
@@ -9,175 +9,148 @@ const regexes = {
|
|
|
9
9
|
url: /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i,
|
|
10
10
|
ip: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/g,
|
|
11
11
|
};
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
12
|
+
export const IS_REQUIRED = "required";
|
|
13
|
+
export const IS_EMAIL = "email";
|
|
14
|
+
export const IS_URL = "url";
|
|
15
|
+
export const IS_IP = "ip";
|
|
16
|
+
export const IS_INTEGER = "integer";
|
|
17
|
+
export const IS_NUMBER = "number";
|
|
18
|
+
export const IS_ALPHA = "alpha";
|
|
19
|
+
export const IS_ALPHANUM = "alphanum";
|
|
20
|
+
export const IS_MIN_LEN = "minlen";
|
|
21
|
+
export const IS_MAX_LEN = "maxlen";
|
|
22
|
+
export const IS_LEN = "len";
|
|
23
|
+
export const IS_MIN = "min";
|
|
24
|
+
export const IS_MAX = "max";
|
|
25
|
+
export const IT_MATCHES = "match";
|
|
26
|
+
export const IS_FILE_SIZE_MB = 'file-size-mb';
|
|
27
|
+
const getDefaultValidators = () => {
|
|
28
|
+
const validators = {
|
|
29
|
+
[IS_REQUIRED]: ({ label, value }) => {
|
|
30
|
+
if (!value || (Array.isArray(value) && !value.length)) {
|
|
31
|
+
return `${label} is mandatory.`;
|
|
32
|
+
}
|
|
33
|
+
return "";
|
|
34
|
+
},
|
|
35
|
+
[IS_EMAIL]: ({ value, label }) => {
|
|
36
|
+
return (!!value && !value.match(regexes['email'])) ? `${label} should be a valid email.` : '';
|
|
37
|
+
},
|
|
38
|
+
[IS_URL]: ({ value, label }) => {
|
|
39
|
+
return !!value && !value.match(regexes['url']) ? `${label} should be a valid URL.` : "";
|
|
40
|
+
},
|
|
41
|
+
[IS_IP]: ({ value, label }) => {
|
|
42
|
+
return !!value && !value.match(regexes['ip']) ? `${label} should be a valid IP.` : "";
|
|
43
|
+
},
|
|
44
|
+
[IS_INTEGER]: ({ value, label }) => {
|
|
45
|
+
return !!value && !value.match(regexes['integer']) ? `${label} must be an integer number.` : "";
|
|
46
|
+
},
|
|
47
|
+
[IS_NUMBER]: ({ value, label }) => {
|
|
48
|
+
return !!value && !value.match(regexes['number']) ? `${label} must be a number.` : "";
|
|
49
|
+
},
|
|
50
|
+
[IS_ALPHA]: ({ value, label }) => {
|
|
51
|
+
return !!value && !value.match(regexes['alpha']) ? `${label} should be a string of alphabets.` : "";
|
|
52
|
+
},
|
|
53
|
+
[IS_ALPHANUM]: ({ value, label }) => {
|
|
54
|
+
return !!value && !value.match(regexes['alphanum']) ? `${label} should be a string of alphanumerics starting with alphabets.` : "";
|
|
55
|
+
},
|
|
56
|
+
[IS_MIN_LEN]: ({ value, label, parts }) => {
|
|
57
|
+
if (!!value) {
|
|
58
|
+
if (!parts || parts.length < 2) {
|
|
59
|
+
return `${label}: min-length validation requires minimum length.`;
|
|
60
|
+
}
|
|
61
|
+
const extra = parts[1].trim();
|
|
62
|
+
return value.length >= parseInt(parts[1], 10) ? "" : `${label} must be at least ${extra} characters long.`;
|
|
63
|
+
}
|
|
64
|
+
return "";
|
|
65
|
+
},
|
|
66
|
+
[IS_MAX_LEN]: ({ value, label, parts }) => {
|
|
67
|
+
if (!!value) {
|
|
68
|
+
if (!parts || parts.length < 2) {
|
|
69
|
+
return `${label}: max-length validation requires maximum length.`;
|
|
70
|
+
}
|
|
71
|
+
const extra = parts[1].trim();
|
|
72
|
+
return value.length <= parseInt(parts[1], 10) ? "" : `${label} must be at most ${extra} characters long.`;
|
|
73
|
+
}
|
|
74
|
+
return "";
|
|
75
|
+
},
|
|
76
|
+
[IS_LEN]: ({ value, label, parts }) => {
|
|
77
|
+
if (!!value) {
|
|
78
|
+
if (!parts || parts.length < 2) {
|
|
79
|
+
return `${label}: length validation requires length.`;
|
|
80
|
+
}
|
|
81
|
+
const extra = parts[1].trim();
|
|
82
|
+
return value.length === parseInt(parts[1], 10) ? "" : `${label} must exactly be ${extra} characters long.`;
|
|
83
|
+
}
|
|
84
|
+
return "";
|
|
85
|
+
},
|
|
86
|
+
[IS_MAX]: ({ value, label, parts }) => {
|
|
87
|
+
if (!!value) {
|
|
88
|
+
if (!parts || parts.length < 2) {
|
|
89
|
+
return `${label}: max validation requires the maximum value.`;
|
|
90
|
+
}
|
|
91
|
+
const extra = parts[1].trim();
|
|
92
|
+
return parseInt(value, 10) <= parseInt(parts[1], 10) ? "" : `${label} must not be greater than ${extra}.`;
|
|
93
|
+
}
|
|
94
|
+
return "";
|
|
95
|
+
},
|
|
96
|
+
[IS_MIN]: ({ value, label, parts }) => {
|
|
97
|
+
if (!!value) {
|
|
98
|
+
if (!parts || parts.length < 2) {
|
|
99
|
+
return `${label}: min validation requires the minimum value.`;
|
|
100
|
+
}
|
|
101
|
+
const extra = parts[1].trim();
|
|
102
|
+
return parseInt(value, 10) >= parseInt(parts[1], 10) ? "" : `${label} must not be less than ${extra}.`;
|
|
103
|
+
}
|
|
104
|
+
return "";
|
|
105
|
+
},
|
|
106
|
+
[IT_MATCHES]: ({ values, value, label, parts }) => {
|
|
107
|
+
if (value) {
|
|
108
|
+
if (!parts || parts.length < 2) {
|
|
109
|
+
return `${label}: match validation requires id of field to match`;
|
|
110
|
+
}
|
|
111
|
+
const partyName = parts[1].trim();
|
|
112
|
+
const partyValue = values[partyName];
|
|
113
|
+
return value === partyValue ? "" : `${label} does not match ${makeName(partyName)}.`;
|
|
114
|
+
}
|
|
115
|
+
return "";
|
|
116
|
+
},
|
|
117
|
+
[IS_FILE_SIZE_MB]: ({ value, node, label, parts }) => {
|
|
118
|
+
if (!!value) {
|
|
119
|
+
if (!parts || parts.length < 2) {
|
|
120
|
+
return `${label}: max file size in MB validation requires maximum file size of mb value.`;
|
|
121
|
+
;
|
|
122
|
+
}
|
|
123
|
+
const extra = parts[1].trim();
|
|
124
|
+
return isValidFileSize(node, parseInt(extra, 10));
|
|
125
|
+
}
|
|
126
|
+
return "";
|
|
61
127
|
}
|
|
62
|
-
}
|
|
63
|
-
return
|
|
128
|
+
};
|
|
129
|
+
return validators;
|
|
64
130
|
};
|
|
65
|
-
export const useValidator = (errors, values) => {
|
|
131
|
+
export const useValidator = (errors, values, validators = getDefaultValidators()) => {
|
|
66
132
|
const setError = (name, error) => {
|
|
67
133
|
errors.update((prev) => {
|
|
68
134
|
return { ...prev, [name]: error };
|
|
69
135
|
});
|
|
70
136
|
};
|
|
71
|
-
return
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
case 'match':
|
|
84
|
-
error = typeDetails.length < 2 ? inputName + ': match validation requires party target' : '';
|
|
85
|
-
setError(name, error);
|
|
86
|
-
if (error)
|
|
87
|
-
break;
|
|
88
|
-
partyName = typeDetails[1].trim();
|
|
89
|
-
partyValue = get(values)[partyName] || '';
|
|
90
|
-
error = !value || value !== partyValue ? getErrorText(inputName, 'match', typeDetails[1]) : '';
|
|
91
|
-
setError(name, error);
|
|
92
|
-
break;
|
|
93
|
-
case 'email':
|
|
94
|
-
error = !value || !value.match(regexes['email']) ? getErrorText(inputName, 'email') : '';
|
|
95
|
-
setError(name, error);
|
|
96
|
-
break;
|
|
97
|
-
case 'url':
|
|
98
|
-
error = !value || !value.match(regexes['url']) ? getErrorText(inputName, 'url') : '';
|
|
99
|
-
setError(name, error);
|
|
137
|
+
return {
|
|
138
|
+
validate: async ({ name, value, validations = [], node = undefined }) => {
|
|
139
|
+
if (validations.length) {
|
|
140
|
+
const _validations = validations.includes('required') ? ['required', ...validations.filter((val) => val !== 'required')] : [...validations];
|
|
141
|
+
for (let i = 0; i < _validations.length; ++i) {
|
|
142
|
+
const validation = _validations[i], parts = validation.split(':'), type = parts[0].trim();
|
|
143
|
+
const validator = validators[type];
|
|
144
|
+
if (!validator)
|
|
145
|
+
return;
|
|
146
|
+
const error = validator({ name, label: makeName(name), value, values: get(values), node, parts });
|
|
147
|
+
setError(name, error ? error : '');
|
|
148
|
+
if (error) {
|
|
100
149
|
break;
|
|
101
|
-
|
|
102
|
-
error = !value || !value.match(regexes['ip']) ? getErrorText(inputName, 'ip') : '';
|
|
103
|
-
setError(name, error);
|
|
104
|
-
break;
|
|
105
|
-
case 'integer':
|
|
106
|
-
error = !value || !value.match(regexes['integer']) ? getErrorText(inputName, 'integer') : '';
|
|
107
|
-
setError(name, error);
|
|
108
|
-
break;
|
|
109
|
-
case 'number':
|
|
110
|
-
error = !value || !value.match(regexes['number']) ? getErrorText(inputName, 'number') : '';
|
|
111
|
-
setError(name, error);
|
|
112
|
-
break;
|
|
113
|
-
case 'alpha':
|
|
114
|
-
error = !value || !value.match(regexes['alpha']) ? getErrorText(inputName, 'alpha') : '';
|
|
115
|
-
setError(name, error);
|
|
116
|
-
break;
|
|
117
|
-
case 'alphanum':
|
|
118
|
-
error =
|
|
119
|
-
!value || !value.match(regexes['alphanum']) ? getErrorText(inputName, 'alphanum') : '';
|
|
120
|
-
setError(name, error);
|
|
121
|
-
break;
|
|
122
|
-
case 'min-length':
|
|
123
|
-
error = typeDetails.length < 2 ? inputName + ': min-length validation requires width' : '';
|
|
124
|
-
setError(name, error);
|
|
125
|
-
if (error)
|
|
126
|
-
break;
|
|
127
|
-
error =
|
|
128
|
-
!value || value.length < parseInt(typeDetails[1], 10)
|
|
129
|
-
? getErrorText(inputName, 'min-length', typeDetails[1])
|
|
130
|
-
: '';
|
|
131
|
-
setError(name, error);
|
|
132
|
-
break;
|
|
133
|
-
case 'max-length':
|
|
134
|
-
error = typeDetails.length < 2 ? inputName + ': max-length validation requires width' : '';
|
|
135
|
-
setError(name, error);
|
|
136
|
-
if (error)
|
|
137
|
-
break;
|
|
138
|
-
error =
|
|
139
|
-
!value || value.length > parseInt(typeDetails[1], 10)
|
|
140
|
-
? getErrorText(inputName, 'max-length', typeDetails[1])
|
|
141
|
-
: '';
|
|
142
|
-
setError(name, error);
|
|
143
|
-
break;
|
|
144
|
-
case 'length':
|
|
145
|
-
error = typeDetails.length < 2 ? inputName + ': length validation requires width' : '';
|
|
146
|
-
setError(name, error);
|
|
147
|
-
if (error)
|
|
148
|
-
break;
|
|
149
|
-
error =
|
|
150
|
-
!value || value.length !== parseInt(typeDetails[1], 10)
|
|
151
|
-
? getErrorText(inputName, 'length', typeDetails[1])
|
|
152
|
-
: '';
|
|
153
|
-
setError(name, error);
|
|
154
|
-
break;
|
|
155
|
-
case 'max':
|
|
156
|
-
error = typeDetails.length < 2 ? inputName + ': max validation requires max value' : '';
|
|
157
|
-
setError(name, error);
|
|
158
|
-
if (error)
|
|
159
|
-
break;
|
|
160
|
-
error =
|
|
161
|
-
!value || parseInt(value, 10) !== parseInt(typeDetails[1], 10)
|
|
162
|
-
? getErrorText(inputName, 'max', typeDetails[1])
|
|
163
|
-
: '';
|
|
164
|
-
setError(name, error);
|
|
165
|
-
break;
|
|
166
|
-
case 'max-file-size-mb':
|
|
167
|
-
error = typeDetails.length < 2 ? inputName + ': max file size in MB validation requires max-file-size-mb value' : '';
|
|
168
|
-
setError(name, error);
|
|
169
|
-
if (error)
|
|
170
|
-
break;
|
|
171
|
-
error = checkFileSize(node, parseInt(typeDetails[1], 10));
|
|
172
|
-
setError(name, error);
|
|
173
|
-
break;
|
|
174
|
-
default:
|
|
175
|
-
}
|
|
176
|
-
if (error) {
|
|
177
|
-
setError(name, error);
|
|
178
|
-
break;
|
|
150
|
+
}
|
|
179
151
|
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
validators
|
|
182
155
|
};
|
|
183
156
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
declare const microForm: (props?: MicroFormProps) => MicroFormReturn;
|
|
1
|
+
import { microForm } from "./index.svelte.js";
|
|
2
|
+
export { IS_EMAIL, IS_ALPHA, IS_ALPHANUM, IS_INTEGER, IS_IP, IS_LEN, IT_MATCHES as IS_MATCH_FOR, IS_MAX, IS_FILE_SIZE_MB as IS_MAX_FILE_SIZE, IS_MAX_LEN, IS_MIN, IS_MIN_LEN, IS_NUMBER, IS_REQUIRED, IS_URL } from "./form-validators.js";
|
|
4
3
|
export default microForm;
|
package/dist/index.js
CHANGED
|
@@ -1,65 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { formAction } from './form-action.js';
|
|
4
|
-
const microForm = (props) => {
|
|
5
|
-
// form default values
|
|
6
|
-
const data = props?.data || {};
|
|
7
|
-
// form values
|
|
8
|
-
const values = writable(data);
|
|
9
|
-
// internal checks
|
|
10
|
-
const unfits = writable({});
|
|
11
|
-
// external form errors
|
|
12
|
-
const errors = writable({});
|
|
13
|
-
const isdirty = writable(false);
|
|
14
|
-
const isclean = derived(([errors, unfits]), ([$errors, $unfits]) => {
|
|
15
|
-
const errVals = Object.values($errors);
|
|
16
|
-
const unfitVals = Object.values($unfits);
|
|
17
|
-
return (errVals.length === 0 || errVals.reduce((comm, next) => comm && !next, true))
|
|
18
|
-
&& (unfitVals.length === 0 || unfitVals.reduce((comm, next) => comm && !next, true));
|
|
19
|
-
});
|
|
20
|
-
const valid = derived(([isclean, isdirty]), ([$isclean, $isdirty]) => {
|
|
21
|
-
return $isclean && $isdirty;
|
|
22
|
-
});
|
|
23
|
-
const validationMap = {};
|
|
24
|
-
const { options = {
|
|
25
|
-
validateEvent: 'blur'
|
|
26
|
-
} } = props || {};
|
|
27
|
-
const form = formAction(values, errors, unfits, isdirty, options, validationMap);
|
|
28
|
-
const handleSubmit = (e, handler) => {
|
|
29
|
-
e.preventDefault();
|
|
30
|
-
if (!get(valid))
|
|
31
|
-
return;
|
|
32
|
-
handler({ ...get(values) });
|
|
33
|
-
};
|
|
34
|
-
const onsubmit = (handler) => {
|
|
35
|
-
const onSubmit = async (e) => {
|
|
36
|
-
handleSubmit(e, handler);
|
|
37
|
-
};
|
|
38
|
-
return onSubmit;
|
|
39
|
-
};
|
|
40
|
-
const submit = (formNode, handler) => {
|
|
41
|
-
formNode.addEventListener('submit', (e) => {
|
|
42
|
-
handleSubmit(e, handler);
|
|
43
|
-
});
|
|
44
|
-
};
|
|
45
|
-
const reset = () => {
|
|
46
|
-
errors.set({});
|
|
47
|
-
unfits.set({});
|
|
48
|
-
values.set({ ...data });
|
|
49
|
-
for (const [name, { nodeRef, html }] of Object.entries(validationMap).filter(([, { nodeRef }]) => !!nodeRef)) {
|
|
50
|
-
if (nodeRef) {
|
|
51
|
-
nodeRef[html ? "innerHTML" : 'textContent'] = data[name] || '';
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
return {
|
|
56
|
-
values,
|
|
57
|
-
errors,
|
|
58
|
-
valid,
|
|
59
|
-
form,
|
|
60
|
-
submit,
|
|
61
|
-
onsubmit,
|
|
62
|
-
reset,
|
|
63
|
-
};
|
|
64
|
-
};
|
|
1
|
+
import { microForm } from "./index.svelte.js";
|
|
2
|
+
export { IS_EMAIL, IS_ALPHA, IS_ALPHANUM, IS_INTEGER, IS_IP, IS_LEN, IT_MATCHES as IS_MATCH_FOR, IS_MAX, IS_FILE_SIZE_MB as IS_MAX_FILE_SIZE, IS_MAX_LEN, IS_MIN, IS_MIN_LEN, IS_NUMBER, IS_REQUIRED, IS_URL } from "./form-validators.js";
|
|
65
3
|
export default microForm;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { writable, derived, get } from 'svelte/store';
|
|
2
|
+
import { formAction } from './form-action.js';
|
|
3
|
+
import { bindStateToStore } from "./utils.js";
|
|
4
|
+
export const microForm = (props) => {
|
|
5
|
+
// form default values
|
|
6
|
+
const data = props?.data || {};
|
|
7
|
+
// form values
|
|
8
|
+
const _values = writable({ ...data });
|
|
9
|
+
// internal checks
|
|
10
|
+
const unfits = writable({});
|
|
11
|
+
// external form errors
|
|
12
|
+
const _errors = writable({});
|
|
13
|
+
const isdirty = writable(false);
|
|
14
|
+
const isclean = derived(([_errors, unfits]), ([$errors, $unfits]) => {
|
|
15
|
+
const errVals = Object.values($errors);
|
|
16
|
+
const unfitVals = Object.values($unfits);
|
|
17
|
+
return (errVals.length === 0 || errVals.reduce((comm, next) => comm && !next, true))
|
|
18
|
+
&& (unfitVals.length === 0 || unfitVals.reduce((comm, next) => comm && !next, true));
|
|
19
|
+
});
|
|
20
|
+
const _valid = derived(([isclean, isdirty]), ([$isclean, $isdirty]) => {
|
|
21
|
+
return $isclean && $isdirty;
|
|
22
|
+
});
|
|
23
|
+
const validationMap = {};
|
|
24
|
+
const { options = {
|
|
25
|
+
validateEvent: 'blur',
|
|
26
|
+
validators: {}
|
|
27
|
+
} } = props || {};
|
|
28
|
+
const form = formAction(_values, _errors, unfits, isdirty, options, validationMap);
|
|
29
|
+
const handleSubmit = (e, handler) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
if (!get(_valid))
|
|
32
|
+
return;
|
|
33
|
+
handler({ ...get(_values) });
|
|
34
|
+
};
|
|
35
|
+
const onsubmit = (handler) => {
|
|
36
|
+
const onSubmit = async (e) => {
|
|
37
|
+
handleSubmit(e, handler);
|
|
38
|
+
};
|
|
39
|
+
return onSubmit;
|
|
40
|
+
};
|
|
41
|
+
const submit = (formNode, handler) => {
|
|
42
|
+
formNode.addEventListener('submit', (e) => {
|
|
43
|
+
handleSubmit(e, handler);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
const reset = () => {
|
|
47
|
+
_errors.set({});
|
|
48
|
+
unfits.set({});
|
|
49
|
+
_values.set({ ...data });
|
|
50
|
+
isdirty.set(false);
|
|
51
|
+
for (const [name, { nodeRef, html }] of Object.entries(validationMap).filter(([, { nodeRef }]) => !!nodeRef)) {
|
|
52
|
+
if (nodeRef) {
|
|
53
|
+
if (nodeRef.isContentEditable) {
|
|
54
|
+
nodeRef[html ? "innerHTML" : 'textContent'] = data[name] || '';
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
nodeRef["value"] = data[name] || '';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const values = $state({ ...data });
|
|
63
|
+
const errors = $state({});
|
|
64
|
+
const sanity = $state({ ok: get(_valid) });
|
|
65
|
+
bindStateToStore(values, _values);
|
|
66
|
+
bindStateToStore(errors, _errors);
|
|
67
|
+
_valid.subscribe((changes) => {
|
|
68
|
+
sanity.ok = changes;
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
values,
|
|
72
|
+
errors,
|
|
73
|
+
sanity,
|
|
74
|
+
form,
|
|
75
|
+
submit,
|
|
76
|
+
onsubmit,
|
|
77
|
+
reset,
|
|
78
|
+
};
|
|
79
|
+
};
|
package/dist/internal.d.ts
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -1,28 +1,40 @@
|
|
|
1
1
|
/// <reference types="svelte" />
|
|
2
|
-
import { type Writable
|
|
2
|
+
import { type Writable } from "svelte/store";
|
|
3
3
|
import type { Params } from "./internal.js";
|
|
4
|
-
export type {
|
|
5
|
-
export type
|
|
4
|
+
export type Validator = `max:${number}` | `min:${number}` | `len:${number}` | `minlen:${number}` | `maxlen:${number}` | `file-size-mb:${number}` | `match:${string}` | 'required' | 'email' | 'integer' | 'number' | 'alpha' | 'alphanum' | 'url' | 'ip';
|
|
5
|
+
export type ValidatorKey = 'required' | 'email' | 'integer' | 'number' | 'alpha' | 'alphanum' | 'url' | 'ip' | `max` | `min` | `len` | `minlen` | `maxlen` | `file-size-mb` | `match`;
|
|
6
|
+
export type FieldProps = {
|
|
7
|
+
name: string;
|
|
8
|
+
value: string;
|
|
9
|
+
label: string;
|
|
10
|
+
node?: HTMLElement;
|
|
11
|
+
values: Params;
|
|
12
|
+
parts?: string[];
|
|
13
|
+
};
|
|
14
|
+
export type ValidatorType = (props: FieldProps) => string;
|
|
15
|
+
export type ValidatorMap<T> = {
|
|
16
|
+
[VAL in ValidatorKey]: T;
|
|
17
|
+
};
|
|
6
18
|
export type InputTypes = 'text' | 'number' | 'color' | 'time' | 'date' | 'range' | 'email' | 'hidden' | 'password' | 'tel' | 'url';
|
|
7
19
|
export type FieldType = HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement;
|
|
8
20
|
export type InputType = HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement;
|
|
9
21
|
export interface ValidateArgs {
|
|
10
22
|
name: string;
|
|
11
23
|
value: string;
|
|
12
|
-
validations
|
|
24
|
+
validations?: Validator[];
|
|
13
25
|
node?: HTMLElement;
|
|
14
26
|
}
|
|
15
27
|
export type FormReturn = {
|
|
16
28
|
destroy: () => void;
|
|
17
29
|
};
|
|
18
30
|
export type ValidateEvent = 'input' | 'change' | 'keyup' | 'blur';
|
|
19
|
-
export type FormValues =
|
|
20
|
-
export type FormErrors =
|
|
31
|
+
export type FormValues = Params;
|
|
32
|
+
export type FormErrors = Params;
|
|
21
33
|
export type Dirty = Writable<boolean>;
|
|
22
34
|
export type ActionOptions = {
|
|
23
35
|
validateEvent?: ValidateEvent;
|
|
24
36
|
name?: string;
|
|
25
|
-
validations?:
|
|
37
|
+
validations?: Validator[];
|
|
26
38
|
node?: HTMLElement;
|
|
27
39
|
html?: boolean;
|
|
28
40
|
};
|
|
@@ -32,18 +44,21 @@ export type FormSubmitEvent = SubmitEvent & {
|
|
|
32
44
|
};
|
|
33
45
|
export type FormSubmit = (_data: Params) => void;
|
|
34
46
|
export type FormOptions = {
|
|
35
|
-
validateEvent
|
|
47
|
+
validateEvent?: ValidateEvent;
|
|
48
|
+
validators?: Partial<ValidatorMap<ValidatorType>>;
|
|
36
49
|
};
|
|
37
50
|
export type MicroFormProps = {
|
|
38
51
|
data?: Params;
|
|
39
52
|
options?: FormOptions;
|
|
40
53
|
};
|
|
41
|
-
export type FormSanity =
|
|
54
|
+
export type FormSanity = {
|
|
55
|
+
ok: boolean;
|
|
56
|
+
};
|
|
42
57
|
export type MicroFormReturn = {
|
|
43
58
|
values: FormValues;
|
|
44
59
|
errors: FormErrors;
|
|
60
|
+
sanity: FormSanity;
|
|
45
61
|
form: (node: HTMLElement, eventProps?: ActionOptions) => FormReturn;
|
|
46
|
-
valid: FormSanity;
|
|
47
62
|
submit: (formNode: HTMLFormElement, handler: FormSubmit) => void;
|
|
48
63
|
onsubmit: (handler: FormSubmit) => (e: Event) => Promise<void>;
|
|
49
64
|
reset: () => void;
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/// <reference types="svelte" />
|
|
2
|
+
import type { Writable } from "svelte/store";
|
|
3
|
+
import type { Params } from "./internal.js";
|
|
1
4
|
type TEvent = {
|
|
2
5
|
target: HTMLElement;
|
|
3
6
|
};
|
|
@@ -6,4 +9,6 @@ export declare const getEditableContent: (e: TEvent, isHtml: boolean) => {
|
|
|
6
9
|
value: string;
|
|
7
10
|
};
|
|
8
11
|
export declare const makeName: (str: string) => string;
|
|
12
|
+
export declare const isValidFileSize: (node: HTMLInputElement | undefined, maxFileSizeInMB: number) => string;
|
|
13
|
+
export declare const bindStateToStore: (state: Params, store: Writable<Params>) => void;
|
|
9
14
|
export {};
|
package/dist/utils.js
CHANGED
|
@@ -30,3 +30,24 @@ export const makeName = function (str) {
|
|
|
30
30
|
});
|
|
31
31
|
return new_name;
|
|
32
32
|
};
|
|
33
|
+
export const isValidFileSize = (node, maxFileSizeInMB) => {
|
|
34
|
+
if (!node)
|
|
35
|
+
return '';
|
|
36
|
+
const { files } = node;
|
|
37
|
+
if (!files)
|
|
38
|
+
return `${node.name} is required`;
|
|
39
|
+
const max = maxFileSizeInMB * 1024 * 1024;
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
if (file.size > max) {
|
|
42
|
+
return `File '${file.name}' is larger the ${maxFileSizeInMB}MB.`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return '';
|
|
46
|
+
};
|
|
47
|
+
export const bindStateToStore = (state, store) => {
|
|
48
|
+
store.subscribe((changes) => {
|
|
49
|
+
for (const [k, v] of Object.entries(changes)) {
|
|
50
|
+
state[k] = v;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steveesamson/microform",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"postbuild": "touch ./docs/.nojekyll",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"exports": {
|
|
22
22
|
".": {
|
|
23
|
-
"types": "./dist/
|
|
23
|
+
"types": "./dist/types.d.ts",
|
|
24
24
|
"svelte": "./dist/index.js"
|
|
25
25
|
}
|
|
26
26
|
},
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"!dist/**/*.spec.*"
|
|
33
33
|
],
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"svelte": "^
|
|
35
|
+
"svelte": "^5.0.0-next.135"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@sveltejs/adapter-auto": "^3.0.0",
|
|
@@ -51,14 +51,14 @@
|
|
|
51
51
|
"prettier-plugin-svelte": "^3.1.2",
|
|
52
52
|
"publint": "^0.1.9",
|
|
53
53
|
"shiki": "^0.14.7",
|
|
54
|
-
"svelte": "^
|
|
54
|
+
"svelte": "^5.0.0-next.135",
|
|
55
55
|
"svelte-check": "^3.6.0",
|
|
56
56
|
"tslib": "^2.4.1",
|
|
57
57
|
"typescript": "^5.0.0",
|
|
58
58
|
"vite": "^5.0.3"
|
|
59
59
|
},
|
|
60
60
|
"default": "./dist/index.js",
|
|
61
|
-
"types": "./dist/
|
|
61
|
+
"types": "./dist/types.d.ts",
|
|
62
62
|
"type": "module",
|
|
63
63
|
"keywords": [
|
|
64
64
|
"microform",
|