Handle forms like a boss!, reactive forms in vanilla JavaScript.
Tags: javascript, webdev, typescript
Published: 2021-09-12 01:08:21



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: 

```xml
<input placeholder="single Input" role="v-single">
```

```js

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 

```cmd

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.


```xml
<input placeholder="single Input" role="v-single">
```

creating a control 

```js

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

```js
// 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

```js
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

```js

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

```js

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, 

```xml

<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

```js

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 

```js
 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

```js

 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

```js

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 

```xml

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


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


```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)