Skip to content

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.

const validation1 = Validation();
const validation2 = Validation();
// validation2 will run when validation1 becomes "invalid"
validation1.invalid(validation2);

Or unconditionally (sequentially), just one after another:

const validation1 = Validation();
const validation2 = Validation();
// validation2 will run after validation1
validation1.validated(validation2);

So when validation1 gets validated the code snippets above are roughly equivalient to this:

const validation1 = Validation();
const validation2 = Validation();
// conditional execution
validation1.validate().then((res1) => {
if (!res1.isValid) {
validation2.validate();
}
return res1;
});
// unconditional execution
validation1.validate().then((res1) => {
validation2.validate();
return res1;
});

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:

textField.addEventListener(
'input',
Validation(textField)
.constraint(isOnlyLetters)
.valid(paintFieldGreen) // paint green if only letters
.invalid( // otherwise check for the initial value
Validation(textField)
.constraint(isInitValue)
.valid(paintFieldGray) // paint gray if initial value
.invalid(paintFieldRed) // otherwise paint red
)
);

If you wrote it in “vanilla” javascript it might have looked like this:

textField.addEventListener(
'input',
function(e) {
const { value } = e.target;
if (isOnlyLetters(value)) {
paintFieldGreen();
}
else {
if (isInitValue(value)) {
paintFieldGray();
}
else {
paintFieldRed();
}
}
}
);

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.

const validation1 = Validation(element1);
const validation2 = Validation(element1);
const validation3 = Validation(element1);
// validation2({ target: element1 }) // works
validation1.invalid(validation2);
// validation3({ target: element1 }) // works
validation2.invalid(validation3);
// validation1({ target: element1 }) // works
element1.addEventListener('input', validation1);

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.

const validation1 = Validation(element1);
const validation2 = Validation(element1);
const validation3 = Validation(element2);
validation1.invalid(() => validation2.validate()); // works
validation2.invalid(() => validation3.validate()); // works
element1.addEventListener('input', validation1);

Example

In this example there are three chains of validations:

ageV —> careerYearsV —> experienceV —> initValueV

formV({ target: age }) —> ageV.validated(careerYearsV) —> careerYearsV.validated(() => experienceV()) —> experienceV.validated(initValueV) —> initValueV({ target: experience })

ageV —> initValueV

formV({ target: age }) —> ageV.validated(initValueV) —> initValueV({ target: age })

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.

const validation1 = Validation(element1);
const validation2 = Validation(element1);
const validation3 = Validation(element2);
const validationGr23 = Validation.group(validation2, validation3);
// runs predicates added to validation2
validation1.invalid(validationGr23); // validationGr23({ target: element1 })
validation1.invalid(() => validationGr23.validate(element1));
validation1.invalid(() => validationGr23({ target: element1 }));
// runs predicates added to validation3
validation1.invalid(() => validationGr23.validate(element2));
validation1.invalid(() => validationGr23({ target: element2 }));
element1.addEventListener('input', validation1);

Let’s recompose the validation chains from the example above.

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({ 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.