Form Validation As A Higher Order Component Pt.1

Functional Style Input Validation

Introduction

Form handling sounds trivial sometimes, but let’s just take a second to think about what is involved in the process. We need to define form fields, we need to validate the fields, we also might need to display errors according to fact if the input validates, furthermore we need to figure out if the validation is instant or only after clicking on a submit button and so on and so forth.

You might have been looking for ways to tackle this problem by searching for different off the shelf solutions, but more often than not I guess a lot of reimplementing the same old is the norm in actual code. If this is the case, this post will be about exploring alternative approaches for handling form validation, where the ideal outcome should be write once and use almost everywhere.

Sometimes we can solve a recurring task with a straight forward implementation, which we will try to define in this following write up.

The Basics

Let’s begin with the first approach that comes to mind, defining validation functions for every possible case that needs to be validated. To get a better idea, let’s take a closer look at how we would define user name and age validation functions.

const isNameValid = name => name.trim().length >= 6

We want the user name to have a minimum length of 6, which we can easily verify if valid. Now what about the age function? We might want to validate that age can either be empty or has to be a minimum of 18.

const isAgeValid = age => (age === undefined || age >= 18)

So we have written functions that validate our inputs, but we will also need a way to run the validator functions and return all error messages, so we can give the user feedback.

const getErrors = ({ name, age }) => {
const errors = {}
if (!isNameValid(name)) {
errors.name = 'Name has to be a minimum length of 6'
}
// ... more validations
return errors
}

The outcome so far is, that we have implemented a validation that works for a very specific case, calling getError with name and age will return a object containing possible error messages. We’re able to render the error messages inside a view function or do something useful with it. Now, can we do better?

Breaking Things Apart

isNameValid is not really reusable, neither is isAgeValid, so let’s make our functions more reusable, so we can combine them as needed, stretching their usage beyond a single part of the application and turning them into project agnostic functions. But where to begin?

const isNameValid = name => name.trim().length >= 6

Combining helper functions to create predicate functions seems the reasonable approach here. We don’t really care if the input is a name or a street number. What we’re interested in, is the fact that our input data is greater than a predefined length.

const isGreaterThan = (len, data) => data > len

Now our isNameValid can be re-rewritten to something like the following:

const isNameValid = isGreaterThan

And can be called like this

isNameValid(5, name.trim().length)

This doesn’t really look elegant, so let’s refine one more time. We’re interested in the name value’s length, so let’s write a function that expects an input and returns its length.

const getLength = data => data.length
isNameValid(5, getLength(name.trim()))

This is starting to look sensible. Let’s get back to our getErrors function, where we checked every single validation rule manually and set an explicit error message when the predicate returned false.

This is alright and good when we need to quickly get something up and running, but less optimal when done all over the place. Why not abstract away the validation handling? Let’s give it a try.

Refining the Validation Handling

Our first try is to either return an error message once the predicate fails or null if successful is the case.

const isGreatThan = (len, data) => (len > data )
? null
: `A minimum length of ${(len + 1)} is required.`

So calling isGreaterThan function will either return an error string or null. This enables us to define the error message once and use it all over the place. But there is still something suboptimal about this solution. We’ve defined an error message without considering the context it is being applied in. Maybe we want to define a different message in another application or in a different part of the existing app. Time to refactor our implementation once again.

const isGreaterThan = (len, errorMsg, data) => (len > data)
? null
: errorMsg

Now we can call isGreaterThan with a defined minimum length and error message an use it all over the app. Take a look at this.

const errorMsg =  `A minimum length of 6 is required for Name.`
const
isNameGreaterThanFive = isGreaterThan.bind(null, 5, errorMsg)
isNameGreaterThanFive(getLength(name.trim()))

This is all good and somewhat reusable, but what if we could define a function that expects a predicate and error message tuple and either return null or an error string.

const createPredicate = ([test, errorMsg])
=> a => test(a) ? null : errorMsg

Going back to our original isGreaterThan function, we can refactor to this.

const errorMsg =  'A minimum length of 6 is required.'
const isGreaterThanFive
=
createPredicate([isGreaterThan.bind(null, 5), errorMsg])

The last refactor enables us to define validation rules for a given context, possibly run through all the validators and return the errors. To achieve this we we will write a function that expects a map containing validation rules for a set of defined inputs. We will be using Ramda from now on to map, filter etc.

