@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 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 lang='ts'>
28
+ <script>
29
29
  import uForm from "@steveesamson/microform";
30
30
  // default form data, probably passed as props
31
- export let defaultData:any = {};
31
+ let defaultData:any = $props();
32
32
 
33
33
  // Instatiate microform
34
- const { form, values, errors, submit, valid } = uForm({
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
- validateEvent:'blur'
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 is a `svelte store` for form data.
55
- - `errors`, a `FormErrors`, which is a `svelte store` for form errors.
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
- - `valid`, a `FormSanity`, which is a `svelte store` that tells if a form is clean/without errors.
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='username'>
67
- Username:
68
- <input
69
- type='text'
70
- name='username'
71
- id='username'
72
- use:form
73
- data-validations='required'>
74
- {#if $errors.username}
75
- <small>{$errors.username}</small>
76
- {/if}
77
- </label>
78
- <label for='email_account'>
79
- Email Account:
80
- <input
81
- type='text'
82
- name='email_account'
83
- id='email_account'
84
- use:form={{
85
- validations:'required|email'
86
- }}/>
87
- {#if $errors.email_account}
88
- <small>{$errors.email_account}</small>
89
- {/if}
90
- </label>
91
- <label for='gender'>
92
- Gender:
93
- <select
94
- name='gender'
95
- id='gender'
96
- use:form={{
97
- validations:'required',
98
- validateEvent:'change'
99
- }}>
100
- <options value=''>Gender please</option>
101
- <options value='F'>Female</option>
102
- <options value='M'>Male</option>
103
- </select>
104
- {#if $errors.gender}
105
- <small>{$errors.gender}</small>
106
- {/if}
107
- </label>
108
- <label for='password'>
109
- Password:
110
- <input
111
- type='text'
112
- name='password'
113
- id='password'
114
- use:form
115
- data-validations='required'>
116
- {#if $errors.password}
117
- <small>{$errors.password}</small>
118
- {/if}
119
- </label>
120
- <label for='confirm_password'>
121
- Confirm password:
122
- <input
123
- type='text'
124
- name='confirm_password'
125
- id='confirm_password'
126
- use:form
127
- data-validations='required|match:password'>
128
- {#if $errors.confirm_password}
129
- <small>{$errors.confirm_password}</small>
130
- {/if}
131
- </label>
132
- <label for="story">
133
- Story:
134
- <div
135
- contenteditable="true"
136
- use:form={{
137
- validateEvent: 'input',
138
- validations: 'required',
139
- name: 'story',
140
- html: true
141
- }}
142
- />
143
- {#if $errors.story}
144
- <small>{$errors.story}</small>
145
- {/if}
146
- </label>
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={!$valid}>
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 type="text" name="password" id="password" use:form data-validations="required" />
163
- {#if $errors.password}
164
- <small>{$errors.password}</small>
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
- data-validations="required|match:password"
256
+ use:form={{
257
+ validations: ['required','match:password'],
258
+ }}
175
259
  />
176
- {#if $errors.confirm_password}
177
- <small>{$errors.confirm_password}</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="{!$valid}" on:click="{onsubmit(onSubmit)}">Submit form</button>
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
- ```javascript
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|length:20',
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
- type="text"
218
- name="email_account"
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
- Uses both inline `data-validations` on field and `validations` props on `form` action. For instance, the following are perfectly identical:
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|email'
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 $errors.story}
283
- <small>{$errors.story}</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='required'`
294
- - `email`: Usage, `validations='email'`
295
- - `url`: Usage, `validations='url'`
296
- - `ip`: Usage, `validations='ip'`
297
- - `length`: Usage, `validations='length:40'`
298
- - `number`: Usage, `validations='number'`
299
- - `integer`: Usage, `validations='integer'`
300
- - `alpha`: Usage, `validations='alpha'` - only alphabets
301
- - `alphanum`: Usage, `validations='alphanum'` - alphanumeric
302
- - `match`: Usage, `validations='match:<name-of-field-to-match>'`. For instance, this is examplified in our example with `password` and `confirm_password` fields
303
- - `min-length`: Usage, `validations='min-length:6'`
304
- - `max-length`: Usage, `validations='max-length:15'`
305
- - `max`: Usage, `validations='max:25'`
306
- - `max-file-size-mb`: Usage, `validations='max-file-size-mb:30'` - for file upload
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 separating each validation rule with a `pipe`, `|`. For instance, a required field that also should be an email field could be validated thus:
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
- data-validations="required|email"
318
- use:form
391
+ use:form={{
392
+ validations:['required','email']
393
+ }}
319
394
  />
320
395
  ```
321
396
 
322
- # TODO
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
- I shall be working on how to allow users register their `validators` and `error messages` for the purpose of customisation.
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
+ ```
@@ -1,4 +1,5 @@
1
1
  /// <reference types="svelte" />
2
2
  import { type Writable } from "svelte/store";
3
- import type { FormErrors, FormOptions, FormValues, Params, FormAction } from "./types.js";
4
- export declare const formAction: (values: FormValues, errors: FormErrors, unfits: Writable<Params>, isdirty: Writable<boolean>, options: FormOptions, validationMap: Params) => FormAction;
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;
@@ -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', 'file'].includes(node.type.toLowerCase());
10
+ return node instanceof HTMLInputElement && ['radio', 'checkbox'].includes(node.type.toLowerCase());
11
11
  };
12
- const checkFormFitness = (values, unfits, validationMap) => {
13
- const validate = useValidator(unfits, values);
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 validate = useValidator(errors, values);
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, validations: dsvalidations = '' } = node.dataset || {};
24
- const { name = dsname, validations = dsvalidations, validateEvent = options.validateEvent, html = false } = eventProps || {};
25
- validationMap[name] = { validations, html, nodeRef: isField(node) ? false : node };
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
- checkFormFitness(values, unfits, validationMap);
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
- import type { FormErrors, FormValues, ValidateArgs } from "./types.js";
2
- export declare const useValidator: (errors: FormErrors, values: FormValues) => ({ name, value, validations, node }: ValidateArgs) => Promise<void>;
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
+ };
@@ -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 getErrorText = (inputName, errorType, extra = '') => {
13
- inputName = makeName(inputName);
14
- switch (errorType.toLowerCase()) {
15
- case 'required':
16
- return inputName + ' is mandatory.';
17
- case 'match':
18
- return inputName + ' does not match ' + makeName(extra);
19
- case 'func':
20
- return inputName + extra;
21
- case 'captcha':
22
- return inputName + ' does not match the image.';
23
- case 'email':
24
- return inputName + ' should be a valid email.';
25
- case 'url':
26
- return inputName + ' should be a valid URL.';
27
- case 'cron':
28
- return inputName + ' should be a valid cron expression.';
29
- case 'ip':
30
- return inputName + ' should be a valid IP.';
31
- case 'integer':
32
- return inputName + ' must be an integer.';
33
- case 'number':
34
- return inputName + ' should be a number.';
35
- case 'alpha':
36
- return inputName + ' should be a string of alphabets.';
37
- case 'alphanum':
38
- return inputName + ' should be a string of alphanumerics starting with alphabets.';
39
- case 'min-length':
40
- return inputName + ' must be at least ' + extra + ' characters long.';
41
- case 'max-length':
42
- return inputName + ' must not be more than ' + extra + ' characters long.';
43
- case 'length':
44
- return inputName + ' must be exactly ' + extra + ' characters long.';
45
- // case 'length':
46
- // return inputName + ' must not be greater than ' + extra + '.';
47
- default:
48
- return errorType;
49
- }
50
- };
51
- const checkFileSize = (node, maxFileSizeInMB) => {
52
- if (!node)
53
- return "";
54
- const { files } = node;
55
- if (!files)
56
- return `${node.name} is required`;
57
- const max = maxFileSizeInMB * 1024 * 1024;
58
- for (const file of files) {
59
- if (file.size > max) {
60
- return `File '${file.name}' is larger the ${maxFileSizeInMB}MB.`;
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 async ({ name, value, validations = '', node = undefined }) => {
72
- let error = '';
73
- if (validations) {
74
- const inputName = name, _validations = validations.split('|');
75
- for (let i = 0; i < _validations.length; ++i) {
76
- const validation = _validations[i], typeDetails = validation.split(':'), type = typeDetails[0].trim();
77
- let partyName, partyValue;
78
- switch (type.toLowerCase()) {
79
- case 'required':
80
- error = !value || !value.length ? getErrorText(inputName, 'required') : '';
81
- setError(name, error);
82
- break;
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
- case 'ip':
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
- } //end
181
- }
152
+ }
153
+ },
154
+ validators
182
155
  };
183
156
  };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- export * from "./types.js";
2
- import type { MicroFormProps, MicroFormReturn } from './types.js';
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
- export * from "./types.js";
2
- import { writable, derived, get } from 'svelte/store';
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
+ };
@@ -1,3 +1,4 @@
1
- export type Params = {
2
- [key: string]: string | number | Array<any> | any;
1
+ export type Params<T = any> = {
2
+ [key: string | number | symbol]: T;
3
3
  };
4
+ export type Primitive = string | number | boolean | string[] | number[] | boolean[];
package/dist/types.d.ts CHANGED
@@ -1,28 +1,40 @@
1
1
  /// <reference types="svelte" />
2
- import { type Writable, type Readable } from "svelte/store";
2
+ import { type Writable } from "svelte/store";
3
3
  import type { Params } from "./internal.js";
4
- export type { Params };
5
- export type FieldTypes = "Standard" | "Popover" | "Checkable";
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: string;
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 = Writable<Params>;
20
- export type FormErrors = Writable<Params>;
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?: string;
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: 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 = Readable<boolean>;
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.11",
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/index.d.ts",
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": "^4.0.0"
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": "^4.2.7",
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/index.d.ts",
61
+ "types": "./dist/types.d.ts",
62
62
  "type": "module",
63
63
  "keywords": [
64
64
  "microform",