DEV Community

Cover image for Handle forms like a boss!, reactive forms in vanilla JavaScript.
sk
sk

Posted on • Updated on

Handle forms like a boss!, reactive forms in vanilla JavaScript.

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">
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

creating a control


import {vcontrol, validators} from " vanilla-forms"


const single = new vcontrol("", "v-single", {validators: [validators.required]})
Enter fullscreen mode Exit fullscreen mode

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}


Enter fullscreen mode Exit fullscreen mode

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}

     }

}


Enter fullscreen mode Exit fullscreen mode

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}

     }

}



Enter fullscreen mode Exit fullscreen mode

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 




Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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

})


Enter fullscreen mode Exit fullscreen mode

new vgroup takes a single object, with objects inside representing controls

 const signup = new vgroup({

    // controls go here
 })

Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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>



Enter fullscreen mode Exit fullscreen mode

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]}},

})

Enter fullscreen mode Exit fullscreen mode

main.js


import {signup, single, exoticform} from "./forms"


//you can interact with the forms as you like


Enter fullscreen mode Exit fullscreen mode

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)

Oldest comments (0)