我将Knockout与bootstrap-multiselect结合使用。我在一个HTML表中有三列,允许您指定搜索项、搜索操作符和搜索条件。搜索操作符列实际上包含三个不同的元素:一个多选择、一个单选择和一个输入元素;在任何给定时间,实际上只显示三个元素中的一个。显示哪个元素由搜索项列的值控制。
所有select元素初始化为bootstrap-multiselect。搜索条件列和搜索操作符列可以正常工作。然而,搜索项列似乎没有正确地绑定回引导多选隐藏的本机选择。在搜索项列中,选中的值旁边有一个单选按钮,但是文本将显示选择列表中的第一个值。如果我没有将搜索项列初始化为引导多选,那么对于搜索项列的本机选择,一切都按照预期工作。Knockout视图模型按照预期使用正确的值进行更新。
下面是搜索条件表的HTML声明:
<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>
这是我的视图模型,我新建了一个实例并传递给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
}
}();
重述一下:如果我没有将search-item
列连接为引导多选,那么一切都可以工作。如果我将它连接起来,列将显示列表中的第一个值。在searchDropDownList
绑定处理程序中设置断点将始终显示通过valueAccessor获得的值是给定search-item
元素列表中的第一项。
我很聪明,无法理解或解释searc-item
专栏的这种行为,特别是因为其他两个专栏似乎没有问题。
您可以将'change'事件附加到下拉列表并设置ko对象。让我们假设'#example'是你的下拉列表:
var $example = $('#example');
$example.multiselect();
$example.on('change', function() {
//set your ko object with the selected value
itemObject.SelectedItem($example.val());
});