Introducing Custom Validations for Start-End DateTime scenarios on Open Event Frontend

Several modules of Open Event Frontend involve start and end date-times. While for simple type fields like text, dropdowns or radio buttons, default semantic UI validations are available, which are used inside the app via the form mixin. 

Any component which extends the form mixin can make specify the validations required using the getValidationRules hook.

For instance, this set of rules will enforce validations on the field called ticket_price which will prohibit it from being left empty, or something other than a real number.

getValidationRules() {
 ticketPrice: {
          identifier : 'ticket_price',
          rules      : [
            {
              type   : 'empty',
              prompt : this.l10n.t('Please give your ticket a price')
            },
            {
              type   : 'number',
              prompt : this.l10n.t('Please give a proper price for you ticket')
            },
            {
              type   : 'decimal[0..]',
              prompt : this.l10n.t('Ticket price should be greater than 0')
            }
          ]
        },
}

The validations provided by semantic UI only extend to single fields, and are independent of each other. However, in our use case we have four fields:

  1. Start date
  2. Start time
  3. End date
  4. End time

The general requirement is that the DateTime object formed by joining the start date and the start time should be before the DateTime object obtained by joining the end date and time. Also, these four distributed fields exist only on the frontend, on the server they are actually just two fields startsAt and endsAt each carrying UTC time values of DateTime objects. To split them into date and time a new computed function is created which is invoked in the model definitions of various resources. Consider the two following complex fields defined in the model according to the schema on the server.

 startsAt               : attr('moment'),
 endsAt                 : attr('moment')

In order to split them into two that helper is used as follows:

startsAtDate : computedDateTimeSplit.bind(this)('startsAt', 'date', 'endsAt'),
startsAtTime : computedDateTimeSplit.bind(this)('startsAt', 'time', 'endsAt'),
endsAtDate   : computedDateTimeSplit.bind(this)('endsAt', 'date')
  endsAtTime   : computedDateTimeSplit.bind(this)('endsAt', 'time'),

These values can then be used inside individual fields. To enhance the user experience jquery Calendar module is used to allow the user to enter the date and time values using a calendar and time picker as shown below.

The computedDateTimeSplit helper, takes in the property whose part it is splitting, along with the specification of the part it will split. It also takes an optional endProperty argument, which is passed if it is being called for a start property. This function returns a pair of getters and setters,the get function returns the part of datetime object requested like, date or time where as the setter sets these values each time this function is called.

export const computedDateTimeSplit = function(property, segmentFormat, endProperty) {
  return computed(property, {
    get() {
      return moment(this.get(property)).format(getFormat(segmentFormat));
    },
    set(key, value) {
      const newDate = moment(value, getFormat(segmentFormat));
      let oldDate = newDate;

      if (segmentFormat === 'time') {
        oldDate.hour(newDate.hour());
        oldDate.minute(newDate.minute());
      } else if (segmentFormat === 'date') {
        oldDate.date(newDate.date());
        oldDate.month(newDate.month());
        oldDate.year(newDate.year());
      } else {
        oldDate = newDate;
      }
      this.set(property, oldDate);
      }
      return value;
    }
  });
};
 

With this complex set up it is not possible to use semantic UI validations, hence we extend the default semantic UI validations, by introducing  custom rules for the form.

A rule called checkDates is introduced inside the getValidationRules hook.

  getValidationRules() {
    window.$.fn.form.settings.rules.checkDates = () => {
      let startDatetime = moment(this.get('data.speakersCall.startsAt'));
      let endDatetime = moment(this.get('data.speakersCall.endsAt'));
      return (endDatetime.diff(startDatetime, 'minutes') > 0);
    };
}

This hook manually compares the end and start datetime objects, and if they are in violation, it returns false, which triggers a semantic UI styled validation.

To enforce the validation, the start and end datetime fields are embedded with 

startDate: {
            identifier : 'start_date',
            rules      : [
              {
                type   : 'empty',
                prompt : this.l10n.t('Please tell us when your event starts')
              },
              {
                type   : 'checkDates',
                prompt : this.l10n.t('Start date & time should be after End date and time ')
              }
            ]
          },
          endDate: {
            identifier : 'end_date',
            rules      : [
              {
                type   : 'empty',
                prompt : this.l10n.t('Please tell us when your event ends')
              },
              {
                type   : 'checkDates',
                prompt : this.l10n.t('Start date & time should be after End date and time')
              }
            ]
          },
          startTime: {
            identifier : 'start_time',
            depends    : 'start_date',
            rules      : [
              {
                type   : 'empty',
                prompt : this.l10n.t('Please give a start time')
              },
              {
                type   : 'checkDates',
                prompt : '.'
              }
            ]
          },
          endTime: {
            identifier : 'end_time',
            rules      : [
              {
                type   : 'empty',
                prompt : this.l10n.t('Please give an end time')
              },
              {
                type   : 'checkDates',
                prompt : '.'
              }
            ]

However one big problem with this approach is the clearing of validation messages, when one field is in violation, technically all four are in violation as you can either adjust the start time or end time to make the datetimes valid. However, semantic UI validations which operate upon the blur listening property operate with respect to a single field at a time, and any changes to a particular field does not retrigger validations on others. So, for instance an end date was added which was occurring on the same date as a start date, and times are the same too, the form will throw validation on all four fields. However, if we just change the time of one of the dates, the dates will be valid again, but the validation error dialogs of only that field will disappear which was actually modified. This default behavior of semantic UI problematic for us. 

onChange() {
      this.onValid(() => {});
    }

Thus an onChange action is attached with the focus-out event of the date and time inputs

{{input type='text' value=value placeholder=placeholder name=name focus-out=(action 'onChange')}}

This action triggers the validations on the entire form, which will clear at any redundant validation error prompts still remaining in the form. As a future improvement, instead of onChange triggering validations on the entire form, only the datetimes can be revalidated, though this will require extension of the form mixin to support more flexible methods to validate forms using latest semantic UI APIs.

Resources 

CosmicCoder96

GSOC 17 @ FOSSASIA | Full Stack Developer | Swimmer

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.