1
votes

I am using Knockout in combination with bootstrap-multiselect. I have three columns in an HTML table which allow you to specify a search item, search operator and search criteria. The search operator column actually holds three different elements: one multi-select, one single select and an input element; only one out of the three elements is actually displayed at any given time. Which element is displayed is controlled by the value of the search item column.

All select elements are initialized as bootstrap-multiselect. The search criteria column and the search operator column work without issue. The search item column however does not seem to be properly binding back to the native select which bootstrap-multiselect hides. The search item column will have the radio button selected next to the value you picked, however the text will display the first value from the select list. If I do not initialize the search item column as a bootstrap-multiselect, everything works as expected with the native select for the search-item column. The Knockout viewmodel is updated with the correct value as expected.

Below is my HTML declaration for the search criteria table:

<table id="search-criteria" class="table table-condensed hidden" data-bind="css: {hidden:SpecifiedCriteria().length == 0}">
    <thead>
    <tr>
        <th class ="search-item">
            Item
        </th>
        <th class="search-operator">
            Operator
        </th>
        <th class="search-criteria">
            Criteria
        </th>
    </tr>
    </thead>
    <tbody data-bind="foreach:SpecifiedCriteria">
    <tr>
        <td class="search-item">
            <select class="search-item" data-bind="searchDropDownList:SelectedItem, foreach: $root.CriteriaItems, value:SelectedItem">
                <optgroup data-bind="attr: {label: label}, foreach: children">
                    <option data-bind="text: DisplayName, option: $data"></option>
                </optgroup>
            </select>
        </td>
        <td class="search-operator">
            <select class="search-operator" data-bind="searchDropDownList:MatchCompareOperator, options:AvailableCompareOperators(), optionsValue: 'Value', optionsText: 'Key', value:MatchCompareOperator"></select>
        </td>
        <td class="search-criteria">
            <input type="text" class="search-criteria" data-bind="value:MatchValue" />
            <select class="search-criteria" data-bind="searchDropDownList:MatchValueListSelectedItemIds, options:MatchValueList, optionsValue:'Id', optionsText:'Description', selectedOptions:MatchValueListSelectedItemIds"></select>
            <select class="search-criteria" multiple="multiple" data-bind="searchDropDownList:MatchValueListSelectedItemIds, options:MatchValueList, optionsValue:'Id', optionsText:'Description', selectedOptions:MatchValueListSelectedItemIds"></select>
        </td>
    </tr>
    </tbody>
</table>

This is my view model, of which I new up an instance and pass into ko.applyBindings():

