Chaining validations
Validation
as a state callback
Since Validation
objects are callable and, while executing on the client side, they are event handler
functions, it is possible to chain their execution by adding them as state callbacks
and run conditionally: one depending on the state of another.
Or unconditionally (sequentially), just one after another:
So when validation1
gets validated the code snippets above are roughly equivalient to this:
The result of validation2
is not “awaited” and does not affect the result of validation1
.
Example
In this example one Validation
object is added as “invalid” state callback of another one. Initially, the input field is painted gray. Try to input letter characters, it turns green. Then type in something other than letters, it turns red. Eventually, if you decide to clear it out, it returns back to its initial state and becomes gray.
Depending on your code style you could write the above validation logic like this:
If you wrote it in “vanilla” javascript it might have looked like this:
Chaining validations of different validatable objects
A state callback function accepts a ValidationResult
object which has the target
property. A Validation
object, when used as an event handler
on the client side, accepts an Event
object which also has the target
property. So it is possible to chain Validation
objects bound to the same validatable object.
If you need to chain validations bound to different validatable objects you can wrap a Validation
into an arrow function to isolate from the passed in argument:
validation2.invalid(() => validation3());
Also the safest way (but rather verbose) and compatible with both client and server side is to call the .validate()
method explicitly and pass no arguments in case of a “single” Validation
. See “Description” of the Validation.validate()
method.
Example
In this example there are three chains of validations:
ageV
—> careerYearsV
—> experienceV
—> initValueV
ageV
—> careerYearsV
—> experienceV
—> initValueV
formV({ target: age })
—> ageV.validated(careerYearsV)
—> careerYearsV.validated(() => experienceV())
—> experienceV.validated(initValueV)
—> initValueV({ target: experience })
ageV
—> initValueV
ageV
—> initValueV
formV({ target: age })
—> ageV.validated(initValueV)
—> initValueV({ target: age })
experienceV
—> initValueV
experienceV
—> initValueV
formV({ target: experience })
—> experienceV.validated(initValueV)
—> initValueV({ target: experience })
The third chain is a part of the first one because validity of data in the experience field depends on data in the age field that is possible experience can not be greater than the entered age minus the start career age which is 21 in this example. Therefore, the experience field needs to be revalidated every time data in the age field changes.
Validity of data in the fields are checked with ageV
and experienceV
validations which are grouped into formV
that serves as the entry point of the form validation process and controls access to the submit button. initValueV
is used to paint the fields gray when they are empty. The experience field gets enabled with careerYearsV
and experienceV
gets revalidated with it. Since careerYearsV
and experienceV
are bound to different fields the latter is wrapped into an arrow function when added as “validated” state callback of the former.
It might look irrational to validate data against field constraints first and after all validations perform the initial value check. But if we put initValueV
first and then will be performing the rest validations unconditionally, the fields will never paint gray when they are empty. So the order of the validations in the chains makes sense.
The next example below shows how the validation chains could have been recomposed and the problem it entails.
Chaining grouping validations
If you need to conditionally validate a particular predicate group of a “grouping” Validation
, pass the validatable object it associated with to the .validate()
method. See “Description” of the Validation.validate()
method.
Let’s recompose the validation chains from the example above.
initValueV
—> ageV
—> careerYearsV
—> initValueV
—> experienceV
initValueV
—> ageV
—> careerYearsV
—> initValueV
—> experienceV
initValueV({ target: age })
—> initValueV.validations[0].invalid(formV)
—> ageV.validated(careerYearsV)
—> careerYearsV.validated(() => initValue.validate(experience))
—> initValueV.validations[1].invalid(formV)
—> experienceV()
initValueV
—> experienceV
initValueV
—> experienceV
initValueV({ target: experience })
—> initValueV.validations[1].invalid(formV)
—> experienceV()
Now initValueV
serves as the entry point of the form validation process and starts validating a field against its constraints if the field is not empty. The second chain is a part of the first one because the experience field still needs to be revalidated when the age field changes. And since initValueV
is a grouping validation, when added as “validated” state callback of careerYearsV
, it gets the experience field as the target object to perform check only on this field leaving out the age field (It is already being checked! If you omit that passed in target object, checks on both fields will be executed and you will get an infinite loop). Next, the passed in object is getting passed further along to formV
to validate only the experience field against its constraints.
Although the order of validations now may look more sound, the problem with this example after recomposing the chains is that now actual form data validation formV
depends on the validation initValueV
which is performed merely for the sake of side effects. Now after transition from “invalid” to empty state the experience field remains “invalid” (though it should be “valid”, it is optional) and the submit button doesn’t get enabled. The opposite true for the age field. This is because formV
is performed conditionally.