skip to Main Content

I’m trying to use a popup modal to create an event in a calendar, and I’m having trouble getting jQuery validation to work on the start time and end time fields for the event.
I can’t figure out how to validate the two fields against one another so that end time will not be valid if it’s before start time.

It seems to work partially on the default dates displayed the first time the form pops up, but if I change the start date it stops working. On the other hand, trying to validate the time part of the date never worked.

View model:

public class EventViewModel
{
    public int Id { get; set; }

    public string Title { get; set; }

    [Required]
    [DataType(DataType.DateTime)]
    [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:yyyy-MM-dd HH:mm}")]
    public DateTime Start { get; set; }

    [Required]
    [DataType(DataType.DateTime)]
    [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:yyyy-MM-dd HH:mm}")]
    [DateGreaterThan("Start", ErrorMessage = "End date has to be later than start date")]
    public DateTime End { get; set; }

    public string Color { get; set; }

    public bool AllDay { get; set; }

    [Required]
    public string StudentId { get; set; }

    [Required]
    public int LessonTypeId { get; set; }
}  

Controller:

public async Task<IActionResult> Index()
    {
        EventViewModel Event = new EventViewModel { Start = DateTime.Now, End = DateTime.Now.AddMinutes(30) };
        var user = await GetCurrentUserAsync();
        var studentList = await GetTeacherStudentsAsync(user);
        ViewData["StudentList"] = new SelectList(studentList, "Id", "FirstName");
        var lessonTypeList = _context.LessonTypes.Where(l => l.TeacherId.Equals(user.Id));
        ViewData["LessonTypeList"] = new SelectList(lessonTypeList, "LessonTypeId", "Description");
        return View(Event);
    }

Index View:

<head>
<script
  src="https://code.jquery.com/jquery-3.2.1.min.js"
  integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
  crossorigin="anonymous"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/css/tether.min.css" />
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" />
  <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.css" />
  <script type='text/javascript' src="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/gcal.js"></script>
  <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.4/jquery.datetimepicker.css" />
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.4/build/jquery.datetimepicker.full.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.6/js/bootstrap.min.js"></script>
  <script type="text/javascript">
    $(document).ready(function() {

      $('#calendar').fullCalendar({
        customButtons: {
          createButton: {
            text: "new event",
            click: function() {
              $('#createModal').modal('show');
            }
          }
        },
        header: {
          left: 'prev,next today createButton',
          center: 'title',
          right: 'month,agendaWeek,agendaDay,listWeek'
        },
        defaultView: "month",
        allDaySlot: false,
        eventLimit: true,
        editable: true,
        navLinks: true,
        events: "/Calendar/GetEvents",
        eventDrop: function(event, delta, revertFunc) {

          alert(event.title + " was dropped on " + event.start.format());

          if (confirm("Are you sure you want to make this change?")) {
            SaveEvent(event);
          } else {
            revertFunc();
          }

        },
        eventResize: function(event, delta, revertFunc) {

          alert(event.title + " is now from " + event.start.format() + " to " + event.end.format());

          if (confirm("Are you sure you want to make this change?")) {
            SaveEvent(event);
          } else {
            revertFunc();
          }

        }
      });

      $.validator.addMethod("laterThan", function(value, element, params) {
        var start = params.split(" ");
        var startDate = new Date(start[0]);
        var startTime = start[1].split(":");
        var end = value.split(" ");
        var endDate = new Date(end[0]);
        var endTime = end[1].split(":");
        if (startDate == endDate) {
          if (parseInt(startTime[0], 10) == parseInt(endTime[0], 10)) {
            return parseInt(startTime[1], 10) > parseInt(endTime[1], 10);
          } else if (parseInt(startTime[0], 10) < parseInt(endTime[0], 10)) return true;
          else return false;
        }
        return this.optional(element) || startDate < endDate;
      }, "End time must be later than start time");

      var validator = $("#createForm").validate({
        rules: {
          Start: "required",
          End: {
            required: true,
            laterThan: $("#Start").val(),
          }
        }
      });

      $(function() {
        $("#Start").datetimepicker({
          format: "Y-m-d H:i",
          onChangeDateTime: function(ct) {
            $(this).valid();
          }
        });
        $("#End").datetimepicker({
          format: "Y-m-d H:i",
          onShow: function(ct) {
            var start = $("#Start").val().split(" ");
            this.setOptions({
              minDate: start[0]
            });
          },
          onChangeDateTime: function(ct) {
            $(this).valid();
          }
        });
      });
    });


    function SaveEvent(Event) {

      var dataRow = {
        Id: Event.id,
        Start: Event.start,
        End: Event.end
      }

      $.ajax({
        method: 'POST',
        url: '@Url.Action("SaveEvent", "Calendar")',
        dataType: "json",
        contentType: "application/json",
        data: JSON.stringify(dataRow),
        error: function(result) {
          alert("Something went wrong... Event Id was: " + Event.id + ", Start Time was: " + Event.start.format());
        }
      });
    }

    function CreateEvent() {

      var valid = validator.form();
    }

    function DeleteEvent(Event) {

      var dataRow = {
        Id: Event.id
      }

      $.ajax({
        method: 'POST',
        url: '@Url.Action("DeleteEvent", "Calendar")',
        dataType: "json",
        contentType: "application/json",
        data: JSON.stringify(dataRow),
        error: function(result) {
          alert("Something went wrong... Event Id was: " + Event.id)
        }
      })

    }
  </script>
