Yup Validation for Date and Time With MomentJS
#formik #forms #momentjs #react #yup
6 min read
Remember this blog post on time input fields? In a somewhat related post, I'm going to write about how I used Yup validation with my Formik forms, to test for valid inputs, in the event that proper date and time input fields are actually rendered by the browser.
If you're not familiar with Yup validation, it's a package I'd really recommend as a simple, lightweight way to run Object based validation. If you're using Formik for form management, using Yup is just one extra small step as Formik already accepts Yup validation objects out of the box.
The specific situation I have is this:
- Two separate Formik forms with two input fields in each:
- Form 1: start date and start time
- Form 2: end date and end time
- The end date cannot be later than today.
- The end date cannot be earlier than the start date.
- If the end date equals the start date, the end time must be 1 hour or more after the start time.
- Upon submission of Form 1, the values for the fields are updated and stored in the Redux state.
I created 2 custom components - DateInput
and TimeInput
- that look like this. ๐๐ป The DateInput
is a wrapper for a react-datepicker
date component (which allows for nicer date pickers). ๐
// Start date and time
<DateInput
label={intl.formatMessage({ id: 'start_date.label' })}
name="start_date"
placeholder={intl.formatMessage({ id: 'start_date.placeholder' })}
/>
<TimeInput
label={intl.formatMessage({ id: 'start_time.label' })}
name="start_time"
placeholder={intl.formatMessage({ id: 'start_time.placeholder' })}
/>
// End date and time
<DateInput
label={intl.formatMessage({ id: 'end_date.label' })}
name="end_date"
placeholder={intl.formatMessage({ id: 'end_date.placeholder' })}
/>
<TimeInput
label={intl.formatMessage({ id: 'end_time.label' })}
name="end_time"
placeholder={intl.formatMessage({ id: 'end_time.placeholder' })}
/>
I won't go into the innards of the components, but you can assume they just pass through any validation rules as props. If you've not come across the intl.formatMessage()
bit before, you can ignore it - it just spits out a string. If you're interested, check out the react-intl
package which helps you out with internationalisation for your React app. ๐ฌ๐ง
Step 1
The DateInput
component takes in minDate
and maxDate
props. This is directly passed into the basic input type="date"
HTML element, which takes option attributes for min
and max
. Check out the MDN docs for more info. In the case of maxDate
, I'm saying here that it needs to take the form of a Date
object and it cannot be later than the date right now. (new Date()
defaults to the date at the moment of instantiation).
Note that for the end_date
field, the minDate
refers to the value for start_date
that's in the Redux store.
<DateInput
label={intl.formatMessage({ id: 'start_date.label' })}
name="start_date"
placeholder={intl.formatMessage({ id: 'start_date.placeholder' })}
maxDate={new Date()}
/>
<DateInput
label={intl.formatMessage({ id: 'end_date.label' })}
name="end_date"
placeholder={intl.formatMessage({ id: 'end_date.placeholder' })}
minDate={new Date(state.start_date)}
maxDate={new Date()}
/>
Step 2
The way I pass my validation rules to Formik is via a validationSchema
that is essentially the Yup validation object. Each key in the Yup object is the name
of the input field I want validated.
Looking at start_date
, I want three separate validation rules - (i) that the value takes the form of a Date
object; (ii) max value for the date is today's date; and (iii) that it's a required field. Note that the second argument to .max()
is the error message string you want displayed if the max
validation fails. You can do the same for .required()
but in my case, I didn't want an error message displayed.
For the start_time
, I've only specified that it is a required string.
// Yup validation for start date and time
validationSchema: Yup.object({
start_date: Yup.date()
.max(new Date(), intl.formatMessage({ id: 'start_date.error.max' }))
.required(),
start_time: Yup.string().required(),
})
Step 3
Here's the fun bit - how to ensure that the end date and time as a whole is at least 1 hour or later than the start date and time.
Starting with the end date, the validation is similar to the start date with the only difference being the addition of a minimum value (cannot be earlier than the start date).
// Yup validation for end date and time
validationSchema: Yup.object({
end_date: Yup.date()
.min(new Date(state.start_date), intl.formatMessage({ id: 'end_date.error.min' }))
.max(new Date(), intl.formatMessage({ id: 'end_date.error.max' }))
.required(),
})
The validation for end time is slightly more convoluted. This is again a required field, but I've also included a .test()
check that takes the following arguments in order - name of your test (can be any string value you want), error message if validation fails and finally, a function that returns either true
or false
. If the function returns false
, it means the validation has failed.
// Yup validation for end date and time
validationSchema: Yup.object({
end_date: Yup.date()
.min(new Date(state.start_date), intl.formatMessage({ id: 'end_date.error.min' }))
.max(new Date(), intl.formatMessage({ id: 'end_date.error.max' }))
.required(),
end_time: Yup.string()
.required(intl.formatMessage({ id: 'end_time.error.required' }))
.test('min_end_time', intl.formatMessage({ id: 'end_time.error.min_time' }), function (value) {
const { end_date } = this.parent
if (state.start_date === moment(end_date).format('ddd MMM DD YYYY')) {
return moment(value, 'HH:mm').isSameOrAfter(moment(state.start_time, 'HH:mm').add(1, 'hours'))
} else {
return true
}
}),
})
Going in depth into the function, the value
argument you pass in is the value of the field you're referring to, in this case the end_time
. As I'm not using an arrow function, I need to refer to this
where Yup then gives us access to a parent
object that contains data for sibling fields (in this case the end_date
. I then use object destructuring to grab the end_date
value and save it to a new const
.
const { end_date } = this.parent
The function that I'm then testing against goes like this:
- If the start date is equivalent to the end date, run a check to see whether the current value of
end_time
is the same or greater than 1 hour after the start time. - If the start date is not equivalent to the end date, we're not bothered, the validation passes. Remember that we've already checked that the end date cannot be earlier than the start date in the steps above.
if (state.start_date === moment(end_date).format('ddd MMM DD YYYY')) {
return moment(value, 'HH:mm').isSameOrAfter(moment(state.start_time, 'HH:mm').add(1, 'hours'))
} else {
return true
}
The thing to note is that end_date
which is returned by Yup, is not in the same date format as what's needed to run the comparison to the start date. I therefore used the moment.js library to reformat it to what I needed. Similarly, I needed to format the start time and current value for end time using Moment to do the time comparison. What's nice is that Moment then has a method we can use that allows us to add 1 hour to the start time, before checking that the end time is either the same or after this point. โฑ
It took a while for me to get all this working, but it was mostly confusion around trying to figure out how best to get the format of the data consistent to allow for comparisons. Using Moment to check for time made this pretty effortless. ๐๐ป