I'm trying without any success to bind the Bootstrap datepicker control
<input class="form-control" data-bind="datePicker : Observation.ObservationDateTime" type="date" />
to a Knockout.js viewmodel. (This is an ASP.NET Core project.) There are so many moving parts involved in this that I cannot isolate the problem in this specific case. What I want to achieve is a simple two-way binding: the date from the viewmodel is populated in the datepicker control; and the date on form submission is sent back to the viewmodel/controller.
I can verify that the date is successfully passed to the Knockout viewmodel. To keep track of its value I am currently printing it as text on the page:
<div>Viewmodel date as text: <span data-bind="text:Observation.ObservationDateTime"></span></div>
It renders like this:
My research indicates that the setup in the (1) domain model, (2) server side viewmodel, (3) client side (Knockout) viewmodel and the (4) view itself, all matter. So I have pasted the relevant code from each below:
Domain model:
[Required]
//[Display(Name = "Date/Time")]
//[DisplayFormat(DataFormatString = "{0:dddd, dd/MM/yyyy HH:mm}", ApplyFormatInEditMode = true)]
[DataType(DataType.Date)]
public DateTime ObservationDateTime { get; set; }
Server viewmodel/controller:
Observation = new Observation() { ObservationDateTime = _systemClock.Now },
Client Knockout.js viewmodel
I have tried various bindingHandlers. Currently (see bit starting 'ko.bindingHandlers.datepicker':
CreateObservationViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, observedSpeciesMapping, self);
ko.bindingHandlers.selectPicker = {
init: function (element, valueAccessor, allBindings) {
$(element).selectpicker('render');
}
};
ko.bindingHandlers.datepicker = {
init: function (element, valueAccessor, allBindingsAccessor) {
//initialize datepicker with some optional options
var options = allBindingsAccessor().datepickerOptions || {};
$(element).datepicker(options);
//when a user changes the date, update the view model
ko.utils.registerEventHandler(element, "changeDate", function (event) {
var value = valueAccessor();
if (ko.isObservable(value)) {
value(event.date);
}
});
},
update: function (element, valueAccessor) {
var widget = $(element).data("datepicker");
//when the view model is updated, update the widget
if (widget) {
widget.date = ko.utils.unwrapObservable(valueAccessor());
if (widget.date) {
widget.setValue();
}
}
}
};
Razor view datepicker Bootstrap control:
<div class="form-group">
<label class="control-label" for="Observation.ObservationDateTime">Date:</label>
<input class="form-control" data-bind="datePicker : Observation.ObservationDateTime" type="date" />
<div>Viewmodel date as text: <span data-bind="text: Observation.ObservationDateTime"></span></div>
</div>
I think the problem is with the datepicker bindingHandler in the Knockout viewmodel. However, despite hours of tinkering, I have not solved it. Any ideas or pointers?
Update Entire viewmodel
CreateObservationViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, observedSpeciesMapping, self);
ko.bindingHandlers.selectPicker = {
init: function (element, valueAccessor, allBindings) {
$(element).selectpicker('render');
}
};
ko.bindingHandlers.datepicker = {
init: function (element, valueAccessor, allBindingsAccessor) {
//initialize datepicker with some optional options
var options = allBindingsAccessor().datepickerOptions || {};
$(element).datepicker(options);
//when a user changes the date, update the view model
ko.utils.registerEventHandler(element, "changeDate", function (event) {
var value = valueAccessor();
if (ko.isObservable(value)) {
value(event.date);
}
});
},
update: function (element, valueAccessor) {
//when the view model is updated, update the widget
var value = ko.unwrap(valueAccessor());
$(element).val(value).datepicker("update");
}
};
self.addObservedSpecies = function () {
var observedSpecies = new ObservedSpeciesViewModel({ Id: 0, BirdId: 0, Quantity: 1 });
self.ObservedSpecies.push(observedSpecies);
};
self.removeObservedSpecies = function () {
if (self.ObservedSpecies().length > 1)
self.ObservedSpecies.pop();
};
self.disableSubmitButton = ko.observable(false);
self.Total = ko.computed(function () {
var total = 0;
total += self.ObservedSpecies().length;
return total;
}),
self.post = function () {
self.disableSubmitButton(true);
if (self.ObservedSpecies().length < 1) {
// ToDo: Implement proper client-side validation of the Observed Species collection
alert("You must choose at least one observed bird species");
self.MessageToClient("You must choose at least one observed bird species...");
self.disableSubmitButton(false);
return;
}
$.ajax({
url: "/Observation/Post/",
type: "POST",
data: ko.toJSON(self),
headers:
{
"content-type": "application/json; charset=utf-8"
},
success: function (data) {
var obj = JSON.parse(data);
if (obj.IsModelStateValid === false) {
self.MessageToClient(obj.MessageToClient);
}
else {
window.location.replace("./Index/");
}
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
self.disableSubmitButton(false);
if (XMLHttpRequest.status === 400) {
$('#MessageToClient').text(XMLHttpRequest.responseText);
}
else {
$('#MessageToClient').text('The web server had an error. The issue has been logged for investigation by the developer.');
}
}
});
};
};
var observedSpeciesMapping = {
'ObservedSpecies': {
key: function (obsevedSpecies) {
return ko.utils.unwrapObservable(obsevedSpecies.Id);
},
create: function (options) {
return new CreateObservationViewModel(options.data);
}
}
};
ObservedSpeciesViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, observedSpeciesMapping, self);
};
Razor view snippet
@section scripts{
<script src="~/js/knockout-3.4.2.js"></script>
<script src="~/js/knockout.mapping-latest.js"></script>
<script src="~/js/jqueryvalidate.js"></script>
<script src="~/js/jquery-validate.bootstrap-tooltip.js"></script>
<script src="~/js/createobservationviewmodel.js"></script>
<script type="text/javascript">
var createObservationViewModel = new CreateObservationViewModel(@Html.Raw(data));
ko.applyBindings(createObservationViewModel);
</script>
}