const createPredicates = validations 
=> R.map(createPredicate, validations)

Which can be refactored to:

const createPredicates = R.map(createPredicate)

Now that we have createPredicates in place we can simply pass in a validation object. The interesting part is that an input might have two or more validations that need to be applied. For example the random field needs to contain a capital letter and have a minimum length of 8.

No problem, we simply map over the validations and create predicate functions for every validation rule that needs to run against a passed in user input.

const validationRules = {
name: [
[ isGreaterThan(5),
`Minimum Name length of 6 is required.`
],
],
random: [
[ isGreaterThan(7), 'Minimum Random length of 8 is required.' ],
[ hasCapitalLetter, 'Random should contain at least one uppercase letter.' ],
]
}

const validations = R.map(createPredicates, validationRules)

This is a good moment for a quick recap of what we have so far. Two functions that can turn a validation map into a set of functions expecting an input and running the needed functions against this input. But where does the input come from?

Imagine we have input data like the following.

const inputData = { name 'abcdef', random: 'z'}

If there are any existing validation rules for a specific input, we want to run the input against those validators, to verify that the data is correct, and else return the error messages. Let’s create a runPredicates function that expects the data and validation tuple and run all predicates against the provided input.

const runPredicates = ([input, validations]) =>
R.map(predFn => predFn(input), createPredicates(validations))

Now that we have a function that knows how to run the predicates with the input we still need a function that iterates over a set of inputs. This is can be tackled with a one liner.

const validate = inputData => 
R.map(data => runPredicates(data), inputData)

Or even simpler.

const validate = R.map(runPredicates)

Before we continue let’s have a look at our current implementation.

const createPredicate = ([test, errorMsg]) 
=> a => test(a) ? null : errorMsg
const createPredicates = R.map(createPredicate)
const runPredicates = ([input, validations]) =>
R.map(predFn => predFn(input), createPredicates(validations))
const validate = R.map(data => runPredicates(data))

What we could do at the moment is run validate with a validation object, containing field keys that map to a tuple containing the input and the predicates.

validate({
name: [
'foobar',
[
[ isGreaterThan(5), 'Minimum length of 6 is required.' ]
],
]
})

Running the above example will return an object containing the following values.

{ name: [null] }

If we would have run against two validation functions we might have gotten this type of result.

{ name: [null, 'Should contain at least one uppercase letter.'] }

We’re only interested in the error messages, and we also want to get rid of the array. Now we can choose between different options. A common approach would be to filter any null values and join the result.

['foo', null, null, 'bar'].filter(x => x !== null).join(', ')

This means that we would have to run this “clean up” operation before returning the result back from our validate function. This would only require a small change inside our validate function.

const validate = R.map(data => 
R.join(' ', R.filter(R.identity, runPredicates(data))))

And result in this output for example.

{ name: 'Minimum length of 6 is required.' }

To summarize the current approach: we have created a validate function that expects a validation object containing an input and predicate tuples which either returns an error or empty string. This might me be enough for your use case, but we can even build upon what we have and provide an even better handling.

Advanced Solution

Now that we have a basic implementation it’s time to refine our solution and make it even more useful. Let’s remember how our createPredicate is implemented.

const createPredicate = ([test, errorMsg]) => a 
=> test(a) ? null : errorMsg

This all good, but let’s add another library to the mix. We will add folktales Either, which will help us to simplify the validation handling. The documentation states thatEither may contain either a value of type a or a value of type b, at any given time ... A common use of this structure is to represent computations that may fail, when you want to provide additional information on the failure.”

For now all we need to know now is that Either can be a Left or a Right container, which holds a value. For a more detailed explanation, I would recommend reading the fantastic Mostly Adequate Guide To Functional Programming (especially Chapters 8–10) by Brian Lonsdorf or consult the Folktale documentation. To gain a better understanding why using Either might make sense, take a closer look at the next example.

const hasCapitalLetter = (a) => /[A-Z]/.test(a)
? Either.Right(a)
: Either.Left('Nope')

So say our validator function fails, we now have an Either instance Left containing an explicit failure message. If the advantage is not clear yet, it will be soon. Looking back at our original createPredicate we can now refactor it, to return either a Right or Left.

const createPredicate = 
([predFn, e]) => a => predFn(a) ? Right(a) : Left(e)

This also implies that we need to change our validate function to handle the new returned values.

