What could be more elegant than testing the validity of a form in the same way you’d test your code?
Vest is a powerful JavaScript data validation library that derives its syntax from modern JS frameworks such as Mocha or Jest. It is framework-agnostic, so you can drop it into your code in no time.
Unit testing applied to data validation? Yep!
If with Mocha we would do this:
import { assert } from 'chai' let hello = () => 'Hello!' assert.equal(hello, 'Hello!')
In a similar manner, with Vest we can do this:
import { enforce } from 'vest' let hello = document.querySelector('#hello').value enforce(hello).equals('Hello!')
To begin with, we need to export a validation function — aka a suit — that will be used to perform tests on input data passed as an object.
import vest, { test, enforce } from 'vest' const suitName = 'signUpSuit' const validate = vest.create(suitName, formData => { test('username', 'Should not be empty', () => { enforce(formData.username).isNotEmpty() }) test('password', 'Should be longer, () => { enforce(formData.password).longerThanOrEquals(8) }) test('password_repeat', 'Should match password', () => { enforce(formData.password_repeat).equals(formData.password) }) }) export default validate
Now we can simply call it when the validation should occur in our logic:
import validate from './signup/validate.js' let response = validate(formData)
Vest is a general-purpose data validator, so it does not include any form-serialization method — but hey, it is pretty easy to collect data! Either you are writing vanilla JS:
let formData = inputs.reduce((data, input) => { return { ...data, ...{ [input.name]: input.value } } , {})
Or using a framework like React:
const [formData, setFormData] = useState({}) const onInputChange = e => { setFormData(data => { ...data, ...{ [e.target.name]: e.target.value } }) }
Now that we know how to collect and pass our data, we need to handle the result of the validation suit in order to tailor a proper response for the user.
Validation always returns an object containing a pool of useful info and methods:
let response = validate(formData)
Anyway, code can quickly get messy when dealing with variables assigned on the fly. Vest provides a .done()
method that can be chained multiple times to validation function. Here we can pass a callback function in order to handle the response gracefully:
validate(formData).done(callback)
Specifying an optional key before the callback function, we can explicitly wait for the validation of a single field. The callback will be executed only when the condition is met, despite its order in the chain.
validate(formData) .done('username', usernameCallback) .done(formCallback)
To check whether any test has failed, we need to call the .hasErrors()
response method.
if (response.hasErrors()) let errors = response.getErrors()
If true, the full list of errors will be accessible from the .getErrors()
method as a key-value object. For every field that didn’t pass a test, there will be an array containing a list of error messages.
Multiple tests can be provided for a single field, and the order of appearance will be reflected in the array.
test('username', 'Cannot be empty', () => { enforce(formData.username).isNotEmpty() }) test('username', 'Must be longer than 3 chars', () => { enforce(formData.username).isLongerOrEquals(4) })
If a name is passed to the methods above, only errors for that particular field will be returned:
if (response.hasErrors('password')) { let passwordErrorMessage = response.getErrors('password')[0] // ...
When vest.warn()
is called for a test, its messages will be accessible from the .hasWarnings()
and .getWarnings()
methods instead.
This allows us to separate response messages between two different levels of importance.
test('username', 'Should not be a number', () => { vest.warn() enforce(formData.username).isNotNumeric() })
Tests can be grouped in order to filter which inputs should be tested at a certain point of our logic. This is highly useful — for example , to validate multipage forms sending data in sequential steps.
const validate = vest.create(suitName, formData => { group('formPage1', () => { // test(...) }) })
The validation result by group will be accessible from .hasErrorsByGroup()
and .getErrorsByGroup()
:
if (response.hasErrorsByGroup('formPage1')) { // ...
Tests can also be filtered out using vest.skip()
. This method accepts either a field name or a group name and can be used to avoid a test under certain conditions.
const validate = vest.create('purchaseSuit', formData => { if (!formData.coupon) vest.skip('coupon') // ...
By calling vest.only()
, we can ask Vest to execute tests for a particular field or group only.
const validate = vest.create('userSuit', (data = {}, inputName = '') => { if (inputName) vest.only(inputName) // ...
This is particularly useful when validating data in real time while the user is typing:
const onInputChange = e => { validate({ [e.target.name]: e.target.value }, e.target.name) .done(e.target.name, callback) }
Vest is stateful by default, so even if we are passing data for a single field — as in the example above — the response will return the most recent validation results for all the fields that have already been tested, making it much easier to update our application state accordingly.
Furthermore, the latest suit state is always accessible from vest.get(suitName)
, and can be reset via vest.reset(suitName)
.
If you would like to run stateless, import a validate()
function directly from Vest instead of creating a new one. This is better suited for server-side validation, where the response is expected to be idempotent. As such, use:
const response = validate('stateless', data => { // ... })
Rather than:
const validate = vest.create('stateful', data => { // ... })
Vest allows for async validation. This is particularly useful when we need to check data against entries in a database — like checking whether a username is already in use.
test('username', 'Should not already exist', async () => { return await checkIfUserExists(formData.username) })
Keep in mind that async responses could come after other sync tests already finished running. Generally speaking, it’s suggested to execute async tests at the bottom end of the suit logic, after all sync requests have run.
In such cases, we can preserve server resources using the vest.draft()
method. This way, we can perform conditional operations against the state of a suit that is currently running and avoid unneeded requests:
test('username', 'Cannot be empty', () => { enforce(formData.username).isNotEmpty() }) if (!vest.draft().hasErrors('username')) { test('username', 'Should not already exist', async () => { return await doesUserExist(formData.username) }) }
enforce()
rules can be chained together, and if the defaults do not satisfy your needs, you can simply create custom logics returning a Boolean or — for better reusability — extend the default pool of methods:
import { enforce } from 'vest' const customRules = { isTruthyStr: value => ['true','on'].includes(String(value).toLowerCase()), isFalsyStr: value => ['false','off'].includes(String(value).toLowerCase()), } enforce.extend(customRules)
With Vest’s any()
, we can express a logical OR between rules. If any of the enforced rules are satisfied, the whole test is considered as passed:
test('comment', 'Must be at least 140 chars, if provided.', any( () => enforce(data.comment).isEmpty(), () => enforce(data.comment).longerThanOrEquals(140) ) )
A tailor-made suit is nothing without class, right? Vest provides a classNames()
function, a powerful method for associating CSS classes to DOM elements based on the validation state:
import classNames from 'vest/classNames' const getFieldClass = classNames(response, { untested: 'is-untested', tested: 'is-tested', valid: 'is-valid', invalid: 'has-errors', warning: 'has-warnings', })
This allows for live updates to our components’ appearance:
function InputText(props) { return <input type='text' name={props.name} value={props.value} className={getFieldClass(props.name)} onChange={e => onInputChange(e)} /> }
In this way, it is possible to express complex UI logics based on inputs’ state, in real time, without getting spaghetti!
There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.
LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.
LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Build confidently — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]