Felte: A form library for Svelte
Felte is a simple to use form library for Svelte. It is based on Svelte stores nd Selte actions for its functionality. No Field
or Form
components, just plain stores and actions to build your form however you like. You can see it in actino in this CodeSandbox demo!
Why
I felt that Svelte would allow to create a simple, almost configuration-less way to handle forms. Current libraries (at least that I have found) still make forms feel reliant on a lot of configuration, or custom Field and Form components which make it a little bit harder to customize styles. I wanted a library that would feel as simple as possible to make a form reactive, without relying on custom components, to make styling and handling forms as simple as possible. TypeScript is also a big plus.
In order to accomplish usage as simple as possible, Felte takes advantage of Svelte actions to be able to make a form reactive using only the use
directive. Felte also has built-in error reporting capabilities by using the browser's Constraint Validation API. This means you can use the :valid
and :invalid
pseudo-classes to style your components and do not need to worry about reporting errors as long as you return appropriate messages from the validate
function.
Installation
npm install --save felte
# Or if you use yarn
yarn add felte
Usage
Felte exports a single createForm
function that accepts a config object with the following interface:
interface FormConfig<D extends Record<string, unknown>> {
initialValues?: D;
validate?: (values: D) => Errors<D>;
onSubmit: (values: D) => void;
onError?: (errors: unknown) => void;
useConstraintApi?: boolean;
}
initialValues
refers to the initial values of the form.validate
is a custom validation function that must return an object with the same props as initialValues, but with error messages orundefined
as values.onSubmit
is the function that will be executed when the form is submited.onError
is a function that will run if the submit throws an exception. It will contain the error catched. This is optional and potential exceptions might as well be handled inside theonSubmit
function. =useConstraintApi
this tells Felte to use or not use the browser's Constraint Validation API to the report errors found in thevalidate
function. By default it isfalse
.
When called, createForm
will return an object with the following interface:
type FormAction = (node: HTMLFormElement) => { destroy: () => void };
export interface Form<D extends Record<string, unknown>> {
form: FormAction;
data: Writable<D>;
errors: Readable<Errors<D>>;
touched: Writable<Touched<D>>;
handleSubmit: (e: Event) => void;
isValid: Readable<boolean>;
isSubmitting: Writable<boolean>;
}
form
is a function to be used with theuse:
directive for Svelte.data
is a writable store with the current values from the form.errors
is a readable store with the current errors.touched
is a readable store that defines if the fields have been touched. It's an object with the same keys as data, but with boolean values.handleSubmit
is the event handler to be passed toon:submit
.isValid
is a readable store that only holds a boolean denoting if the form has any errors or not.isSubmitting
is a writable store that only holds a boolean denoting if the form is submitting or not.
If a validate
function is provided, Felte will add a novalidate
to the form so it doesn't clash with the browser's built-in validations such as the ones resulting from required
, pattern
or due to types such as email
or url
. This is done on JavaScript's mount so the browser's validations will be run if JavaScript does not load. You may add these attributes anyway for accessibility purposes, and leaving them in may help you make these forms works even if JavaScript hasn't loaded yet. If a validate
function is not defined, Felte will not interfere with the browser's validation.
Using the form action
The recommended way to use it is by using the form
action from createForm
and using it in the form element of your form.
<script>
import { createForm } from 'felte'
const { form, data, errors } = createForm({
validate: (values) => {
/* validate and return errors found */
},
onSubmit: async (values) => {
/* call to an api */
},
useConstraintApi: true,
})
$: console.log($data)
$: console.log($errors)
</script>
<form use:form>
<input type=text name=email>
<input type=password name=password>
<input type=submit value="Sign in">
</form>
That's all you need! With the example above you'll see Felte automatically updating the values of data
when you type, as well as errors
when finding an error. Note that the only required property for createForm
is onSubmit
.
Also note that using the data
and errors
store is completely optional in this method, since you already get access to the values of the form in the onSubmit
function, and validation errors are reported with the browser's Constraint Validation API by setting useConstraintApi
to true
.
If using Felte this way, make sure to set the
name
attributes of your inputs since that is what Felte uses to map to thedata
store.
Default values are taken from the fields'
value
and/orchecked
attributes.initialValues
is ignored if you use this approach.
Using this approach data
will be undefined until the form element loads.
Nested forms
Felte supports the usage of nested objects for forms by setting the name of an input to the format of object.prop
. It supports multiple levels. The behaviour is the same as previously explained, taking the default values from the value
and/or checked
attributes when appropriate.
<form use:form>
<input name="account.email">
<input name="account.password">
<input name="profile.firstName">
<input name="profile.lastName">
<input type="submit" value="Create account">
</form>
You can also "namespace" the inputs using the fieldset
tag like this:
<form use:form>
<fieldset name="account">
<input name="email">
<input name="password">
</fieldset>
<fieldset name="profile">
<input name="firstName">
<input name="lastName">
</fieldset>
<input type="submit" value="Create account">
</form>
Both of these would result in a data object with this shape:
{
account: {
email: '',
password: '',
},
profile: {
firstName: '',
lastName: '',
},
}
Again, be mindful of the fact that data
will be undefined until the form element loads.
Dynamic forms
You can freely add/remove fields from the form and Felte will handle it.
<form use:form>
<fieldset name="account">
<input name="email">
<input name="password">
</fieldset>
{#if condition}
<fieldset name="profile" data-unset-on-remove=true>
<input name="firstName">
<input name="lastName" data-unset-on-remove=false>
</fieldset>
{/if}
<input type="submit" value="Create account">
</form>
The data-unset-on-remove=true
tells Felte to remove the property from the data object when the HTML element is removed from the DOM. By default this is false. If you do not set this attribute to true
, the properties from the removed elements will remain in the data object untouched.
You can set the data-unset-on-remove=true
attribute to a fieldset
element and all the elements contained within the fieldset will be unset on removal of the node, unless any element within the fieldset element have data-unset-on-remove
set to false.
Felte takes any value that is not
true
asfalse
on thedata-unset-on-remove
attribute.
Binding to inputs
Since data
is a writable store, you can also bind the data properties to your inputs instead of using the form
action.
<script>
import { createForm } from 'felte'
const { handleSubmit, data, errors } = createForm({
initialValues: {
email: '',
password: '',
},
validate: (values) => {
/* validate and return errors found */
},
onSubmit: async (values) => {
/* call to an api */
},
})
$: console.log($data)
$: console.log($errors)
</script>
<form on:submit="{handleSubmit}">
<input type=text bind:value="{$data.email}">
<input type=password bind:value="{$data.password}">
<input type=submit value="Sign in">
</form>
With this approach you should see a similar behaviour to the previous way of using this. Note that the name
attribute is optional here, but the initialValues
property for createForm
is required. It is a bit more verbose, so it's recommended to use the previous way of handling forms.