</head>

<body>
  <div id='calendar'></div>

  <div class="modal fade" id="createModal" tabindex="-1" role="dialog" aria-labelledby="createModalLabel" aria-hidden="true">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <h4 class="modal-title" id="createModalLabel">Create New Event</h4>
          <button type="button" class="close" data-dismiss="modal">×</button>
        </div>
        <div class="modal-body">
          <form id="createForm">
            <div class="form-group">
              <label for="StudentId" class="col-md-2 form-control-label">Student</label>
              <div class="col-md-10">
                <select asp-for="StudentId" asp-items="ViewBag.StudentList" class="form-control"></select>
              </div>
            </div>
            <div class="form-group">
              <label for="LessonTypeId" class="col-md-3 form-control-label">Lesson Type</label>
              <div class="col-md-9">
                <select asp-for="LessonTypeId" asp-items="ViewBag.LessonTypeList" class="form-control"></select>
              </div>
            </div>
            <div class="form-group">
              <label for="Start" class="col-md-2 form-control-label">Start</label>
              <div class="col-md-10">
                <input asp-for="Start" class="form-control" id="Start" name="Start" />
              </div>
            </div>
            <div class="form-group">
              <label for="End" class="col-md-2 form-control-label">End</label>
              <div class="col-md-10">
                <input asp-for="End" class="form-control" id="End" name="End" />
              </div>
            </div>
          </form>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-primary" id="createButton" onclick="CreateEvent()">Create</button>
          <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
        </div>
      </div>
    </div>
  </div>
</body>

Any suggestions would be greatly appreciated!

Edit (20/08/2017) –
I tried incorporating custom validation using Microsoft’s guides, but that does not seem to work either (one data attribute added to view model as well).

Validation Class:

public class DateGreaterThan : ValidationAttribute, IClientModelValidator
{
    private readonly string _earlierDate;

    public DateGreaterThan (string earlierDate)
    {
        _earlierDate = earlierDate;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        ErrorMessage = ErrorMessageString;

        var lateDate = (DateTime)value;
        var otherProperty = validationContext.ObjectType.GetProperty(_earlierDate);
        if (otherProperty == null)
            throw new ArgumentException("Property with this name could not be found");

        var earlyDate = (DateTime)otherProperty.GetValue(validationContext.ObjectInstance);

        if (lateDate <= earlyDate)
            return new ValidationResult(ErrorMessage);

        return ValidationResult.Success;
    }

    public void AddValidation(ClientModelValidationContext context)
    {
        var error = FormatErrorMessage(context.ModelMetadata.GetDisplayName());
        context.Attributes.Add("data-val", "true");
        context.Attributes.Add("data-val-error", error);
    }
}

2

Answers


  1. Chosen as BEST ANSWER

    So after a few weeks of testing different options and breaking my head over this, I realized that the validation I had been trying to implement had the wrong logic for several reasons:

    1. Because of how I built the custom jQuery validation, the value of the start date being compared was only passed to the validation once - when the page loaded. Therefore, it did not change when the user changed the input for the start field (so it stayed at either default or null).
    2. I completely ignored the face that date objects store time as well - as I was trying to do the time comparison manually.

    It only took a few minutes of tinkering after that to fix those issues by:

    1. Instead of passing parameters to the custom validation, feeding it the start date directly from the form using jQuery.
    2. Getting rid of all of the manual hour comparison code and doing a simple date comparison.

    Now the custom validation code is cut down to 3 simple lines:

            $.validator.addMethod("laterThan", function (value, element) {
                var startDate = new Date($("#Start").val());
                var endDate = new Date(value);
    
                return this.optional(element) || startDate < endDate;
            }, "End time must be later than start time");  
    

    Thanks for the suggestions!


  2. The best choice for your cases I think is ‘MVC Foolproof Validation’. You can use ‘GreaterThan’ or ‘LessThan’ data annotaions for your cases. So you can have something like this in you annotaion in your ‘End’ property:

    [GreaterThan("Start", ErrorMessage = "Expiry Date must be greater than Activation Date", PassOnNull = true)]
    

    Check this for more information about Foolproof.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search