knockoutjs complex array databind in multiple select listbox

5.3k views Asked by At

I have found sample in the knockoutjs website. here they are binding values to the multiple select list box. But they are using very simple observable array. availableCountries and chosenCountries.

<p>
    Choose some countries you'd like to visit:
    <select data-bind="options: availableCountries, selectedOptions: chosenCountries" size="5" multiple="true"></select>
</p>

<script type="text/javascript">
    var viewModel = {
        availableCountries : ko.observableArray(['France', 'Germany', 'Spain']),
        chosenCountries : ko.observableArray(['Germany']) // Initially, only Germany is selected
    };

    // ... then later ...
    viewModel.chosenCountries.push('France'); // Now France is selected too
</script>

But My model is too complex and mentioned the part of my model as json format.

"JobOrderDelivTranscript" : [{
        "TranscriptType" : {
            "Id" : 1,
            "Name" : null,
            "CreatedBy" : 0,
            "CreatedDate" : "0001-01-01T00:00:00",
            "ModifiedBy" : 0,
            "ModifiedDate" : "0001-01-01T00:00:00",
            "IsActive" : false,
            "EntityStatus" : 0,
            "ErrorMessage" : null,
            "ExternalID" : 0,
            "ExternalSystemID" : 0
        },
        "Id" : 1,
        "Name" : null,
        "CreatedBy" : 0,
        "CreatedDate" : "0001-01-01T00:00:00",
        "ModifiedBy" : 0,
        "ModifiedDate" : "0001-01-01T00:00:00",
        "IsActive" : false,
        "EntityStatus" : 0,
        "ErrorMessage" : null,
        "ExternalID" : 0,
        "ExternalSystemID" : 0
    }, {
        "TranscriptType" : {
            "Id" : 2,
            "Name" : null,
            "CreatedBy" : 0,
            "CreatedDate" : "0001-01-01T00:00:00",
            "ModifiedBy" : 0,
            "ModifiedDate" : "0001-01-01T00:00:00",
            "IsActive" : false,
            "EntityStatus" : 0,
            "ErrorMessage" : null,
            "ExternalID" : 0,
            "ExternalSystemID" : 0
        },
        "Id" : 2,
        "Name" : null,
        "CreatedBy" : 0,
        "CreatedDate" : "0001-01-01T00:00:00",
        "ModifiedBy" : 0,
        "ModifiedDate" : "0001-01-01T00:00:00",
        "IsActive" : false,
        "EntityStatus" : 0,
        "ErrorMessage" : null,
        "ExternalID" : 0,
        "ExternalSystemID" : 0
    }
]

here my "chosenCountries" will be JobOrderDelivTranscript(). if i am selecting first option it should be mapped with JobOrderDelivTranscript()[0].TranscriptType.Id. in their example they are using string array but i have to bind with complex data. How can I do that.

Even I tried with custom bindings

ko.bindingHandlers['selectedCustomOptions'] = {
            getSelectedValuesFromSelectNode: function (selectNode) {
                var result = [];
                var nodes = selectNode.childNodes;
                for (var i = 0, j = nodes.length; i < j; i++) {
                    var node = nodes[i], tagName = ko.utils.tagNameLower(node);
                    if (tagName == "option" && node.selected)
                        result.push(ko.selectExtensions.readValue(node));
                    else if (tagName == "optgroup") {
                        var selectedValuesFromOptGroup = ko.bindingHandlers['selectedCustomOptions'].getSelectedValuesFromSelectNode(node);
                        Array.prototype.splice.apply(result, [result.length, 0].concat(selectedValuesFromOptGroup)); // Add new entries to existing 'result' instance
                    }
                }
                return result;
            },
            'init': function (element, valueAccessor, allBindingsAccessor) {
                ko.utils.registerEventHandler(element, "change", function () {
                    var value = valueAccessor();
                    var valueToWrite = ko.bindingHandlers['selectedCustomOptions'].getSelectedValuesFromSelectNode(this);
                    ko.jsonExpressionRewriting.writeValueToProperty(value, allBindingsAccessor, 'value', valueToWrite);
                });
            },
            'update': function (element, valueAccessor) {
                if (ko.utils.tagNameLower(element) != "select")
                    throw new Error("values binding applies only to SELECT elements");

                var newValue = ko.utils.unwrapObservable(valueAccessor());
                if (newValue && typeof newValue.length == "number") {
                    var nodes = element.childNodes;
                    for (var i = 0, j = nodes.length; i < j; i++) {
                        var node = nodes[i];
                        if (ko.utils.tagNameLower(node) === "option")
                            ko.utils.setOptionNodeSelectionState(node, arrayIndexOf(newValue, ko.selectExtensions.readValue(node)) >= 0);
                    }
                }
            }
        };

        function arrayIndexOf (array, item) {
            if (typeof Array.prototype.indexOf == "function")
                return Array.prototype.indexOf.call(array, item);
            for (var i = 0, j = array.length; i < j; i++)
                if (array[i].TranscriptType.Id() === item.Id)
                    return i;
            return -1;
        }

I have made the options get selected but json data was not getting updated.

is there any simple way?

Thanks in advance.

1

There are 1 answers

3
madcapnmckay On

I'm not 100% I understand your question but it seems you are trying to bind to a complex object with the selectedOptions binding. There are two ways to do what you want. The first is to use the optionsValue binding combined with a computed to pull your your binding id down to the object root level (Unfortunately the optionsValue binding only works at the root so optionsValue: 'TranscriptType.Id' doesn't work).

<p>Choose some countries you'd like to visit:</p>
<select data-bind="options: availableCountries, optionsText: optionsText, 
      optionsValue: 'id', selectedOptions: 
      chosenCountries" size="5" multiple="true"></select>

<p data-bind="text: ko.toJSON(chosenCountries)">
</p>

var JobOrderDelivTranscript = function(id) {
    var self = this;
    this.TranscriptType = {
        Id : id
    }
    this.id = ko.computed(function() {
        return self.TranscriptType.Id
    });
};

http://jsfiddle.net/madcapnmckay/6K6kH/

The second way is to not use the optionsValue, in which case KO will use the object references to test equality. As long as you keep the same object references in your chosenCounties array everything will work.

var viewModel = function () {
    var self = this;
    this.availableCountries = ko.observableArray([
        new JobOrderDelivTranscript("Some Transcript 1"),
        new JobOrderDelivTranscript("Some Transcript 2"),
        new JobOrderDelivTranscript("Some Transcript 3")]);

    this.chosenCountries = ko.observableArray([ self.availableCountries()[0] ]);

    this.optionsText = function(option) {
        return option.TranscriptType.Id;
    };        
};

var vm = new viewModel();
vm.chosenCountries.push(vm.availableCountries()[1]);

http://jsfiddle.net/madcapnmckay/hmsqf/

Both ways have advantages and disadvantages, it depends on your particular situation which is the correct fit.

EDIT

To do the same thing with the mapping plugin you will need to use mapping Options which are covered in the docs here under "Advanced Usage".

Here is an example which you should be able to adapt.

http://jsfiddle.net/madcapnmckay/hmsqf/2/

In order to structure your code better I would recommend creating many javascript classes as I do in the example with Order this allows logic to be contained in descrete blocks. I would also not recommend using old jquery selectors, many beginners with KO think that it's ok to mix the two. In my opinion it's diluting the separation of concerns between your view model and the view. Why use a $(selector).click when you can use a click binding.

Hope this helps.