Avatar

๐Ÿ‘‹๐Ÿป, I'm Julia.

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. ๐Ÿ™Œ๐Ÿป

ยฉ 2016-2024 Julia Tan ยท Powered by Next JS.