microform

microform is a tiny library for managing forms in svelte/sveltekit.

Installation

# In your project directory
npm install @steveesamson/microform

or

# In your project directory
yarn add @steveesamson/microform

Usage

Once you’ve added microform to your project, use it as shown below, in your view(.svelte files):

In the view Script

<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:{}
    }
});

// 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:

In the view 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:

<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:

<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:

<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:

<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:

<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:

<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:

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:

<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

<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:

Therefore, the following is equivalent to the configuration in Approach 1:

Approach 2

<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:

<input
	type="text"
	name="comment"
	id="comment"
    use:form={{
        validations:['required','len:30']
    }}
/>