1

I am having the following select in my html:

<select class="form-control" data-bind="multiSelectCommaSeparated: CityIds, options: $root.Cities, optionsText: 'CityName', optionsValue: 'CityId', valueUpdate: 'change'" multiple="true"></select>

I am trying to write a knockout custom binding to update the CityIds observable with comma separated values whenever user selects multiple cities in the multi select drop down. For example if I have the following select:

<select id="cities" multiple="multiple">
    <option value="1">City 1</option>
    <option value="2">City 2</option>
    <option value="3">City 3</option>
    <option value="4">City 4</option>
</select>

and the user selects first 3 cities, then my observable should have 1,2,3. If the user de-selects City 3, then the observable should have 1,2 and this should happen as soon soon as the user selects/de-selects any value in the select.

I have written the following custom binding by using reference from this question:

ko.bindingHandlers.multiSelectCommaSeparated = {
    init: function (element, valueAccessor) {
        var selMulti = $.map($(element.id + " option:selected"), function (el, i) {
            return $(el).text();
        });
        valueAccessor(selMulti);
    },
    update: function (element, valueAccessor) {
        var selMulti = $.map($(element.id + " option:selected"), function (el, i) {
            return $(el).text();
        });
        valueAccessor(selMulti);
    }
}

In the above custom binding, the update event is not firing when I change my selection in the multi select dropdown. What should I change in the custom binding to achieve my requirement?

Community
  • 1
  • 1
seadrag0n
  • 848
  • 1
  • 16
  • 40

1 Answers1

6

I'm not quite straight up answering your question, someone else (or even I) may do so in a separate answer. Instead, let me propose an alternative way to handle this, which is IMHO better suited to Knockout's MVVM style.

Construct your view model to hold the CSV string you want as a computed observable. For example:

var ViewModel = function() {
    var self = this;

    self.Cities = [
        {CityId: 1, CityName: "City 1"},
        {CityId: 2, CityName: "City 2"},
        {CityId: 3, CityName: "City 3"},
        {CityId: 4, CityName: "City 4"}
    ];

    self.SelectedCities = ko.observableArray([]);

    self.SelectedCitiesCsv = ko.computed(function(){
        return self.SelectedCities().join(",");
    });
};

You can test this with this View:

<select class="form-control" 
        data-bind="selectedOptions: SelectedCities,
                   options: $root.Cities, 
                   optionsText: 'CityName',
                   optionsValue: 'CityId',
                   valueUpdate: 'change'"
        multiple="true"></select>

<hr />

CSV: <p data-bind="text: SelectedCitiesCsv"></p>

See this fiddle for a demo.

The advantages of this approach over a custom binding include:

  • Unit testable;
  • No custom bindings needed;
  • Knockout keeps our CSV string in synch with the View, no custom code needed;
  • No dependency on jQuery needed, if possible leverage KO to keep your JS ignorant of the actual DOM (which again improves testability);

In the case where you're not using constructor functions for your viewmodels, but plain javascript objects, you have to add computed observables after creating the object with the base properties. Something like this:

var viewModel =  {
    Cities :[
        {CityId: 1, CityName: "City 1"},
        {CityId: 2, CityName: "City 2"},
        {CityId: 3, CityName: "City 3"},
        {CityId: 4, CityName: "City 4"}
    ],

    SelectedCities : ko.observableArray([])
};

viewModel.SelectedCitiesCsv = ko.computed(function(){
    return viewModel.SelectedCities().join(",");
});

Or see this modified fiddle.

Jeroen
  • 60,696
  • 40
  • 206
  • 339
  • I am trying to use this approach in my view model, but I am facing one problem. I have modified your fiddle to demonstrate this problem: [modified fiddle](http://jsfiddle.net/39KA8/). I have written my entire view model in this format, so is there some other way to write `computed` observables using this syntax? – seadrag0n Jun 23 '14 at 10:24
  • 1
    Write it outside the vm: `ViewModel.SelectedCitiesCsv = ko.computed(function(){ return ViewModel.SelectedCities().join(","); });` – GôTô Jun 23 '14 at 12:05
  • 1
    Though I'd recommend using constructor function based viewmodels as in my first example. you can use @GôTô's suggestion and add computed observables to plain javascript objects as well, if you add them in a second step. See my answer modifications for tips on this. – Jeroen Jun 23 '14 at 12:11
  • I totally agree using function based viewmodels – GôTô Jun 23 '14 at 12:13
  • @Jeroen the modified fiddle is working. I just have one question, which style of knockout ViewModel declaration is better, `function based` based or `plain JS objects` like I am using? If I use `function` based view model then how can I refer to its observables outside of the view model? A link to some kind of article on this will be very helpful :) – seadrag0n Jun 23 '14 at 12:48
  • You can only access constructed view models if you construct them in the correct scope. The difference in a Knockout-specific-context is [explained on Stack Overflow](http://stackoverflow.com/q/9589419/419956). It can also help to read about closures [on SO](http://stackoverflow.com/questions/111102/how-do-javascript-closures-work) or the [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures). – Jeroen Jun 23 '14 at 14:11