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
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:
It only took a few minutes of tinkering after that to fix those issues by:
Now the custom validation code is cut down to 3 simple lines:
Thanks for the suggestions!
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:
Check this for more information about Foolproof.