@steveesamson/microform
Version:
`microform` is a tiny library for managing forms in `svelte/sveltekit`.
499 lines (445 loc) • 13.3 kB
Markdown
# microform
`microform` is a tiny library for managing forms in `svelte/sveltekit`.
## Installation
```bash
# In your project directory
npm install @steveesamson/microform
```
or
```bash
# In your project directory
yarn add @steveesamson/microform
```
[Demo and Docs](https://steveesamson.github.io/microform)
## Usage
Once you've added `microform` to your project, use it as shown below, in your view(`.svelte` files):
### In the view Script
```ts
<script>
import uform from "@steveesamson/microform";
// default form data, probably passed as props
let defaultData:any = $props();
// Instatiate microform
const { form, values, errors, submit, sanity } = uform({
// Set default form data
data:{...defaultData},
// Set a global event for validation, can be overriden on a each field.
// Possible values: blur, change, input, keyup
options:{
// Default event that triggers validation
validateEvent:'blur',
// Configure validators here
validators:{}
// Configurable wait time in millis for form field
// to initialize; default is 200 millis - needed for more involving fields.
fieldWaitTimeInMilliSecond: 200
// Default false, set to true to know if changes are happening.
debug: false
}
});
// Submit handler
// data is the collected form data
// Only called on valid form
// A form is valid when it has no error and at least one of the fields has changed
const onSubmit = (data:unknown) => {
console.log(data);
}
</script>
```
On the instantiation of `microform`, we have access to:
- `values`, a `FormValues`, which represents form data.
- `errors`, a `FormErrors`, which represents form errors.
- `form`, which is a `svelte action` that actually does the `microform` magic.
- `submit`, which is another `svelte action` to handle form submission.
- `sanity`, a `FormSanity`, which tells us if a form is clean/without errors by it's `ok` property.
- `reset`, a function to reset form
- `onsubmit`, a function to handle form submission.
### In the view Html
```html
<form use:submit={onSubmit}>
<label for="fullname">
Name:
<input
type="text"
name="fullname"
id="fullname"
use:form={{ validations: ['required'] }}
/>
{#if errors.fullname}
<small>{errors.fullname}</small>
{/if}
</label>
<label for="dob">
DOB:
<input
type="date"
name="dob"
id="dob"
use:form={{ validations: ['required'] }}
/>
{#if errors.dob}
<small>{errors.dob}</small>
{/if}
</label>
<label for="supper_time">
Supper time:
<input
type="time"
name="supper_time"
id="supper_time"
use:form={{ validations: ['required'] }}
/>
{#if errors.supper_time}
<small>{errors.supper_time}</small>
{/if}
</label>
<label for="password">
Password:
<input
type="password"
name="password"
id="password"
use:form={{ validations: ['required'] }}
/>
{#if errors.password}
<small>{errors.password}</small>
{/if}
</label>
<label for="gender">
Gender:
<select
name="gender"
id="gender"
use:form={{ validateEvent: 'change', validations: ['required'] }}
>
<option value="">Select gender</option>
<option value="M">Male</option>
<option value="F">Female</option>
</select>
{#if errors.gender}
<small>{errors.gender}</small>
{/if}
</label>
<label for="email">
Email:
<input
type="text"
name="email"
id="email"
use:form={{ validations: ['required', 'email'] }}
/>
{#if errors.email}
<small>{errors.email}</small>
{/if}
</label>
<label for="resume">
Resume:
<input
type="file"
name="resume"
id="resume"
use:form={{
validateEvent:'change',
validations: ['required', 'file-size-mb:3']
}}
/>
{#if errors.resume}
<small>{errors.resume}</small>
{/if}
</label>
<label for="comment">
Comment:
<textarea
name="comment"
id="comment"
use:form={{ validations: ['required'] }}
></textarea>
{#if errors.comment}
<small>{errors.comment}</small>
{/if}
</label>
<label for="beverage">
Beverage:
{#each items as item (item.value)}
<span>
<input
type="radio"
name="beverage"
value={item.value}
use:form={{ validateEvent: 'input', validations: ['required'] }}
bind:group={values.beverage}
/>
{item.label}
</span>
{/each}
{#if errors.beverage}
<small>{errors.beverage}</small>
{/if}
</label>
<label for="favfoods">
Fav Foods:
{#each foods as item (item.value)}
<span>
<input
type="checkbox"
name="favfoods"
value={item.value}
use:form={{ validateEvent: 'change', validations: ['required'] }}
bind:group={values['favfoods']}
/>
{item.label}
</span>
{/each}
{#if errors.favfoods}
<small>{errors.favfoods}</small>
{/if}
</label>
<label for="story">
Story:
<div
contenteditable="true"
use:form={{
validateEvent: 'input',
validations: ['required'],
name: 'story',
html: true
}}
></div>
{#if errors.story}
<small>{errors.story}</small>
{/if}
</label>
<button
type='submit'
disabled={!sanity.ok}>
Submit form
</button>
</form>
```
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:
```html
<form>
<label for="password">
Password:
<input
type="text"
name="password"
id="password"
use:form={{
validations: ['required']
}}
/>
{#if errors.password}
<small>{errors.password}</small>
{/if}
</label>
<label for="confirm_password">
Confirm password:
<input
type="text"
name="confirm_password"
id="confirm_password"
use:form={{
validations: ['required','match:password'],
}}
/>
{#if errors.confirm_password}
<small>{errors.confirm_password}</small>
{/if}
</label>
<button type="button" disabled="{!sanity.ok}" onclick="{onsubmit(onSubmit)}">Submit form</button>
</form>
```
## microform Features
`microform` performs its magic by relying the `form` action. The `form` action can optionally accept the following:
```ts
<input
use:form={{
// an optional list of validations
// default is '' - no validations
validations: ['email', 'len:20'],
// an optional string of
// any of blur, change, input, keyup.
// default is blur
validateEvent: 'input',
// an optional string that allows passing field names
// especially useful for contenteditables
// that have no native name attribute
// default is ''
name: '',
// an optional boolean indicating
// whether content should be treated as plain text or html
// also useful for contenteditables
// default is false
html: true
}}
/>
```
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:
```html
<input ... value="{values.email_account}" />
```
### 1. Validations
`validations` is an array of validations to check field values against for correctnes. Uses `validations` props on `form` action. For instance, the following:
```html
<input
type='text'
name='email_account'
id='email_account'
use:form={{
validations:[ 'required', 'email' ]
}}
/>
```
### 2. Validation Event
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:
```html
<select
name='gender'
id='gender'
use:form={{
validations:['required'],
validateEvent:'change'
}}>
<options value=''>Gender please</option>
<options value='F'>Female</option>
<options value='M'>Male</option>
</select>
```
### 3. Supports for contenteditable
`microform` supports `contenteditable` out-of-box:
```html
<form use:submit={onSubmit}>
<label for="story">
Story:
<div
contenteditable="true"
use:form={{
validateEvent: 'input',
validations: ['required'],
name: 'story',
html: true
}}
></div>
{#if errors.story}
<small>{errors.story}</small>
{/if}
</label>
</form>
```
### 4. Provides usable default validations
`microform` provides a set of usable validations out-of-box. The following is a list of provided validations:
- `required`: Usage, `validations:['required']`
- `email`: Usage, `validations:['email']`
- `url`: Usage, `validations:['url']`
- `ip`: Usage, `validations:['ip']`
- `len:<number>`: Usage, `validations:['len:40']`
- `number`: Usage, `validations:['number']`
- `integer`: Usage, `validations:['integer']`
- `alpha`: Usage, `validations:['alpha']` - only alphabets
- `alphanum`: Usage, `validations:['alphanum']` - alphanumeric
- `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
- `minlen:<number>`: Usage, `validations:['minlen:6']`
- `maxlen:<number>`: Usage, `validations:['maxlen:15']`
- `max:<number>`: Usage, `validations:['max:25']`
- `file-size-mb:<number>`: Usage, `validations:['file-size-mb:30']` - for file upload
Every validation listed above also comes with a very good default error message.
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:
```html
<input
type="text"
name="email_account"
id="email_account"
use:form={{
validations:['required','email']
}}
/>
```
# Overriding validators
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.
### Approach 1
```ts
<script>
import uform, { type FieldProps } from "@steveesamson/microform";
// default form data, probably passed as props
export let defaultData:any = {};
// Instatiate microform
const { form, values, errors, submit, sanity } = uform({
// Set default form data
data:{...defaultData},
// Set a global event for validation, can be overriden on a each field.
// Possible values: blur, change, input, keyup
options:{
// Default event that triggers validation
validateEvent:'blur',
// Configure validators here
validators:{
len:({name, label, parts}:FieldProps) =>{
if (!parts || parts.length < 2) {
return `${label}: length validation requires length.`;
}
const extra = parts[1].trim();
return !!value && value.length !== parseInt(parts[1], 10) ? `${label} must exactly be ${extra} characters long.` : "";
}
}
}
});
</script>
```
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:
- IS_REQUIRED = `required`
- IS_EMAIL = `email`
- IS_URL = `url`
- IS_IP = `ip`
- IS_INTEGER = `integer`
- IS_NUMBER = `number`
- IS_ALPHA = `alpha`
- IS_ALPHANUM = `alphanum`
- IS_MIN_LEN = `minlen`
- IS_MAX_LEN = `maxlen`
- IS_LEN = `len`
- IS_MIN = `min`
- IS_MAX = `max`
- IT_MATCHES = `match`
- IS_FILE_SIZE_MB = `file-size-mb`
Therefore, the following is equivalent to the configuration in `Approach 1`:
### Approach 2
```ts
<script>
import uform, { type FieldProps, IS_LEN } from "@steveesamson/microform";
// default form data, probably passed as props
export let defaultData:any = {};
// Instatiate microform
const { form, values, errors, submit, sanity } = uform({
// Set default form data
data:{...defaultData},
// Set a global event for validation, can be overriden on a each field.
// Possible values: blur, change, input, keyup
options:{
// Default event that triggers validation
validateEvent:'blur',
// Configure validators here
validators:{
[IS_LEN]:({name, label, parts}:FieldProps) =>{
if (!parts || parts.length < 2) {
return `${label}: length validation requires length.`;
}
const extra = parts[1].trim();
return !!value && value.length !== parseInt(parts[1], 10) ? `${label} must exactly be ${extra} characters long.` : "";
}
}
}
});
</script>
```
The validation will be used on the view as below:
```html
<input
type="text"
name="comment"
id="comment"
use:form={{
validations:['required','len:30']
}}
/>
```