searchCriteria = function () {

    var comparisonOperators = Array(
        { "Key": "Starts With", "Value": 0 },
        { "Key": "Greater Than", "Value": 1 },
        { "Key": "Less Than", "Value": 2 },
        { "Key": "Greater Or Equal To", "Value": 3 },
        { "Key": "Less or Equal To", "Value": 4 },
        { "Key": "Not Equal To", "Value": 5 },
        { "Key": "Equal To", "Value": 6 },
        { "Key": "Contains", "Value": 7 },
        { "Key": "Does Not Contain", "Value": 8 });

    var criteriaDataType = {
        Alpha: 0,
        AlphaNumeric: 1,
        Integer: 2,
        Date: 3,
        DateTime: 4,
        Boolean: 5
    };

    //available styles constants
    var matchStyle = {
        InputBox: 0,
        DropDownList: 1,
        MultiSelectDropDownList: 2,
        DatePicker: 3
    };

    //available operator constants
    var matchCompareOperator = {
        StartsWith: 0,
        GreaterThan: 1,
        LessThan: 2,
        GreaterOrEqualTo: 3,
        LessOrEqualTo: 4,
        NotEqualTo: 5,
        EqualTo: 6,
        Contains: 7,
        DoesNotContain: 8
    };

    //determine which if the operator is eligible for the matchStyle
    function filterOperators(operator, specifiedMatchStyle, specifiedCriteriaDataType) {
        var eligible = false;

        switch (specifiedMatchStyle) {
            case matchStyle.MultiSelectDropDownList:
            case matchStyle.DropDownList:
                eligible = (operator.Value === matchCompareOperator.EqualTo || operator.Value === matchCompareOperator.NotEqualTo);
                break;
            case matchStyle.DatePicker:
                eligible = (operator.Value !== matchCompareOperator.StartsWith &&
                                   operator.Value !== matchCompareOperator.Contains &&
                                   operator.Value !== matchCompareOperator.DoesNotContain);
                break;
            case matchStyle.InputBox:
                if (specifiedCriteriaDataType !== criteriaDataType.Integer &&
                    specifiedCriteriaDataType !== criteriaDataType.Date &&
                    specifiedCriteriaDataType !== criteriaDataType.DateTime) {

                    if (operator.Value !== matchCompareOperator.GreaterThan &&
                        operator.Value !== matchCompareOperator.GreaterOrEqualTo &&
                        operator.Value !== matchCompareOperator.LessThan &&
                        operator.Value !== matchCompareOperator.LessOrEqualTo) {

                        eligible = true;
                    }

                } else {
                    if (operator.Value !== matchCompareOperator.StartsWith)
                        eligible = true;
                }

                break;
        }

        return eligible;
    }

    var searchCriteriaModel = function (criteriaItems, postUrl) {

        var modelObject = this;

        //setup the model elements
        modelObject.SpecifiedCriteria = ko.mapping.fromJS([]);
        modelObject.ComparisonOperators = comparisonOperators;

        var resultsDataTable = $("#search-results").dataTable();

        //create the criteria group items
        modelObject.CriteriaItems = [];

        var group = function (label, children) {
            this.label = ko.observable(label);
            this.children = ko.observableArray(children);
        }

        var groups = ko.utils.arrayMap(criteriaItems, function (item) { return item.DisplayGroupName; });
        groups = ko.utils.arrayGetDistinctValues(groups).sort();

        //filter the criteria items array based on the group and build the list of grouped items
        ko.utils.arrayForEach(groups, function (groupItemDisplayGroupName) {
            modelObject.CriteriaItems.push(
                new group(groupItemDisplayGroupName,
                    ko.utils.arrayFilter(criteriaItems, function (item) {
                        return (item.DisplayGroupName === groupItemDisplayGroupName);
                    })
                )
            );
        });

        var criteriaItem = function () {
            var itemObject = this;

            //setup the properties we are binding observables on
            itemObject.SelectedItem = ko.observable();
            itemObject.MatchValueColumnName = ko.observable();
            itemObject.MatchCompareOperator = ko.observable();
            itemObject.MatchValue = ko.observable();
            itemObject.DisplayName = ko.observable();
            itemObject.MatchStyle = ko.observable();
            itemObject.MatchValueList = ko.observableArray();
            itemObject.MatchValueListSelectedItemIds = ko.observableArray();
            itemObject.AvailableCompareOperators = ko.observableArray();

            //a computed which contains just the list of our selected items
            itemObject.MatchValueListSelectedItems = ko.computed(function () {
                return ko.utils.arrayFilter(itemObject.MatchValueList(), function (matchValueListItem) {
                    return ko.utils.arrayFirst(itemObject.MatchValueListSelectedItemIds(), function (selectedItemId) {
                        return (matchValueListItem.Id === selectedItemId);
                    });
                });
            });

            itemObject.SelectedItem.subscribe(function (newValue) {
                if (newValue === undefined || newValue === null)
                    return;

                //copy over all of the other attributes from the selected item to our item
                itemObject.DataIdColumnName = newValue.DataIdColumnName;
                itemObject.DataIdColumnValue = newValue.DataIdColumnValue;
                itemObject.DisplayGroupName = newValue.DisplayGroupName;
                itemObject.DisplayName(newValue.DisplayName);

                itemObject.MatchValueDataType = newValue.MatchValueDataType;
                itemObject.MatchValue(newValue.MatchValue);
                itemObject.MatchStyle(newValue.MatchStyle);
                itemObject.MatchValueColumnName = newValue.MatchValueColumnName;

                //initialize the compareoperator list
                itemObject.AvailableCompareOperators.removeAll();
                ko.utils.arrayPushAll(itemObject.AvailableCompareOperators,
                    ko.utils.arrayFilter(modelObject.ComparisonOperators, function (operator) { return filterOperators(operator, newValue.MatchStyle, newValue.MatchValueDataType) }));

                itemObject.MatchCompareOperator(newValue.MatchCompareOperator);

                //initialize the value list
                itemObject.MatchValueList.removeAll();
                if (newValue.MatchValueList !== null)
                    ko.utils.arrayPushAll(itemObject.MatchValueList, newValue.MatchValueList);

                itemObject.MatchValueListDisplayMember = newValue.MatchValueListDisplayMember;
                itemObject.MatchValueListSelectedItemIds.removeAll();
            });

        };

        //add a new item
        modelObject.addCriteriaItem = function () {
            var item = new criteriaItem();
            modelObject.SpecifiedCriteria.push(item);
        };

        //clear all items
        modelObject.clearCriteriaItems = function () {
            modelObject.SpecifiedCriteria.removeAll();
            $("#item-criteria-results").addClass("hidden");
            resultsDataTable.fnClearTable();
            modelObject.addCriteriaItem();
        };

        //seed with an empty row
        modelObject.addCriteriaItem();

        ko.bindingHandlers.option = {
            update: function (element, valueAccessor) {
                var value = ko.utils.unwrapObservable(valueAccessor());
                ko.selectExtensions.writeValue(element, value);
            }
        };

        //set up the ko search-multiselect binding for use with bootstrap multiselect
        ko.bindingHandlers.searchDropDownList = {
            update: function (element, valueAccessor, allBindings, viewMode, bindingContext) {

                if (element.nodeName.toLowerCase() !== "select")
                    return;

                //get the value and wire up the subscription
                var value = ko.utils.unwrapObservable(valueAccessor());
                if (value === null || value === undefined)
                    return;

                //cache the element so we don't have to look it up every time
                var triggerElement = $(element);
                var row = $(triggerElement).parents("tr");

                //initialize the multi-select list
                var searchItem = null;
                var operator = $(row).find("select.search-operator");

                //initialize the multi-selects if this is our first time through
                if (!triggerElement.hasClass("initialized")) {
                    if (triggerElement.hasClass("search-item")) {
                        //make sure we deselect any item
                        //triggerElement.prop("selectedIndex", -1);

                        triggerElement.multiselect({
                            buttonWidth: "100%",
                            maxHeight: 310,
                            enableFiltering: true,
                            enableCaseInsensitiveFiltering: true
                        });

                    } else {
                        triggerElement.multiselect({ buttonWidth: "100%" });
                    }

                    //flag the element as initialized
                    triggerElement.addClass("initialized");

                    //toggle off the operator and all of the criteria entry boxes
                    operator.siblings(".btn-group").addClass("hidden");
                    $(row).find("select.search-criteria").siblings(".btn-group").addClass("hidden");
                    $(row).find("input.search-criteria").addClass("hidden");
                    return;
                }

                //toggle the display of the operator and the search criteria
                if (triggerElement.hasClass("search-item")) {

                    if (operator.hasClass("initialized")) {
                        operator.multiselect("rebuild");
                        $(operator).siblings(".btn-group").first().removeClass("hidden");
                    }

                    //toggle off all of the criteria entry boxes
                    $(row).find("select.search-criteria").siblings(".btn-group").addClass("hidden");
                    $(row).find("input.search-criteria").addClass("hidden");

                    var searchCriteria = null;
                    if (value.MatchStyle === matchStyle.MultiSelectDropDownList)
                        searchCriteria = $(row).find("select[multiple='multiple'].search-criteria");
                    else if (value.MatchStyle === matchStyle.DropDownList)
                        searchCriteria = $(row).find("select:not([multiple]).search-criteria");
                    else
                        searchCriteria = $(row).find("input[type=text].search-criteria");

                    if (searchCriteria != null) {
                        if (searchCriteria.hasClass("initialized")) {
                            //we are a select
                            searchCriteria.multiselect("rebuild");
                            searchCriteria.next(".btn-group").removeClass("hidden");
                        } else {
                            searchCriteria.removeClass("hidden"); //we are an inputbox
                        }
                    }
                }
            }

        };
    }

    return {
        criteriaDataType: criteriaDataType,
        matchStyle: matchStyle,
        matchCompareOperator: matchCompareOperator,
        SearchCriteriaModel: searchCriteriaModel
    }
}();

Just to recap: if I do not wire up the search-item column as a bootstrap-multiselect, everything works. If I do wire it up, the column will display the first value in the list. Setting a breakpoint in the searchDropDownList binding handler will always show the value gotten via the valueAccessor to be the first item in the list for the given search-item element.

I am wits end and cannot understand or explain this behavior for the searc-item column, especially since the other two columns seeming work without issue.

1

1 Answers

0
votes

You can attach a 'change' event to the drop down list and set the ko object. Let's suppose that '#example' is your drop down list:

  var $example = $('#example');
  $example.multiselect();
  $example.on('change', function() {
      //set your ko object with the selected value
      itemObject.SelectedItem($example.val());
  });