solar-forms
v0.0.5
Published
Form library for SolidJS inspired by Angular's reactive forms
Downloads
2
Maintainers
Readme
import { createFormGroup, formGroup, Validators as V } from 'solar-forms';
const Registration = ({ onSubmit }: Props) => {
const fg = createFormGroup({
email: ['', { validators: [V.required] }],
name: '',
password: ['', { validators: [V.required] }],
acceptTerms: [false, { validators: [V.is(true)] }]
});
const [form, setForm] = fg.value;
const validAll = fg.validAll;
return (
<>
<form use:formGroup={fg}>
<label htmlFor="email">Email</label>
<input id="email" type="email" formControlName="email" />
<label htmlFor="name">Name (optional)</label>
<input id="name" type="text" formControlName="name" />
<label htmlFor="password">Password</label>
<input id="password" type="password" formControlName="password" />
<label htmlFor="acceptTerms">Accept terms</label>
<input id="acceptTerms" type="checkbox" formControlName="acceptTerms" />
<button type="submit" disabled={!validAll()} onClick={onSubmit}>
Submit
</button>
</form>
</>
);
};
About
Solar Forms allows you to create reactive and type-safe state for your form controls. It lets you take over form controls and access key information like control's current value, whether it's disabled, valid, etc. as SolidJS signals. Form controls can also be pre-configured with validator functions, ensuring your form won't be marked as valid unless all data is correct.
⚠️ This library is still in very early stages of development. It is not encouraged to use it in production applications. Although we encourage you to try it out and give some feedback!
Features
- Create form group as a set of related controls that you can manage.
- Use form control properties like
value
,disabled
,dirty
andtouched
. - Use form group properties like
disabledAll
,dirtyAll
andtouchedAll
. - Pre-configure form controls with built-in or custom validator functions to ensure you have all information you need before the form is submitted.
- Check if a single form control or an entire form group is valid with
valid
andvalidAll
properties. - Access validation errors with
errors
form control property. - Access all form group and form control properties as SolidJS signals.
- Create nested form control structures.
Installation
# using npm
npm install solar-forms
# using yarn
yarn add solar-forms
If you encounter any issues when setting up Solar Forms, try consulting our FAQ section!
Documentation
- Online examples
- Getting started
- Creating form group
- Binding our form group to the
form
element usingformGroup
directive - Accessing form control values at any time
- Managing
disabled
form control property - Managing
disabledAll
form group property - Managing
dirty
form control property - Managing
dirtyAll
form group property - Managing
touched
form control property - Managing
touchedAll
form group property - Validating form controls
- Binding form controls to different types of
<input>
elements - Binding form controls to
<select>
element - Binding form controls to
<textarea>
element - Form control errors
- Nested form groups
- FAQ
- Roadmap
- Support
- Contribution guidelines
- Inspirations
Online examples
Getting started
Creating form group
One of main elements of Solar Forms is createFormGroup
function, that lets you
instantiate form group that you can use to manage your form:
const fg = createFormGroup({
// ➡️ Registering form control under `firstName` name
firstName: 'John',
});
createFormGroup
has a single argument and that is an object representing structure
of your form. In the case above we define the firstName
form control and we set
an initial value to it.
Later you'll see just how much we can expand your form group and which properties of it we'll be able to manage!
Binding our form group to the form
element using formGroup
directive
Second of the main concepts of Solar Forms is the formGroup
SolidJS directive,
that allows you to bind your form group to your form
HTML element:
// Component definition
const fg = createFormGroup({
// 1️⃣ Form control named `firstName`
firstName: 'John',
});
// 2️⃣ Binding form group to the form using `use:formGroup`
return (
<form use:formGroup={fg}>
<label htmlFor="firstName">First name</label>
{/* 3️⃣ Binding form control to the input element using `formControlName` property */}
<input id="firstName" type="text" formControlName="firstName" />
</form>
);
Under the hood, the formGroup
directive sets up the entire set of SolidJS signals
and initiates form control properties.
Still, our form group needs to know which form controls belong to which form inputs.
This is why we bind those together by assigning formControlName
property to form inputs
with the same name, as defined for the form control.
Accessing form control values at any time
After you set up your form group the way shown above, you can access your form control
values using the value
signal:
// Component definition
const fg = createFormGroup({
firstName: 'John',
});
// ➡️ Accessing form group `value` signal
const [form, setForm] = fg.value;
return (
<form use:formGroup={fg}>
<label htmlFor="firstName">First name</label>
<input id="firstName" type="text" formControlName="firstName" />
</form>
);
The value
signal contains tuple of reactive value for our form group and a setter
function. You'll see this pattern along the way of learning about the rest of form control properties:
// Component definition
const fg = createFormGroup({
firstName: 'John',
});
const [form, setForm] = fg.value;
// 1️⃣ Accessing form groups's reactive value
const logForm = () => console.log(JSON.stringify(form()));
return (
<>
<form use:formGroup={fg}>
<label htmlFor="firstName">First name</label>
<input id="firstName" type="text" formControlName="firstName" />
</form>
{/* 2️⃣ Clicking this logs "{"firstName":"John"}" */}
<button onClick={logForm}>
Log form value
</button>
</>
);
A setter function allows us to set the form controls' values at will:
// Component definition
const fg = createFormGroup({
firstName: 'John',
});
const [form, setForm] = fg.value;
// ➡️ Changing form control value
const changeName = () => setForm({ ...form(), firstName: 'Tom' });
return (
<>
<form use:formGroup={fg}>
<label htmlFor="firstName">First name</label>
<input id="firstName" type="text" formControlName="firstName" />
</form>
<button onClick={changeName}>
Change first name
</button>
</>
);
As an argument, setter functions for form controls accept new form group value object, or a setter callback:
const fg = createFormGroup({
firstName: 'John',
});
const [form, setForm] = fg.value;
// 1️⃣ Update form value by passing value object
const changeName1 = () => setForm({ ...form(), firstName: 'Tom' });
// 2️⃣ Update form value by passing setter callback
const changeName2 = () => setForm(f => ({ ...f, firstName: 'Tom' }));
Managing disabled
form control property
When defining your form group, you can mark individual form controls as disabled by default. You can do that by passing a tuple of default value and form control config, instead of a single value:
const fg = createFormGroup({
// With this, the `firstName` form control is marked as disabled by default
firstName: ['John', { disabled: true }],
});
As you may have already realised, if you do not implicitly mark control as disabled, only by passing only a default value, the form control is enabled by default:
const fg = createFormGroup({
// The `firstName` form control is set as enabled by default
firstName: 'John',
});
You can access the disabled
state of the form controls by using a specific form group signal:
const fg = createFormGroup({
firstName: ['John', { disabled: true }],
});
// Accessing the reactive enabled/disabled state of form controls
// and a setter function under `disabled` property
const [disabled, setDisabled] = fg.disabled;
Under the hood, the disabled
state is bound to your form elements, so that any change to form
control state is reflected in the UI:
// Component definition
const fg = createFormGroup({
firstName: ['John', { disabled: true }],
});
const [disabled, setDisabled] = fg.disabled;
return (
<>
<form use:formGroup={fg}>
<label htmlFor="firstName">First name</label>
{/* ➡️ This form element is now disabled */}
<input id="firstName" type="text" formControlName="firstName" />
</form>
</>
);
Also, any update to the disabled
form control state is reflected in the UI as well:
// Component definition
const fg = createFormGroup({
firstName: ['John', { disabled: true }],
});
const [disabled, setDisabled] = fg.disabled;
const enableFirstName = () => setDisabled(d => ({ ...d, firstName: false }));
return (
<>
<form use:formGroup={fg}>
<label htmlFor="firstName">First name</label>
{/* 1️⃣ This form element is disabled on initialization */}
<input id="firstName" type="text" formControlName="firstName" />
</form>
{/* 2️⃣ After clicking this button, the `firstName` form input becomes enabled */}
<button onClick={enableFirstName}>
Enable first name
</button>
</>
);
Managing disabledAll
form group property
If you value information on whether your entire form is disabled (meaning, every form input
element is disabled), you can use the disabledAll
form group property, which also is
a SolidJS signal:
const fg = createFormGroup({
firstName: ['John', { disabled: true }],
lastName: 'Smith',
});
// Accessing information on whether the entire form is disabled or enabled
const [disabledAll, setDisabledAll] = fg.disabledAll;
This aggregates all disabled
form control properties under one boolean
value:
const fg = createFormGroup({
firstName: ['John', { disabled: true }],
lastName: 'Smith',
});
const [disabledAll, setDisabledAll] = fg.disabledAll;
// Logs `false`, because the `lastName` form control is enabled
console.log(disabledAll());
You can also set your entire form as enabled or disabled using the setDisabledAll
setter function:
// Component definition
const fg = createFormGroup({
firstName: ['John', { disabled: true }],
lastName: 'Smith',
});
const [disabledAll, setDisabledAll] = fg.disabledAll;
// 1️⃣ Set all form elements as disabled with this single call
const disableForm = () => setDisabledAll(true);
return (
<>
<form use:formGroup={fg}>
<label htmlFor="firstName">First name</label>
<input id="firstName" type="text" formControlName="firstName" />
<label htmlFor="lastName">Last name</label>
<input id="lastName" type="text" formControlName="lastName" />
</form>
{/* 2️⃣ After clicking this button, all form elements become disabled */}
<button onClick={disableForm}>
Disable form
</button>
</>
);
⚠️ As a contrary to
value
ordisabled
form control properties,disabledAll
is a form group property, as it represents state of the entire form, not its individual elements.
As an argument, the setter function for disabledAll
property accepts boolean or a setter callback:
const fg = createFormGroup({
firstName: 'John',
});
const [disabledAll, setDisabledAll] = fg.disabledAll;
// 1️⃣ Update `disabledAll` state by passing boolean
const disableForm1 = () => setDisabledAll(true);
// 2️⃣ Update `disabledAll` state by passing setter callback
const disableForm2 = () => setDisabledAll(d => !d);
Managing dirty
form control property
After you define your form group, it tracks whether the user has already
changed the form input value from UI. That's what the dirty
form control property is for:
const fg = createFormGroup({
firstName: 'John',
});
// Accessing the reactive `dirty` state of form controls
// and a setter function under the `dirty` signal
const [dirty, setDirty] = fg.dirty;
Under the hood, every form control is marked as "pristine" (as an opposite to "dirty") on initialization ,
so all dirty
values for form controls are false
by default:
const fg = createFormGroup({
firstName: 'John',
});
const [dirty, setDirty] = fg.dirty;
// 1️⃣ Logs `false`, as user did not update the form input yet
console.log(dirty().firstName);
Whenever the user changes the form input value from UI, the dirty
property for the form control
is set to true
:
// Component definition
const fg = createFormGroup({
firstName: '',
});
const [dirty, setDirty] = fg.dirty;
const logDirtyForFirstName = () => console.log(dirty().firstName);
return (
<>
<form use:formGroup={fg}>
<label htmlFor="firstName">First name</label>
{/* 1️⃣ Imagine user updating their first name from UI */}
<input id="firstName" type="text" formControlName="firstName" />
</form>
{/* 2️⃣ After the update, clicking this button logs `true` */}
<button onClick={logDirtyForFirstName}>
Log
</button>
</>
);
You can also mark your form controls as "dirty" or "pristine" (as an opposite to "dirty") programmatically as well:
// Component definition
const fg = createFormGroup({
firstName: '',
});
const [dirty, setDirty] = fg.dirty;
const markFirstNameAsPristine = () => setDirty(d => ({ ...d, firstName: false }));
const logDirtyForFirstName = () => console.log(dirty().firstName);
return (
<>
<form use:formGroup={fg}>
<label htmlFor="firstName">First name</label>
{/* 1️⃣ Imagine user updates their first name from UI */}
<input id="firstName" type="text" formControlName="firstName" />
</form>
{/* 2️⃣ After the update, clicking this button marks the `firstName` */}
{/* form control as "pristine" again */}
<button onClick={markFirstNameAsPristine}>
Change dirty
</button>
{/* 3️⃣ Clicking this button logs `false`, as we set the property to it ourselves */}
<button onClick={logDirtyForFirstName}>
Log
</button>
</>
);
Managing dirtyAll
form group property
Similarly to disabledAll
, there is a dirtyAll
form group property aggregating all form controls'
dirty
properties under one boolean
value. dirtyAll
holds information whether the entire form
(meaning every form control in the form) is "dirty":
const fg = createFormGroup({
firstName: 'John',
lastName: 'Smith',
});
// 1️⃣ Accessing information on whether the entire form is dirty
const [dirtyAll, setDirtyAll] = fg.dirtyAll;
// 2️⃣ Logs `false`, because all form controls weren't updated from UI
console.log(dirtyAll());
You can also set your entire form as "dirty" or "pristine" (as an opposite to "dirty")
using the setDirtyAll
setter function:
// Component definition
const fg = createFormGroup({
firstName: 'Johm',
lastName: 'Smith',
});
const [dirtyAll, setDirtyAll] = fg.dirtyAll;
// 1️⃣ Sets all form elements as "dirty" with a single call
const changeAllToDirty = () => setDirtyAll(true);
const logDirtyAll = () => console.log(dirtyAll());
return (
<>
<form use:formGroup={fg}>
<label htmlFor="firstName">First name</label>
<input id="firstName" type="text" formControlName="firstName" />
<label htmlFor="lastName">Last name</label>
<input id="lastName" type="text" formControlName="lastName" />
</form>
{/* 2️⃣ After clicking this button, all form elements are marked as "dirty" */}
<button onClick={changeAllToDirty}>
Change all to dirty
</button>
{/* 3️⃣ After the update, clicking this button logs `true` */}
<button onClick={logDirtyAll}>
Log dirtyAll
</button>
</>
);
As an argument, the setter function for dirtyAll
property accepts boolean
or a setter callback:
const fg = createFormGroup({
firstName: 'John',
});
const [dirtyAll, setDirtyAll] = fg.dirtyAll;
// 1️⃣ Update `dirtyAll` state by passing boolean
const markAllAsDirty1 = () => setDirtyAll(true);
// 2️⃣ Update `dirtyAll` state by passing setter callback
const markAllAsDirty2 = () => setDirtyAll(d => !d);
Managing touched
form control property
After you define your form group, it tracks whether the user has already
triggered a blur event
on the form input value. With this you can track whether the user has already been focused on
a specific form input element, or not. That's what the touched
form control property is for:
const fg = createFormGroup({
firstName: 'John',
});
// 1️⃣ Accessing the reactive "touched" state of form controls
// and a setter function under the `touched` signal
const [touched, setTouched] = fg.touched;
Under the hood, every form control is marked as "untouched" on initialization, so
all touched
values for form controls are false
by default:
const fg = createFormGroup({
firstName: 'John',
});
const [touched, setTouched] = fg.touched;
// 1️⃣ Logs `false`, as there hasn't been a "blur" event triggered yet on the `firstName` form input
console.log(touched().firstName);
Whenever user switches from the specific form control to another (triggering the "blur" event),
the touched
property for the form control is set to true
:
// Component definition
const fg = createFormGroup({
firstName: 'John',
lastName: 'John',
});
const [touched, setTouched] = fg.touched;
const logTouchedForFirstName = () => console.log(touched().firstName);
return (
<>
<form use:formGroup={fg}>
<label htmlFor="firstName">First name</label>
{/* 1️⃣ Imagine user switching from this form input... */}
<input id="firstName" type="text" formControlName="firstName" />
<label htmlFor="lastName">Last name</label>
{/* 2️⃣ ... to this one */}
<input id="lastName" type="text" formControlName="lastName" />
</form>
{/* 3️⃣ After the "blur" event has been triggered, clicking this button logs `true` */}
<button onClick={logTouchedForFirstName}>
Log
</button>
</>
);
You can also mark your form controls as "touched" or "untouched" programmatically:
// Component definition
const fg = createFormGroup({
firstName: 'John',
lastName: 'John',
});
const [touched, setTouched] = fg.touched;
const changeTouchedForFirstName = () => setTouched(d => ({ ...d, firstName: false }));
const logTouchedForFirstName = () => console.log(touched().firstName);
return (
<>
<form use:formGroup={fg}>
<label htmlFor="firstName">First name</label>
{/* 1️⃣ Imagine user switching from this form input... */}
<input id="firstName" type="text" formControlName="firstName" />
<label htmlFor="lastName">Last name</label>
{/* 2️⃣ ... to this one */}
<input id="lastName" type="text" formControlName="lastName" />
</form>
{/* 3️⃣ After the "blur" event has been triggered, clicking this */}
{/* switches `touched` to `false` again */}
<button onClick={changeTouchedForFirstName}>
Change touched
</button>
{/* 4️⃣ Clicking this button logs `false`, as we set the property to it ourselves */}
<button onClick={logTouchedForFirstName}>
Log
</button>
</>
);
Managing touchedAll
form group property
Similarly to disabledAll
and dirtyAll
, there is a touchedAll
form group property aggregating
all form controls' touched
properties under one boolean
value. touchedAll
holds information
whether the entire form (meaning every form control in the form) is "touched":
const fg = createFormGroup({
firstName: 'Johm',
lastName: 'Smith',
});
// 1️⃣ Accessing information on whether the entire form is "touched"
const [touchedAll, setTouchedAll] = fg.touchedAll;
// 2️⃣ Logs `false`, because "blur" event wasn't triggered for all form inputs
console.log(touchedAll());
You can also mark your entire form as "touched" or "untouched" using the setTouchedAll
setter
function:
// Component definition
const fg = createFormGroup({
firstName: 'Johm',
lastName: 'Smith',
});
const [touchedAll, setTouchedAll] = fg.touchedAll;
// 1️⃣ Sets all form elements as "touched" with a single call
const changeAllToTouched = () => setTouchedAll(true);
const logTouchedAll = () => console.log(touchedAll());
return (
<>
<form use:formGroup={fg}>
<label htmlFor="firstName">First name</label>
<input id="firstName" type="text" formControlName="firstName" />
<label htmlFor="lastName">Last name</label>
<input id="lastName" type="text" formControlName="lastName" />
</form>
{/* 2️⃣ After clicking this button, all form elements are marked as "touched" */}
<button onClick={changeAllToTouched}>
Change all to touched
</button>
{/* 3️⃣ After the update, clicking this button logs `true` */}
<button onClick={logTouchedAll}>
Log touchedAll
</button>
</>
);
As an argument, the setter function for touchedAll
property accepts boolean
or a setter callback:
const fg = createFormGroup({
firstName: 'John',
});
const [touchedAll, setTouchedAll] = fg.touchedAll;
// 1️⃣ Update `touchedAll` state by passing boolean
const markAllAsDirty1 = () => setTouchedAll(true);
// 2️⃣ Update `touchedAll` state by passing setter callback
const markAllAsDirty2 = () => setTouchedAll(d => !d);
Validating form controls
Setting up form control validators
As you've probably worked with forms before (as a developer or a user), you realised that not every user input should be valid. We do not accept emails with wrong format, empty passwords, terms checkbox not ticked or dates of birth that make you over 200 years old.
With Solar Forms you can take control over your user's input and define how your form controls should be validated. You can do that by using built-in or custom validator functions:
import { FormControl, ValidatorFn } from 'solar-forms';
const required: ValidatorFn = (formControl: FormControl) =>
formControl.value ? null : { required: true };
This is a very simple example of how to define custom required
validator function for your
form controls. Here we are checking whether the value is falsy (but remember that 0
is also falsy!)
and if it is, we return the record with validation error. If the value is valid, we return
null
, meaning there are no validation errors.
FormControl
and ValidatorFn
types seem important here, so let's take a look at their definitions:
export interface FormControl {
value: string | number | boolean | Date | null;
disabled: boolean;
touched: boolean;
dirty: boolean;
}
export interface ValidatorFn {
(control: FormControl): ValidationErrors | null;
}
export type ValidationErrors = {
[key: string]: unknown;
};
As you can see, when defining a validator function, you can use various data about your form control that may be important to you: its current value and whether it is disabled, touched or dirty.
With validator function defined, you can pre-configure your form controls with it when creating your form group:
const required = (formControl) =>
formControl.value ? null : { required: true };
const fg = createFormGroup({
// Adding validator(s) to your form control
password: ['', { validators: [required] }],
});
As you see, you can add validator(s) to your form control by passing a list of validator functions
to the config object under the validators
key.
Accessing the valid
form control property
After you define your form group, it tracks whether the form control values are valid or invalid.
That's what the valid
form control property is for:
const required = (formControl) =>
formControl.value ? null : { required: true };
const fg = createFormGroup({
password: ['', { validators: [required] }],
});
// Accessing the reactive "valid" state of form controls
const valid = fg.valid;
Under the hood, every form control is marked as valid or invalid on initialization, based on whether the form control value passes the all validator functions.
const required = (formControl) =>
formControl.value ? null : { required: true };
const fg = createFormGroup({
password: ['', { validators: [required] }],
});
const valid = fg.valid;
// Logs `false` as the value is required and it's an empty string on initialization
console.log(valid().password);
After changing form control values, (either with UI or using value setter functions), the validation
functions are run again against form control values and the valid
state is updated accordingly.
// Component definition
const required = (formControl) =>
formControl.value ? null : { required: true };
const fg = createFormGroup({
password: ['', { validators: [required] }],
});
const valid = fg.valid;
const logValidForPassword = () => console.log(valid().password);
return (
<>
<form use:formGroup={fg}>
<label htmlFor="password">Password</label>
{/* 1️⃣ Imagine user typing their password */}
<input id="password" type="password" formControlName="password" />
</form>
{/* 2️⃣ After user types their password, clicking this button logs `true` */}
<button onClick={logValidForPassword}>
Log
</button>
</>
);
Accessing the validAll
form group property
valid
form control property also has the corresponding form group property - validAll
.
It represents aggregated data on all form controls being valid or not. That means you
can use validAll
accessor to check whether the whole form is valid or not:
const required = (formControl) =>
formControl.value ? null : { required: true };
const fg = createFormGroup({
name: '',
password: ['', { validators: [required] }],
});
// 1️⃣ Accessing the reactive "validAll" state of form controls
const validAll = fg.validAll;
// 2️⃣ Logs `false` as one of form control values is required and it's
// an empty string on initialization
console.log(validAll());
When all form controls are valid (all form controls' validator functions pass), validAll
returns
true
as well:
// Component definition
const required = (formControl) =>
formControl.value ? null : { required: true };
const fg = createFormGroup({
name: '',
password: ['', { validators: [required] }],
});
const validAll = fg.validAll;
const logValidAll = () => console.log(validAll());
return (
<>
<form use:formGroup={fg}>
<label htmlFor="name">Name (optional)</label>
<input id="name" type="text" formControlName="name" />
<label htmlFor="password">Password</label>
{/* 1️⃣ Imagine user typing their password */}
<input id="password" type="password" formControlName="password" />
</form>
{/* 2️⃣ After user types their password, clicking this button logs `true` */}
<button onClick={logValidAll}>
Log
</button>
</>
);
Accessing validation errors
At the same time, form group tracks all validation errors at a given time with the errors
form control property:
const required = (formControl) =>
formControl.value ? null : { required: true };
const fg = createFormGroup({
password: ['', { validators: [required] }],
});
// Accessing the reactive "errors" state of form controls
const errors = fg.errors;
On initialization and on every form value update validator functions are run against form control
values and the errors
form control property is updated accordingly:
const required = (formControl) =>
formControl.value ? null : { required: true };
const fg = createFormGroup({
name: '',
password: ['', { validators: [required] }],
});
const errors = fg.errors;
// 1️⃣ Logs "{"required":"This is required!"}" as the value is required
// and it's an empty string on initialization
console.log(JSON.stringify(errors().password));
// 2️⃣ Logs "null" as the value has no validators, so it's valid by default
console.log(JSON.stringify(errors().name));
error
form control property holds an object with all validation errors aggregated under one
object.
⚠️ Because of the fact, that
error
form control property holds an object with all validation errors aggregated under one record, it is advised to name keys for your validation errors object in a unique way when creating your custom validator functions.Doing otherwise may result in overwriting your keys inside the
ValidationErrors
record.
Built-in validators
As you've learned, form group accepts list of validators that match the
ValidatorFn
interface. This means you can write custom validators
for your own use cases.
As an alternative, you can use built-in validators provided by Solar Forms. Here's a brief introduction:
required
validator
Validator that requires the control have a non-empty value
(null
and''
are treated as empty values).
import { createFormGroup, Validators as V } from 'solar-forms';
const fg = createFormGroup({
firstName: ['', { validators: [V.required] }],
lastName: ['Smith', { validators: [V.required] }],
});
const errors = fg.errors;
// ➡️ Logs `{ required: true }`
console.log(errors().firstName);
// ➡️ Logs `null`
console.log(errors().lastName);
min
validator
Validator that requires the control's value to be greater than or equal to the provided number.
import { createFormGroup, Validators as V } from 'solar-forms';
const fg = createFormGroup({
minorAge: [16, { validators: [V.min(21)] }],
adultAge: [30, { validators: [V.min(21)] }],
});
const errors = fg.errors;
// ➡️ Logs `{ min: true }`
console.log(errors().minorAge);
// ➡️ Logs `null`
console.log(errors().adultAge);
max
validator
Validator that requires the control's value to be less than or equal to the provided number.
import { createFormGroup, Validators as V } from 'solar-forms';
const fg = createFormGroup({
invalidAmount: [30, { validators: [V.max(10)] }],
validAmount: [5, { validators: [V.max(10)] }],
});
const errors = fg.errors;
// ➡️ Logs `{ max: true }`
console.log(errors().invalidAmount);
// ➡️ Logs `null`
console.log(errors().validAmount);
minLength
validator
Validator that requires the length of the control's string-based value's length to be greater than or equal to the provided minimum length.
import { createFormGroup, Validators as V } from 'solar-forms';
const fg = createFormGroup({
invalidPassword: ['grfr', { validators: [V.minLength(8)] }],
validPassword: ['wdnaw#@!udnwahe3w@#$@!', { validators: [V.minLength(8)] }],
});
const errors = fg.errors;
// ➡️ Logs `{ minLength: true }`
console.log(errors().invalidPassword);
// ➡️ Logs `null`
console.log(errors().validPassword);
maxLength
validator
Validator that requires the length of the control's string-based value's length to be lower than or equal to the provided maximum length.
import { createFormGroup, Validators as V } from 'solar-forms';
const fg = createFormGroup({
invalidInput: ['qwertyuiopasdfg', { validators: [V.maxLength(10)] }],
validInput: ['qwerty', { validators: [V.maxLength(10)] }],
});
const errors = fg.errors;
// ➡️ Logs `{ maxLength: true }`
console.log(errors().invalidInput);
// ➡️ Logs `null`
console.log(errors().validInput);
is
validator
Validator that requires the value of the form control to be equal to the provided value.
import { createFormGroup, Validators as V } from 'solar-forms';
const fg = createFormGroup({
invalidCaptcha: ['q1q1q1', { validators: [V.is('q1w2e3')] }],
validCaptcha: ['q1w2e3', { validators: [V.is('q1w2e3')] }],
nonAcceptedTerms: [false, { validators: [V.is(true)] }],
acceptedTerms: [true, { validators: [V.is(true)] }],
});
const errors = fg.errors;
// ➡️ Logs `{ is: true }`
console.log(errors().invalidCaptcha);
// ➡️ Logs `null`
console.log(errors().validCaptcha);
// ➡️ Logs `{ is: true }`
console.log(errors().nonAcceptedTerms);
// ➡️ Logs `null`
console.log(errors().acceptedTerms);
isAnyOf
validator
Validator that requires the value of the form control to be equal to one of provided values.
import { createFormGroup, Validators as V } from 'solar-forms';
const fg = createFormGroup({
invalidCountry: ['Poland', { validators: [V.isAnyOf(['Spain', 'France', 'Germany'])] }],
validCountry: ['Spain', { validators: [V.isAnyOf(['Spain', 'France', 'Germany'])] }],
});
const errors = fg.errors;
// ➡️ Logs `{ isAnyOf: true }`
console.log(errors().invalidCountry);
// ➡️ Logs `null`
console.log(errors().validCountry);
email
validator
Validator that requires the value of the form control to have a valid email format.
import { createFormGroup, Validators as V } from 'solar-forms';
const fg = createFormGroup({
invalidEmail: ['test', { validators: [V.email] }],
validEmail: ['[email protected]', { validators: [V.email] }],
});
const errors = fg.errors;
// ➡️ Logs `{ email: true }`
console.log(errors().invalidEmail);
// ➡️ Logs `null`
console.log(errors().validEmail);
pattern
validator
Validator that requires the value of the form control to have a format matching provided regular expression.
import { createFormGroup, Validators as V } from 'solar-forms';
const regexp = /^([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/gm;
const fg = createFormGroup({
invalidHour: ['test', { validators: [V.pattern(regexp)] }],
validHour: ['12:00:00', { validators: [V.pattern(regexp)] }],
});
const errors = fg.errors;
// ➡️ Logs `{ pattern: { requiredPattern: ..., actualValue: 'test' } }`
console.log(errors().invalidHour);
// ➡️ Logs `null`
console.log(errors().validHour);
Binding form controls to different types of input elements
There are a lot of possible types for an HTML input element and Solar Forms allows you to work with HTML input elements of every major type:
- text
- password
- tel
- url
- number
- range
- date
- datetime-local
- time
- checkbox
- radio
Type of text
This is the most basic input element - a string-based input element type. Accordingly, you can
define your form control's default value as string
or null
:
// Component definition
const fg = createFormGroup({
// 1️⃣ Default value is set here as string
name: '',
});
return (
<form use:formGroup={fg}>
<label htmlFor="name">Name</label>
{/* 2️⃣ Here we define the `type` attribute of the input element */}
<input id="name" type="text" formControlName="name" />
</form>
);
Type of email
Another string-based input element type. For the corresponding form control you can
define the default value as string
or null
:
// Component definition
const fg = createFormGroup({
// 1️⃣ Default value is set here as string
email: '',
});
return (
<form use:formGroup={fg}>
<label htmlFor="email">Email</label>
{/* 2️⃣ Here we define the `type` attribute of the input element */}
<input id="email" type="email" formControlName="email" />
</form>
);
Type of password
Another string-based input element type. For the corresponding form control you can
define the default value as string
or null
:
// Component definition
const fg = createFormGroup({
// 1️⃣ Default value is set here as string
password: '',
});
return (
<form use:formGroup={fg}>
<label htmlFor="password">Password</label>
{/* 2️⃣ Here we define the `type` attribute of the input element */}
<input id="password" type="password" formControlName="password" />
</form>
);
Type of tel
Another string-based input element type. For the corresponding form control you can
define the default value as string
or null
:
// Component definition
const fg = createFormGroup({
// 1️⃣ Default value is set here as string
tel: '',
});
return (
<form use:formGroup={fg}>
<label htmlFor="tel">Tel</label>
{/* 2️⃣ Here we define the `type` attribute of the input element */}
<input id="tel" type="tel" formControlName="tel" />
</form>
);
Type of url
Another string-based input element type. For the corresponding form control you can
define the default value as string
or null
:
// Component definition
const fg = createFormGroup({
// 1️⃣ Default value is set here as string
url: '',
});
return (
<form use:formGroup={fg}>
<label htmlFor="url">URL</label>
{/* 2️⃣ Here we define the `type` attribute of the input element */}
<input id="url" type="url" formControlName="url" />
</form>
);
Type of number
The most basic number-based type of input element. In this case, for the corresponding form control
you can define the default value as number
or null
:
// Component definition
const fg = createFormGroup({
// 1️⃣ Default value is set here as number
age: 0,
});
return (
<form use:formGroup={fg}>
<label htmlFor="age">URL</label>
{/* 2️⃣ Here we define the `type` attribute of the input element */}
<input id="age" type="number" formControlName="age" />
</form>
);
Type of range
Another number-based type of input element. For the corresponding form control
you can define the default value as number
or null
:
// Component definition
const fg = createFormGroup({
// 1️⃣ Default value is set here as number
skillLevel: 0,
});
return (
<form use:formGroup={fg}>
<label htmlFor="skillLevel">Skill level</label>
{/* 2️⃣ Here we define the `type` attribute of the input element */}
<input id="skillLevel" type="range" formControlName="skillLevel" />
</form>
);
Type of date
A date-based type of input element. In this case, for the corresponding form control
you can define the default value as Date
object, string
, number
or null
:
⚠️ If you wish to bind string values to the
date
form control, remember about using proper date text formats.
// Component definition
const fg = createFormGroup({
// 1️⃣ Default values are set here
dateDate: new Date(),
dateString: new Date().toISOString().split('T')[0],
dateNumber: new Date().getTime(),
});
return (
<form use:formGroup={fg}>
<label htmlFor="dateDate">Date [Date]</label>
{/* 2️⃣ Here we define the `type` attribute of the input element */}
<input id="dateDate" type="date" formControlName="dateDate" />
<label htmlFor="dateString">Date [string]</label>
<input id="dateString" type="date" formControlName="dateString" />
<label htmlFor="dateNumber">Date [number]</label>
<input id="dateNumber" type="date" formControlName="dateNumber" />
</form>
);
Type of datetime-local
A date-based type of input element. In this case, for the corresponding form control
you can define the default value as string
, number
or null
:
⚠️ If you wish to bind
string
values to thedatetime-local
form control, remember about using proper date text formats.
// Component definition
const fg = createFormGroup({
// 1️⃣ Default values are set here
dateString: new Date().toISOString().split('.')[0],
dateNumber: new Date().getTime(),
});
return (
<form use:formGroup={fg}>
<label htmlFor="dateString">Date [string]</label>
{/* 2️⃣ Here we define the `type` attribute of the input element */}
<input id="dateString" type="datetime-local" formControlName="dateString" />
<label htmlFor="dateNumber">Date [number]</label>
<input id="dateNumber" type="datetime-local" formControlName="dateNumber" />
</form>
);
Type of time
A time-based type of input element. In this case, for the corresponding form control
you can define the default value as Date
, string
, number
or null
:
⚠️ If you wish to bind
string
values to thetime
form control, remember about using proper time text formats.
// Component definition
const fg = createFormGroup({
// 1️⃣ Default values are set here
timeDate: new Date(),
timeString: '00:00:00',
timeNumber: new Date().getTime(),
});
return (
<form use:formGroup={fg}>
<label htmlFor="timeDate">Date [Date]</label>
{/* 2️⃣ Here we define the `type` attribute of the input element */}
<input id="timeDate" type="time" formControlName="timeDate" />
<label htmlFor="timeString">Date [string]</label>
<input id="timeString" type="time" formControlName="timeString" />
<label htmlFor="timeNumber">Date [number]</label>
<input id="timeNumber" type="time" formControlName="timeNumber" />
</form>
);
Type of checkbox
A boolean-based type of input element. For the corresponding form control
you can define the default value as boolean
or null
:
// Component definition
const fg = createFormGroup({
// 1️⃣ Default values are set here
acceptTerms: false,
});
return (
<form use:formGroup={fg}>
<label htmlFor="acceptTerms">Date [Date]</label>
{/* 2️⃣ Here we define the `type` attribute of the input element */}
<input id="acceptTerms" type="checkbox" formControlName="acceptTerms" />
</form>
);
Type of radio
A string-based type of input element, where you can choose one of pre-defined set of options.
For the corresponding form control you can define the default value as string
or null
:
// Component definition
const fg = createFormGroup({
// 1️⃣ Default value is set here
// 2️⃣ Imagine you can have 3 options here: "engineering", "testing" and "product"
team: null,
});
return (
<form use:formGroup={fg}>
{/* 3️⃣ Choosing one of 3 options sets form control value to */}
{/* a value assigned to specific radio input */}
<input type="radio" id="radioEngineering" name="team" value="engineering" formControlName="team" />
<label htmlFor="radioEngineering">Engineering</label>
<input type="radio" id="radioProduct" name="team" value="product" formControlName="team" />
<label htmlFor="radioProduct">Product</label>
<input type="radio" id="radioTesting" name="team" value="testing" formControlName="team" />
<label htmlFor="radioTesting">Testing</label>
</form>
);
Binding form controls to <select>
element
You can bind string
values to the <select>
element. You can change value
of the element by choosing one of the predefined options:
type CountryOption = '' | 'Poland' | 'Spain' | 'Germany';
// Component definition
const fg = createFormGroup({
// 1️⃣ Default value is set here
country: '' as CountryOption,
});
return (
<form use:formGroup={fg}>
{/* 2️⃣ Choosing one of available options sets form control value to */}
{/* a value assigned to specific <option> element */}
<label htmlFor="country-select">
Country
<select name="country" id="country-select" formControlName="country">
<option value="">--Please choose an option--</option>
<option value="Poland">Poland</option>
<option value="Spain">Spain</option>
<option value="Germany">Germany</option>
</select>
</label>
</form>
);
Binding form controls to <textarea>
element
textarea
element is a string-based form control. You can
define your form control's default value as string
or null
:
// Component definition
const fg = createFormGroup({
// 1️⃣ Default value is set here
bio: '',
});
return (
<form use:formGroup={fg}>
<label htmlFor="bio">Bio</label>
{/* 2️⃣ Here we bind form control to form group */}
<textarea name="bio" id="bio" formControlName="bio" />
</form>
);
Form control errors
To ensure that the form group defined by us matches the form structure in our template, some additional runtime checks were implemented.
Form control name does not match any key from form group
In case we'd made a mistake while connecting a form group key with the form input element using
formControlName
HTML attribute, we would get a runtime error informing us of the mistake:
// Component definition
const fg = createFormGroup({
firstName: 'John',
});
return (
<>
<form use:formGroup={fg}>
<label for="firstName">First name</label>
<input id="firstName" type="text" formControlName="company" />
</form>
</>
);
This one results in throwing a custom FormControlInvalidKeyError
error with a message:
"company" form control name does not match any key from the form group.
Form control type does not match the type of an input element
In case we'd made a mistake when initializing the value of a form control in our form group, that wasn't supposed to be used with a given HTML element, we would get a runtime error informing us of the mistake:
// Component definition
const fg = createFormGroup({
// 1️⃣ Here we initialize `firstName` form control value as `string`
firstName: 'John',
});
return (
<>
<form use:formGroup={fg}>
<label for="firstName">First name</label>
{/* 2️⃣ But here we use input element with type "number" - types mismatch */}
<input id="firstName" type="number" formControlName="firstName" />
</form>
</>
);
This one results in throwing a custom FormControlInvalidTypeError
error with a message:
Value of the "firstName" form control is expected to be of type [number] but the type was [string].
Nested form groups
You can define nested form groups, by placing a nested record in your form group schema:
const fg = createFormGroup({
firstName: 'John',
// Here we create a nested form group
address: {
city: '',
postalNumber: null
}
});
This makes composing complex form models easier to maintain and logically group together.
When building complex forms, managing the different areas of information is easier in smaller sections. Using nested form groups lets you break large forms groups into smaller, more manageable ones, e.g. for styling or domain purposes.
To represent nested form groups in your template, you must wrap the form input elements
for that nested form group in another element, e.g. div
and declare a formGroupName
attribute:
// Component definition
const fg = createFormGroup({
firstName: 'John',
address: {
city: '',
postalNumber: null
}
});
return (
<>
<form use:formGroup={fg}>
<label for="firstName">First name</label>
<input id="firstName" type="text" formControlName="firstName" />
<div formGroupName="address">
<label for="city">City</label>
<input id="city" type="text" formControlName="city" />
<label for="postalNumber">Postal number</label>
<input id="postalNumber" type="number" formControlName="postalNumber" />
</div>
</form>
</>
);
All rules and features apply to the nested form groups as well:
- accessing the
value
,disabled
,dirty
,touched
,valid
anderrors
signals - pre-configuring nested form control with
disabled
andvalidators
- using proper HTML input elements with proper form control types
- runtime type checking for proper using of form group keys and values
FAQ
I'm getting Uncaught ReferenceError: formGroup is not defined
If you encounter this problem, make sure you have following vite-plugin-solid
options turned on:
// vite.config.ts
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';
export default defineConfig({
// Enable following `vite-plugin-solid` config option:
plugins: [solidPlugin({ typescript: { onlyRemoveTypeImports: true } })],
});
Solution for the problem was found in this answer for similar issue for SolidJS directives.
I'm getting type errors when defining use:formGroup
, formGroupName
and formControlName
attributes
If you encounter TypeScript type errors when using the formGroup
directive and new attributes
with your HTML elements, try extending SolidJS's JSX namespace:
declare module 'solid-js' {
namespace JSX {
interface Directives {
formGroup?: {};
}
interface InputHTMLAttributes<T> {
formControlName?: string;
}
interface SelectHTMLAttributes<T> {
formControlName?: string;
}
interface HTMLAttributes<T> {
formGroupName?: string;
}
}
}
This will allow you to bind Solar's form group to your form elements without TypeScript type errors related to new HTML attributes.
Roadmap
- [x] Creating and exporting built-in validator functions for common usage
- [x] Support for
<select>
element - [ ] Support for
<textarea>
element - [ ] Defining and using form arrays
- [ ] Support for async validators
- [ ] Documentation for API
Support
If you want to say thank you and/or support development of Solar Forms:
- Add a GitHub Star to the project.
- Tweet about the project on your Twitter.
- Write about it on Medium, Dev.to or personal blog.
Contribution guidelines
🚧 Under construction! 🚧
Inspirations
This library is heavily inspired by Angular's reactive forms although it was adapted to match more "hook-like" or "signal-like" form of accessing form group state.
Many thanks to all people who contributed to growth of Angular's reactive forms over the years! 🙏