@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 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,2 @@
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>;
@@ -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
+ };
@@ -0,0 +1,3 @@
1
+ import type { UseFormProps, UseFormReturn } from './types.js';
2
+ declare const useForm: (props?: UseFormProps) => UseFormReturn;
3
+ export default useForm;
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;
@@ -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";
@@ -0,0 +1,9 @@
1
+ type TEvent = {
2
+ target: HTMLElement;
3
+ };
4
+ export declare const getEditableContent: (e: TEvent, isHtml: boolean) => {
5
+ text: string;
6
+ value: string;
7
+ };
8
+ export declare const makeName: (str: string) => string;
9
+ export {};
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
+ }