1
votes

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:

enter image description here

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>
}
1

1 Answers

2
votes

I'm a little uncertain from your description which part isn't working, but there's two issues that I can see.

The first issue is pretty minor and might just be a transcription typo; you're using "datepicker" for the binding definition and "datePicker" in the markup data-bind. Binding names are case sensitive.

The second, primary, issue I think is simply that the date picker doesn't know you've updated the text in its element. Try something like this for your binding's update function:

update: function(element, valueAccessor) {
    //when the view model is updated, update the widget
    var value = ko.unwrap(valueAccessor());
    $(element).datepicker("update", new Date(value));
}

EDIT: Added snippet with working version of your binding

viewModel = function(data) {
  var self = this;
  self.Observation = {
    ObservationDateTime: ko.observable()
  };
}

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).datepicker("update", new Date(value));
  }
};

ko.applyBindings(new viewModel());
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.7.1/css/bootstrap-datepicker.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.7.1/js/bootstrap-datepicker.min.js"></script>


<input type="text" class="form-control" data-bind="datepicker : Observation.ObservationDateTime" style="width: 120px; margin:8px;" />
<div>Viewmodel date as text: <span data-bind="text: Observation.ObservationDateTime"></span></div>
<br/>
<span>Manual change: </span><input type="text" data-bind="textInput: Observation.ObservationDateTime" />