const validate = 
R.map(R.compose(R.sequence(Either.of), runPredicates))

This is all we need to do to enable our validate to return either a Right or Left. To fully understand what is happening here, let’s run through it step by step.

const validate = inputValidation => {
  const result = 
inputValidation.map(validate => runPredicates(validate))
  return R.sequence(Either.of, result)
}

We’re mapping over all input/validation pairs and applying the runPredicates function with the pairs. What we get back as a result is an object containing an array of Eithers, but what we really want to know is if there is a Left instance somewhere inside that array.

The interesting part to understand here, is that as soon as one Left is found the result will be a Left. If no Left is found, the result will be a single Right containing all successful results. This is exactly what Ramda’s sequence method does, so we simply run the sequence with the result of our mapped validation pairs. Here’s an example straight from the Ramda documentation with a Maybe.

R.sequence(Maybe.of, [Just(1), Just(2), Just(3)])
//=> Just([1, 2, 3])
R.sequence(Maybe.of, [Just(1), Just(2), Nothing()])
//=> Nothing()

Let’s also recap on our input data.

const validationRules = {
name: [
[ isGreaterThan(5),
`Minimum Name length of 6 is required.`
],
],
random: [
[ isGreaterThan(7), 'Minimum Random length of 8 is required.' ],
[ hasCapitalLetter, 'Random should contain at least one uppercase letter.' ],
]
}

const inputData = { name: 'Foo', random: 'test' }

We have validationRules and we have inputData, that could be coming via a form submit or a fetch, it doesn’t really matter. What we need is a away to combine the two inputs into a bigger structure. This can be accomplished by quickly adding a helper function that takes care of creating the validation object.

const makeValidationObject = R.mergeWithKey((k, l, r) => [l, r])

We’re using Ramda’s mergeWithKey function, which expects two objects and returns a [input, validation] in our case as soon as two keys exist in both objects. Finally, let’s expose a function that expects the data that needs to be validated and the validation rules themselves, so we don’t have to manually take care of creating the new structure.

const getErrors =  R.compose(validate, makeValidationObject)

When we pass in our input data and the validation rules to getErrors, we receive an object containing all the keys with either Left or Right values as a result.

getErrors(inputData, validationRules)

Here is the complete code up until now.

import R from 'ramda'
import
Either from 'data.either'

const
{ Right, Left } = Either

const makePredicate =
([predFn, e]) => a => predFn(a) ? Right(a) : Left(e)

const makePredicates = R.map(makePredicate)

const runPredicates = ([input, validations]) =>
R.map(predFn => predFn(input), makePredicates(validations))

const validate =
R.map(R.compose(R.sequence(Either.of), runPredicates))

const makeValidationObject = R.mergeWithKey((k, l, r) => [l, r])

const getErrors = R.compose(validate, makeValidationObject)

We’re almost done here, but there a couple of things that we should consider. First of all, we can’t handle objects that contain other objects at the moment, just think of a multi tab for example, where we might have a structure like this:

{ tab1: { userName: '' }, tab2: { streetNr: 2 } }

This isn’t a huge problem, but it’s not covered with our current implementation. The other aspect is that we still have an Either which isn’t very helpful when we need to represent a final result back to the user. This is where things get really interesting actually and where we will see how everything comes together when we want to render the result inside a UI component.

Just to clarify, what we have when we call getErrors is the following.

{
name: Left('Mininum Length of 6'),
random: Right('FoobarBaz')
}

What we’re interested in are the errors. Let’s write a function that knows how to handle an Either instance by applying the cata method on the instance and returning the desired value.

const displayError = result =>
result.cata({
Right: a => null,
Left: errorMsg => errorMsg
})

Now all we need to do is map over the result and call displayError for every property.

R.map(displayError, getErrors(inputData, validationRules))

In this specific case we either receive a string or a null.

{ name: null, random: "Minimum Random length of 8 is required." }

This should suffice for now and if we like we could replace our displayError function with another function that might return a DOM representation for example. We will see how we can leverage the fact when we build the high order component for form handling in the next part of this two parts posting.

Here is a full example.

Outro

We have written our own framework agnostic validation handling which we will expand on in the next part, where we will build a higher order component that takes care of rendering our errors as well as the validation itself.

Continue with Part 2, to see the High Order Component implementation.

Any thoughts? Let me know.

Any questions or Feedback? Connect via Twitter

Links

Ramda

Folktale Either