so i was playing w/ forms in vanilla JS recently, short story it was very frustrating, repetitive and tedious, and repetition is a good sign for abstraction, which i did, inspired by the angular reactive forms module.
simply: creating an abstraction around inputs that get user input, perform validation and return errors in a single value, furthermore a way to group multiple abstractions into a super abstraction which perform exactly the same thing
in short a way to encapsulate a single input into a value, that can get the value, perform validations
and track errors, example:
<input placeholder="single Input" role="v-single">
const single = new vcontrol("", "v-single", {validators: [validators.required, len]})
// sub to changes
let unsub = single.subscribe((val)=> {
console.log(val)
})
if(single.valid){
}
console.log(single.errors)
console.log(single.value)
The example is for a single input. you can compose multiple inputs into a single value called vgroup.
Tutorial
the module does not have any dependencies, you can install it via npm
npm i vanilla-forms
Concepts
vcontrol - encapsulate a single input and observe for validity, changes and errors
vgroup - compose multiple vcontrols into a single value
getting started
vanillaForms uses the html role attribute to reference an input element, you can name your roles for elements anything, preferable it must start with v- to avoid any clash with native roles if they exist.
<input placeholder="single Input" role="v-single">
creating a control
import {vcontrol, validators} from " vanilla-forms"
const single = new vcontrol("", "v-single", {validators: [validators.required]})
The first param in vcontrol is the default value, second the role, third options, we will cover options later
with the above you have access to the validity of the input, the current value of the input, subscriptions to changes and a map of errors
// subscribe to input changes, subscribe returns an unsubscribe function,
let unsub = single.subscribe((val)=> {
console.log(val)
})
setTimeout(() => {
unsub() // stop listening to changes
}, 3000);
console.log(single.all)// {value: '', valid: false, errors: Map(1)}
console.log(single.valid) // boolean
console.log(single.value) // user input
console.log(single.errors) // Map(1) {'required' => true}
Errors are informed by validators, key in the map being the error and value boolean, true means the error occurred, you can create custom validators
before we talk about vgroup, lets talk about validators
Validators
are just functions executed against the input, to determine whether it is valid or not, for example the built in validators.required is the following function written in TS
function required(value:any){
if(value.length > 0 || typeof value === 'number'){
return {valid: true}
}
else {
return {reason: "required", error: true}
}
}
The fn is just testing if the input value's length is bigger than 0 or of type number for input like ranges, which is valid, you can have multiple validators in a single control, if one of them fails the entire input is invalid, you can create custom validators and just put them in the validators array, all you need is a function that takes in the control value and performs a check , and then returns an object based on the result of that check
example: checking for value length
function len(controlValue){
if(controlValue.length < 8 ){
return {reason: "length must be 8", error: true}
}
else{
return {valid: true}
}
}
On check failure, an object with a reason and boolean error is returned and otherwise an object with valid for a successful input, you can perform as many checks as you want, as long as the function returns one of the objects, and take as a parameter the controlValue, to use it you just put the function in the validators array in the options object
const single = new vcontrol("", "v-single", {validators: [validators.required, len]})
//the input will be checked against two validators required and len if one fails the entire input is invalid
I only included one built in validator as an example, because there are many ways to validate inputs, for various reasons like emails etc and I am avoiding bloating the package, and I cannot cover them all, if somehow you find this package useful and have cool custom validators, if you don't mind please contribute by creating a separate npm package and contacting me, I'll place the link to it in the read me section
vGroups
grouping multiple controls together,
<form>
<input placeholder="username" role="v-username">
<input placeholder="password" role="v-password" type="password">
<input placeholder="email" role="v-email" type="email">
</form>
creating a group is similar to creating a control, the only difference is you do not have to create multiple new controls and compose them together the package handles that internally
const signup = new vgroup({
user: {defaultVal: "Hello",
element: "v-username",
opts: {validators: [validators.required, len]}
}, // user control
password: {defaultVal: "Hello world", element: "v-password", opts: {validators: [validators.required]}}, // password control
email: {defaultVal: "Hello world", element: "v-email", opts: {validators: [validators.required]}}, // email control
})
new vgroup takes a single object, with objects inside representing controls
const signup = new vgroup({
// controls go here
})
a control inside the group is an object, it's key is used for value printing, and errors so you can Identifier which input you are accessing
user: {defaultVal: "Hello",
element: "v-username",
opts: {validators: [validators.required, len]}
}
// user will be used to identifier which value is for which control
// opts stands for options, you have to explicitly define it, we already covered validators
now you can print errors, subscribe and check for validity for the group
if(signup.valid){
console.log("signup is valid")
}
const signup_unsub = signup.subscribe((val)=> {
console.log(val)
console.log(signup.errors)
})
console.log(signup.errors)
if one element in the group fails, the entire group is invalid, you can access the errors in the map of the same name and show them to users
full example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body{
/* margin: 0;
padding: 0;
box-sizing: border-box; */
display: grid;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
form{
display: grid;
gap: 5px;
}
input {
/* width: 50%; */
height: 28px;
}
</style>
</head>
<body>
<form>
<input placeholder="username" role="v-username">
<input placeholder="password" role="v-password" type="password">
<input placeholder="email" role="v-email" type="email">
</form>
<br>
<form>
<input placeholder="single Input" role="v-single">
</form>
<br>
<form>
<input type="range" role="v-range">
<input type="color" role="v-color">
<input type="file" role="v-file">
</form>
<script src="main.js"></script>
</body>
</html>
forms.js
import {vcontrol, validators, vgroup} from " vanilla-forms"
function len(controlValue){
if(controlValue.length < 8 ){
return {reason: "length must be 8", error: true}
}
else{
return {valid: true}
}
}
export const signup = new vgroup({
user: {defaultVal: "Hello",
element: "v-username",
opts: {validators: [validators.required, len]}
},
password: {defaultVal: "Hello world", element: "v-password", opts: {validators: [validators.required]}},
email: {defaultVal: "Hello world", element: "v-email", opts: {validators: [validators.required]}},
})
export const single = new vcontrol("", "v-single", {validators: [validators.required, len]})
export const exoticform = new vgroup({
range : {defaultVal: 20, element: "v-range", opts: {validators: [validators.required]}},
color : {defaultVal: "#3e1919", element: "v-color", opts: {validators: [validators.required]}},
file : {defaultVal: "", element: "v-file", opts: {validators: [validators.required]}},
})
main.js
import {signup, single, exoticform} from "./forms"
//you can interact with the forms as you like
to use imports directly in the browser you must declare type module in the script, for my case I didn't i was using the parcel bundler
Notes
validity - in terms of validity for default values I was torn in between, cause technically a default value does not mean valid, cause the form is still clean, meaning the user has not touched it yet, so be aware of that, also some inputs like text input default values validate, while some like ranges always fulfill the required because they have the value of 0 initially, even without setting a value, 0 is a value, but this is something that can be fixed with time
checkboxes and radios - these inputs are quite different from the normal ones, their support is loading
Thank you
Thank you for reading, if you found this useful or not feedback is greatly appreciated
=o)
Top comments (0)