@steveesamson/microform 0.0.1
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 +322 -0
- package/dist/form-action.d.ts +6 -0
- package/dist/form-action.js +69 -0
- package/dist/form-validators.d.ts +2 -0
- package/dist/form-validators.js +183 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +64 -0
- package/dist/types.d.ts +50 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +32 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# microform
|
|
2
|
+
|
|
3
|
+
`microform` is a tiny library for managing forms in `svelte/sveltekit`.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# In your project directory
|
|
9
|
+
npm install microform
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
or
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# In your project directory
|
|
16
|
+
yarn add microform
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
Once you've added `microform` to your project, use it as shown below, in your view(`.svelte` files):
|
|
22
|
+
|
|
23
|
+
### In the view Script
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
<script lang='ts'>
|
|
27
|
+
import uForm from "microform";
|
|
28
|
+
// default form data, probably passed as props
|
|
29
|
+
export let defaultData:any = {};
|
|
30
|
+
|
|
31
|
+
// Instatiate microform
|
|
32
|
+
const { form, values, errors, submit, valid } = uForm({
|
|
33
|
+
// Set default form data
|
|
34
|
+
data:{...defaultData},
|
|
35
|
+
// Set a global event for validation, can be overriden on a each field.
|
|
36
|
+
// Possible values: blur, change, input, keyup
|
|
37
|
+
validateEvent:'blur'
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Submit handler
|
|
41
|
+
// data is the collected form data
|
|
42
|
+
// Only called on valid form
|
|
43
|
+
// A form is valid when it has no error and at least one of the fields has changed
|
|
44
|
+
const onSubmit = (data:unknown) => {
|
|
45
|
+
console.log(data);
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
On the instantiation of `microform`, we have access to:
|
|
51
|
+
|
|
52
|
+
- `values`, a `FormValues`, which is a `svelte store` for form data.
|
|
53
|
+
- `errors`, a `FormErrors`, which is a `svelte store` for form errors.
|
|
54
|
+
- `form`, which is a `svelte action` that actually does the `microform` magic.
|
|
55
|
+
- `submit`, which is another `svelte action` to handle form submission.
|
|
56
|
+
- `valid`, a `FormSanity`, which is a `svelte store` that tells if a form is clean/without errors.
|
|
57
|
+
- `reset`, a function to reset form
|
|
58
|
+
- `onsubmit`, a function to handle form submission.
|
|
59
|
+
|
|
60
|
+
### In the view Html
|
|
61
|
+
|
|
62
|
+
```html
|
|
63
|
+
<form use:submit={onSubmit}>
|
|
64
|
+
<label for='username'>
|
|
65
|
+
Username:
|
|
66
|
+
<input
|
|
67
|
+
type='text'
|
|
68
|
+
name='username'
|
|
69
|
+
id='username'
|
|
70
|
+
use:form
|
|
71
|
+
data-validations='required'>
|
|
72
|
+
{#if $errors.username}
|
|
73
|
+
<small>{$errors.username}</small>
|
|
74
|
+
{/if}
|
|
75
|
+
</label>
|
|
76
|
+
<label for='email_account'>
|
|
77
|
+
Email Account:
|
|
78
|
+
<input
|
|
79
|
+
type='text'
|
|
80
|
+
name='email_account'
|
|
81
|
+
id='email_account'
|
|
82
|
+
use:form={{
|
|
83
|
+
validations:'required|email'
|
|
84
|
+
}}/>
|
|
85
|
+
{#if $errors.email_account}
|
|
86
|
+
<small>{$errors.email_account}</small>
|
|
87
|
+
{/if}
|
|
88
|
+
</label>
|
|
89
|
+
<label for='gender'>
|
|
90
|
+
Gender:
|
|
91
|
+
<select
|
|
92
|
+
name='gender'
|
|
93
|
+
id='gender'
|
|
94
|
+
use:form={{
|
|
95
|
+
validations:'required',
|
|
96
|
+
validateEvent:'change'
|
|
97
|
+
}}>
|
|
98
|
+
<options value=''>Gender please</option>
|
|
99
|
+
<options value='F'>Female</option>
|
|
100
|
+
<options value='M'>Male</option>
|
|
101
|
+
</select>
|
|
102
|
+
{#if $errors.gender}
|
|
103
|
+
<small>{$errors.gender}</small>
|
|
104
|
+
{/if}
|
|
105
|
+
</label>
|
|
106
|
+
<label for='password'>
|
|
107
|
+
Password:
|
|
108
|
+
<input
|
|
109
|
+
type='text'
|
|
110
|
+
name='password'
|
|
111
|
+
id='password'
|
|
112
|
+
use:form
|
|
113
|
+
data-validations='required'>
|
|
114
|
+
{#if $errors.password}
|
|
115
|
+
<small>{$errors.password}</small>
|
|
116
|
+
{/if}
|
|
117
|
+
</label>
|
|
118
|
+
<label for='confirm_password'>
|
|
119
|
+
Confirm password:
|
|
120
|
+
<input
|
|
121
|
+
type='text'
|
|
122
|
+
name='confirm_password'
|
|
123
|
+
id='confirm_password'
|
|
124
|
+
use:form
|
|
125
|
+
data-validations='required|match:password'>
|
|
126
|
+
{#if $errors.confirm_password}
|
|
127
|
+
<small>{$errors.confirm_password}</small>
|
|
128
|
+
{/if}
|
|
129
|
+
</label>
|
|
130
|
+
<label for="story">
|
|
131
|
+
Story:
|
|
132
|
+
<div
|
|
133
|
+
contenteditable="true"
|
|
134
|
+
use:form={{
|
|
135
|
+
validateEvent: 'input',
|
|
136
|
+
validations: 'required',
|
|
137
|
+
name: 'story',
|
|
138
|
+
html: true
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
{#if $errors.story}
|
|
142
|
+
<small>{$errors.story}</small>
|
|
143
|
+
{/if}
|
|
144
|
+
</label>
|
|
145
|
+
|
|
146
|
+
<button
|
|
147
|
+
type='submit'
|
|
148
|
+
disabled={!$valid}>
|
|
149
|
+
Submit form
|
|
150
|
+
</button>
|
|
151
|
+
</form>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
While the above example uses the `submit` action of `microform`, form could also be submitted by using the `onsubmit` function of `microform`. See the following:
|
|
155
|
+
|
|
156
|
+
```html
|
|
157
|
+
<form>
|
|
158
|
+
<label for="password">
|
|
159
|
+
Password:
|
|
160
|
+
<input type="text" name="password" id="password" use:form data-validations="required" />
|
|
161
|
+
{#if $errors.password}
|
|
162
|
+
<small>{$errors.password}</small>
|
|
163
|
+
{/if}
|
|
164
|
+
</label>
|
|
165
|
+
<label for="confirm_password">
|
|
166
|
+
Confirm password:
|
|
167
|
+
<input
|
|
168
|
+
type="text"
|
|
169
|
+
name="confirm_password"
|
|
170
|
+
id="confirm_password"
|
|
171
|
+
use:form
|
|
172
|
+
data-validations="required|match:password"
|
|
173
|
+
/>
|
|
174
|
+
{#if $errors.confirm_password}
|
|
175
|
+
<small>{$errors.confirm_password}</small>
|
|
176
|
+
{/if}
|
|
177
|
+
</label>
|
|
178
|
+
|
|
179
|
+
<button type="button" disabled="{!$valid}" on:click="{onsubmit(onSubmit)}">Submit form</button>
|
|
180
|
+
</form>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## microform Features
|
|
184
|
+
|
|
185
|
+
`microform` performs its magic by relying the `form` action. The `form` action can optionally accept the following:
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
<input
|
|
189
|
+
use:form={{
|
|
190
|
+
// an optional list of validations
|
|
191
|
+
// default is '' - no validations
|
|
192
|
+
validations: 'email|length:20',
|
|
193
|
+
// an optional string of
|
|
194
|
+
// any of blur, change, input, keyup.
|
|
195
|
+
// default is blur
|
|
196
|
+
validateEvent: 'input',
|
|
197
|
+
// an optional string that allows passing field names
|
|
198
|
+
// especially useful for contenteditables
|
|
199
|
+
// that have no native name attribute
|
|
200
|
+
// default is ''
|
|
201
|
+
name: '',
|
|
202
|
+
// an optional boolean indicating
|
|
203
|
+
// whether content should be treated as plain text or html
|
|
204
|
+
// also useful for contenteditables
|
|
205
|
+
// default is false
|
|
206
|
+
html: true
|
|
207
|
+
}}
|
|
208
|
+
/>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
You need not bind the `values` to your fields except when there is a definite need for it as `form` will coordinate all value changes based on the `data` passed at instantiation, if any. Therefore, constructs like the following might not be neccessary:
|
|
212
|
+
|
|
213
|
+
```html
|
|
214
|
+
<input
|
|
215
|
+
type="text"
|
|
216
|
+
name="email_account"
|
|
217
|
+
id="email_account"
|
|
218
|
+
value="{$values.email_account}"
|
|
219
|
+
data-validations="required|email"
|
|
220
|
+
use:form
|
|
221
|
+
/>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 1. Validations
|
|
225
|
+
|
|
226
|
+
Uses both inline `data-validations` on field and `validations` props on `form` action. For instance, the following are perfectly identical:
|
|
227
|
+
|
|
228
|
+
```html
|
|
229
|
+
<input
|
|
230
|
+
type='text'
|
|
231
|
+
name='email_account'
|
|
232
|
+
id='email_account'
|
|
233
|
+
data-validations='required|email'
|
|
234
|
+
use:form/>
|
|
235
|
+
|
|
236
|
+
<input
|
|
237
|
+
type='text'
|
|
238
|
+
name='email_account'
|
|
239
|
+
id='email_account'
|
|
240
|
+
use:form={{
|
|
241
|
+
validations:'required|email'
|
|
242
|
+
}}/>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### 2. Validation Event
|
|
246
|
+
|
|
247
|
+
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:
|
|
248
|
+
|
|
249
|
+
```html
|
|
250
|
+
<select
|
|
251
|
+
name='gender'
|
|
252
|
+
id='gender'
|
|
253
|
+
use:form={{
|
|
254
|
+
validations:'required',
|
|
255
|
+
validateEvent:'change'
|
|
256
|
+
}}>
|
|
257
|
+
<options value=''>Gender please</option>
|
|
258
|
+
<options value='F'>Female</option>
|
|
259
|
+
<options value='M'>Male</option>
|
|
260
|
+
</select>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### 3. Supports for contenteditable
|
|
264
|
+
|
|
265
|
+
`microform` supports `contenteditable` out-of-box:
|
|
266
|
+
|
|
267
|
+
```html
|
|
268
|
+
<form use:submit={onSubmit}>
|
|
269
|
+
<label for="story">
|
|
270
|
+
Story:
|
|
271
|
+
<div
|
|
272
|
+
contenteditable="true"
|
|
273
|
+
use:form={{
|
|
274
|
+
validateEvent: 'input',
|
|
275
|
+
validations: 'required',
|
|
276
|
+
name: 'story',
|
|
277
|
+
html: true
|
|
278
|
+
}}
|
|
279
|
+
/>
|
|
280
|
+
{#if $errors.story}
|
|
281
|
+
<small>{$errors.story}</small>
|
|
282
|
+
{/if}
|
|
283
|
+
</label>
|
|
284
|
+
</form>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### 4. Provides usable default validations
|
|
288
|
+
|
|
289
|
+
`microform` provides a set of usable validations out-of-box. The following is a list of provided validations:
|
|
290
|
+
|
|
291
|
+
- `required`: Usage, `validations='required'`
|
|
292
|
+
- `email`: Usage, `validations='email'`
|
|
293
|
+
- `url`: Usage, `validations='url'`
|
|
294
|
+
- `ip`: Usage, `validations='ip'`
|
|
295
|
+
- `length`: Usage, `validations='length:40'`
|
|
296
|
+
- `number`: Usage, `validations='number'`
|
|
297
|
+
- `integer`: Usage, `validations='integer'`
|
|
298
|
+
- `alpha`: Usage, `validations='alpha'` - only alphabets
|
|
299
|
+
- `alphanum`: Usage, `validations='alphanum'` - alphanumeric
|
|
300
|
+
- `match`: Usage, `validations='match:<name-of-field-to-match>'`. For instance, this is examplified in our example with `password` and `confirm_password` fields
|
|
301
|
+
- `min-length`: Usage, `validations='min-length:6'`
|
|
302
|
+
- `max-length`: Usage, `validations='max-length:15'`
|
|
303
|
+
- `max`: Usage, `validations='max:25'`
|
|
304
|
+
- `max-file-size-mb`: Usage, `validations='max-file-size-mb:30'` - for file upload
|
|
305
|
+
|
|
306
|
+
Every validation listed above also comes with a very good default error message.
|
|
307
|
+
|
|
308
|
+
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:
|
|
309
|
+
|
|
310
|
+
```html
|
|
311
|
+
<input
|
|
312
|
+
type="text"
|
|
313
|
+
name="email_account"
|
|
314
|
+
id="email_account"
|
|
315
|
+
data-validations="required|email"
|
|
316
|
+
use:form
|
|
317
|
+
/>
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
# TODO
|
|
321
|
+
|
|
322
|
+
I shall be working on how to allow users register their `validators` and `error messages` for the purpose of customisation.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/// <reference types="svelte" />
|
|
2
|
+
import { type Writable } from "svelte/store";
|
|
3
|
+
import type { ActionOptions, FormErrors, FormOptions, FormValues, Params } from "./types.js";
|
|
4
|
+
export declare const formAction: (values: FormValues, errors: FormErrors, unfits: Writable<Params>, isdirty: Writable<boolean>, options: FormOptions, validationMap: Params) => (node: HTMLElement, eventProps?: ActionOptions) => {
|
|
5
|
+
destroy(): void;
|
|
6
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { get } from "svelte/store";
|
|
2
|
+
import { useValidator } from "./form-validators.js";
|
|
3
|
+
import { getEditableContent } from "./utils.js";
|
|
4
|
+
const isField = (node) => {
|
|
5
|
+
return node instanceof HTMLSelectElement ||
|
|
6
|
+
node instanceof HTMLInputElement ||
|
|
7
|
+
node instanceof HTMLTextAreaElement;
|
|
8
|
+
};
|
|
9
|
+
const isExcluded = (node) => {
|
|
10
|
+
return node instanceof HTMLInputElement && ['radio', 'checkbox', 'file'].includes(node.type.toLowerCase());
|
|
11
|
+
};
|
|
12
|
+
const checkFormFitness = (values, unfits, validationMap) => {
|
|
13
|
+
const validate = useValidator(unfits, values);
|
|
14
|
+
const _values = get(values);
|
|
15
|
+
for (const [name, { validations }] of Object.entries(validationMap)) {
|
|
16
|
+
validate({ name, value: _values[name], validations, });
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
export const formAction = (values, errors, unfits, isdirty, options, validationMap) => {
|
|
20
|
+
const validate = useValidator(errors, values);
|
|
21
|
+
return (node, eventProps) => {
|
|
22
|
+
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 };
|
|
26
|
+
const storedValue = get(values)[name] || '';
|
|
27
|
+
let defValue = storedValue;
|
|
28
|
+
if (isField(node) && !isExcluded(node)) {
|
|
29
|
+
defValue = node.value || storedValue;
|
|
30
|
+
node.value = defValue;
|
|
31
|
+
}
|
|
32
|
+
else if (node.isContentEditable) {
|
|
33
|
+
defValue = node.innerHTML || storedValue;
|
|
34
|
+
node.innerHTML = defValue;
|
|
35
|
+
}
|
|
36
|
+
values.update((data) => {
|
|
37
|
+
return { ...data, [name]: defValue };
|
|
38
|
+
});
|
|
39
|
+
let unsubscribe;
|
|
40
|
+
const updateNode = (e) => {
|
|
41
|
+
if (!unsubscribe) {
|
|
42
|
+
unsubscribe = values.subscribe((data) => {
|
|
43
|
+
validate({ name, value: data[name], validations, node });
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (isField(node) && !isExcluded(node)) {
|
|
47
|
+
const value = e.target.value || '';
|
|
48
|
+
values.update((data) => {
|
|
49
|
+
return { ...data, [name]: value };
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else if (node.isContentEditable) {
|
|
53
|
+
const { value: htm, text } = getEditableContent({ target: node }, html);
|
|
54
|
+
values.update((data) => {
|
|
55
|
+
return { ...data, [name]: htm, [`${name}-text`]: text };
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
checkFormFitness(values, unfits, validationMap);
|
|
59
|
+
isdirty.set(true);
|
|
60
|
+
};
|
|
61
|
+
node.addEventListener(validateEvent, updateNode);
|
|
62
|
+
return {
|
|
63
|
+
destroy() {
|
|
64
|
+
unsubscribe?.();
|
|
65
|
+
node.removeEventListener(validateEvent, updateNode);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { get } from "svelte/store";
|
|
2
|
+
import { makeName } from "./utils.js";
|
|
3
|
+
const regexes = {
|
|
4
|
+
number: /^[-+]?[0-9]+(\.[0-9]+)?$/g,
|
|
5
|
+
alpha: /^[A-Z\s]+$/gi,
|
|
6
|
+
alphanum: /^[A-Z]+[A-Z0-9]+$/gi,
|
|
7
|
+
integer: /^[-+]?\d+$/g,
|
|
8
|
+
email: /^[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}$/gi,
|
|
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
|
+
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
|
+
};
|
|
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.`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return "";
|
|
64
|
+
};
|
|
65
|
+
export const useValidator = (errors, values) => {
|
|
66
|
+
const setError = (name, error) => {
|
|
67
|
+
errors.update((prev) => {
|
|
68
|
+
return { ...prev, [name]: error };
|
|
69
|
+
});
|
|
70
|
+
};
|
|
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);
|
|
100
|
+
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;
|
|
179
|
+
}
|
|
180
|
+
} //end
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { writable, derived, get } from 'svelte/store';
|
|
2
|
+
import { formAction } from './form-action.js';
|
|
3
|
+
const useForm = (props) => {
|
|
4
|
+
// form default values
|
|
5
|
+
const data = props?.data || {};
|
|
6
|
+
// form values
|
|
7
|
+
const values = writable(data);
|
|
8
|
+
// internal checks
|
|
9
|
+
const unfits = writable({});
|
|
10
|
+
// external form errors
|
|
11
|
+
const errors = writable({});
|
|
12
|
+
const isdirty = writable(false);
|
|
13
|
+
const isclean = derived(([errors, unfits]), ([$errors, $unfits]) => {
|
|
14
|
+
const errVals = Object.values($errors);
|
|
15
|
+
const unfitVals = Object.values($unfits);
|
|
16
|
+
return (errVals.length === 0 || errVals.reduce((comm, next) => comm && !next, true))
|
|
17
|
+
&& (unfitVals.length === 0 || unfitVals.reduce((comm, next) => comm && !next, true));
|
|
18
|
+
});
|
|
19
|
+
const valid = derived(([isclean, isdirty]), ([$isclean, $isdirty]) => {
|
|
20
|
+
return $isclean && $isdirty;
|
|
21
|
+
});
|
|
22
|
+
const validationMap = {};
|
|
23
|
+
const { options = {
|
|
24
|
+
validateEvent: 'blur'
|
|
25
|
+
} } = props || {};
|
|
26
|
+
const form = formAction(values, errors, unfits, isdirty, options, validationMap);
|
|
27
|
+
const handleSubmit = (e, handler) => {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
if (!get(valid))
|
|
30
|
+
return;
|
|
31
|
+
handler({ ...get(values) });
|
|
32
|
+
};
|
|
33
|
+
const onsubmit = (handler) => {
|
|
34
|
+
const onSubmit = async (e) => {
|
|
35
|
+
handleSubmit(e, handler);
|
|
36
|
+
};
|
|
37
|
+
return onSubmit;
|
|
38
|
+
};
|
|
39
|
+
const submit = (formNode, handler) => {
|
|
40
|
+
formNode.addEventListener('submit', (e) => {
|
|
41
|
+
handleSubmit(e, handler);
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
const reset = () => {
|
|
45
|
+
errors.set({});
|
|
46
|
+
unfits.set({});
|
|
47
|
+
values.set({ ...data });
|
|
48
|
+
for (const [name, { nodeRef, html }] of Object.entries(validationMap).filter(([, { nodeRef }]) => !!nodeRef)) {
|
|
49
|
+
if (nodeRef) {
|
|
50
|
+
nodeRef[html ? "innerHTML" : 'textContent'] = data[name] || '';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
values,
|
|
56
|
+
errors,
|
|
57
|
+
valid,
|
|
58
|
+
form,
|
|
59
|
+
submit,
|
|
60
|
+
onsubmit,
|
|
61
|
+
reset,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
export default useForm;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/// <reference types="svelte" />
|
|
2
|
+
import { type Writable, type Readable } from "svelte/store";
|
|
3
|
+
export type FieldTypes = "Standard" | "Popover" | "Checkable";
|
|
4
|
+
export type InputTypes = 'text' | 'number' | 'color' | 'time' | 'date' | 'range' | 'email' | 'hidden' | 'password' | 'tel' | 'url';
|
|
5
|
+
export type FieldType = HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement;
|
|
6
|
+
export type InputType = HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement;
|
|
7
|
+
export interface ValidateArgs {
|
|
8
|
+
name: string;
|
|
9
|
+
value: string;
|
|
10
|
+
validations: string;
|
|
11
|
+
node?: HTMLElement;
|
|
12
|
+
}
|
|
13
|
+
export type ValidateEvent = 'input' | 'change' | 'keyup' | 'blur';
|
|
14
|
+
export type Params = {
|
|
15
|
+
[key: string]: string | number | Array<any> | any;
|
|
16
|
+
};
|
|
17
|
+
export type FormValues = Writable<Params>;
|
|
18
|
+
export type FormErrors = Writable<Params>;
|
|
19
|
+
export type Dirty = Writable<boolean>;
|
|
20
|
+
export type ActionOptions = {
|
|
21
|
+
validateEvent?: ValidateEvent;
|
|
22
|
+
name?: string;
|
|
23
|
+
validations?: string;
|
|
24
|
+
node?: HTMLElement;
|
|
25
|
+
html?: boolean;
|
|
26
|
+
};
|
|
27
|
+
export type FormAction = (node: HTMLElement, eventProps?: ActionOptions) => {
|
|
28
|
+
destroy: () => void;
|
|
29
|
+
};
|
|
30
|
+
export type FormSubmitEvent = SubmitEvent & {
|
|
31
|
+
currentTarget: EventTarget & HTMLFormElement;
|
|
32
|
+
};
|
|
33
|
+
export type FormSubmit = (_data: Params) => void;
|
|
34
|
+
export type FormOptions = {
|
|
35
|
+
validateEvent: ValidateEvent;
|
|
36
|
+
};
|
|
37
|
+
export type UseFormProps = {
|
|
38
|
+
data?: Params;
|
|
39
|
+
options?: FormOptions;
|
|
40
|
+
};
|
|
41
|
+
export type FormSanity = Readable<boolean>;
|
|
42
|
+
export type UseFormReturn = {
|
|
43
|
+
values: FormValues;
|
|
44
|
+
errors: FormErrors;
|
|
45
|
+
form: (node: HTMLElement, eventProps?: ActionOptions) => void;
|
|
46
|
+
valid: FormSanity;
|
|
47
|
+
submit: (formNode: HTMLFormElement, handler: FormSubmit) => void;
|
|
48
|
+
onsubmit: (handler: FormSubmit) => (e: Event) => Promise<void>;
|
|
49
|
+
reset: () => void;
|
|
50
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import {} from "svelte/store";
|
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const getEditableContent = (e, isHtml) => {
|
|
2
|
+
const el = e.target;
|
|
3
|
+
const text = el.textContent?.trim() || "";
|
|
4
|
+
if (!isHtml) {
|
|
5
|
+
return { text, value: text };
|
|
6
|
+
}
|
|
7
|
+
let htm = el.innerHTML;
|
|
8
|
+
htm = htm.trim();
|
|
9
|
+
htm = htm.replace(/<br>/g, "");
|
|
10
|
+
htm = htm.replace(/<div><\/div>/g, "");
|
|
11
|
+
htm = htm.replace(/<p><\/p>/g, "");
|
|
12
|
+
htm = htm.replace(/<div>/g, "<p>");
|
|
13
|
+
htm = htm.replace(/<\/div>/g, "</p>");
|
|
14
|
+
htm = htm.trim()
|
|
15
|
+
? htm.indexOf("<p>") === -1
|
|
16
|
+
? `<p>${htm}</p>`
|
|
17
|
+
: htm
|
|
18
|
+
: htm;
|
|
19
|
+
return { value: htm.trim(), text };
|
|
20
|
+
};
|
|
21
|
+
export const makeName = function (str) {
|
|
22
|
+
const index = str.indexOf('_');
|
|
23
|
+
if (index < 0) {
|
|
24
|
+
return str === 'id' ? str.toUpperCase() : str.charAt(0).toUpperCase() + str.substring(1);
|
|
25
|
+
}
|
|
26
|
+
const names = str.split('_');
|
|
27
|
+
let new_name = '';
|
|
28
|
+
names.forEach(function (s) {
|
|
29
|
+
new_name += new_name.length > 0 ? ' ' + makeName(s) : makeName(s);
|
|
30
|
+
});
|
|
31
|
+
return new_name;
|
|
32
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@steveesamson/microform",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "vite dev",
|
|
6
|
+
"build": "vite build && npm run package",
|
|
7
|
+
"preview": "vite preview",
|
|
8
|
+
"package": "svelte-kit sync && svelte-package && publint",
|
|
9
|
+
"prepublishOnly": "npm run package",
|
|
10
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
11
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
12
|
+
"lint": "prettier --check . && eslint .",
|
|
13
|
+
"format": "prettier --write ."
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"svelte": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"!dist/**/*.test.*",
|
|
24
|
+
"!dist/**/*.spec.*"
|
|
25
|
+
],
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"svelte": "^4.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@sveltejs/adapter-auto": "^3.0.0",
|
|
31
|
+
"@sveltejs/adapter-static": "^3.0.1",
|
|
32
|
+
"@sveltejs/kit": "^2.0.0",
|
|
33
|
+
"@sveltejs/package": "^2.0.0",
|
|
34
|
+
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
|
35
|
+
"@types/eslint": "8.56.0",
|
|
36
|
+
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
37
|
+
"@typescript-eslint/parser": "^6.0.0",
|
|
38
|
+
"eslint": "^8.56.0",
|
|
39
|
+
"eslint-config-prettier": "^9.1.0",
|
|
40
|
+
"eslint-plugin-svelte": "^2.35.1",
|
|
41
|
+
"mdsvex": "^0.11.0",
|
|
42
|
+
"prettier": "^3.1.1",
|
|
43
|
+
"prettier-plugin-svelte": "^3.1.2",
|
|
44
|
+
"publint": "^0.1.9",
|
|
45
|
+
"svelte": "^4.2.7",
|
|
46
|
+
"svelte-check": "^3.6.0",
|
|
47
|
+
"tslib": "^2.4.1",
|
|
48
|
+
"typescript": "^5.0.0",
|
|
49
|
+
"vite": "^5.0.3"
|
|
50
|
+
},
|
|
51
|
+
"svelte": "./dist/index.js",
|
|
52
|
+
"types": "./dist/index.d.ts",
|
|
53
|
+
"type": "module",
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"shiki": "^0.14.7"
|
|
56
|
+
}
|
|
57
|
+
}
|