Reputation: 867
Hi All,
I am using Knockoutjs in conjuction with Jquery UI widgets to display a auto-complete box with multiple spans for each selected item.
I am following below approach
1) In the viewmodel have an observable array (selecteditems) and bind it to a declarative template to show SPANs
2) An input box bound to JQUERY UI autocomplete widget to show suggestions, and on each selection add a new item to the selecteditems array, using a CustomBindingHandler.
3) Use a CustomBindingHandler to show a JQUERY UI ToolTip widget to the each SPAN which are bound to observable array selecteditems.
Issue- that I am facing is JQUERY UI ToolTip widget is showing up in the load without any issues, but whenever there is a change in the selecteditems array, the Tooltip widget is not recognized in the CustomBindingHandler
Any help would be appreciated very much.
<div>
<div style="max-height: 105px;" data-bind="foreach: selectedItems">
<span data-bind="text: name, id: id, assignToolTip: id"></span>
<input data-bind="assignAutoComplete: { rootVm: $root }" type="email" value="">
</div>
</div>
<script>
var MyViewModel = function () {
this.selectedItems = ko.observableArray(
[{ name: "eww", id: "ww" },
{ name: "aa", id: "vv" },
{ name: "xx", id: "zz" }]);
};
ko.bindingHandlers.assignToolTip = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
if ($(element) != undefined) {
var currentDataItem = ko.dataFor(element);
$(element).tooltip({
items: 'span',
track: true,
content: function () {
return "<ul><li>" + currentDataItem.name + "</li><li>" + currentDataItem.id + "</li></ul>";
}
});
}
},
};
ko.bindingHandlers.assignAutoComplete = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
if ($(element) != undefined) {
var currentDataItem = ko.dataFor(element);
$(element).autocomplete({
source: function (request, response) {
$.ajax({
url: "http://ws.geonames.org/searchJSON",
dataType: "jsonp",
data: {
featureClass: "P",
style: "full",
maxRows: 12,
name_startsWith: request.term
},
success: function (data) {
response($.map(data.geonames, function (item) {
return {
label: item.name + (item.adminName1 ? ", " + item.adminName1 : "") + ", " + item.countryName,
value: item.name
};
}));
}
});
},
minLength: 2,
select: function (event, ui) {
var settings = valueAccessor();
var rootVm = settings.rootVm;
rootVm.selectedItems.push({ name: ui.item.label, id: ui.item.label });
return false;
},
open: function () {
$(this).removeClass("ui-corner-all").addClass("ui-corner-top");
},
close: function () {
$(this).removeClass("ui-corner-top").addClass("ui-corner-all");
}
});
}
}
};
ko.applyBindings(new MyViewModel());
</script>
<script src="~/Scripts/jquery-ui-1.10.3.js"></script>
Upvotes: 0
Views: 1323
Reputation: 338326
The API documentation for the jQuery UI Tooltip widget suggests that the tooltip logic is intended to be bound to the container instead of to the individual elements.
For instance, to get a tooltip for all <li>
in an <ul>
in pure jQuery, you would do this:
$("ul").tooltip({
items: "li",
content: function () {
return "tooltip text for this element";
}
});
The main advantage is that you don't need to bind/unbind/update any tooltip logic when the container's children change. The other advantage is that this puts less load on the page because it registers only a single tooltip instead of several ones.
You can (and should!) use this approach because it fits your requirements perfectly. You have a container that has a variable number of children that should all show a tooltip with content built after the same logic.
Since we bind to the container, we need a tiny proxy function on the container's view model to retrieve the individual tooltip text for us.
The HTML template:
<div data-bind="
foreach: items,
tooltip: {items: 'label', content: tooltipContentProxy}
">
<div>
<label data-bind="text: name, attr: {for: id}"></label>
<input data-bind="attr: {id: id}, value: inputVal, valueUpdate: 'keyup'" type="text" />
</div>
</div>
The tooltip
custom binding handler:
ko.bindingHandlers.tooltip = {
init: function (element, valueAccessor) {
var options = ko.unwrap(valueAccessor());
$(element).tooltip(options);
}
};
Note how we
update
handler because this setup in decoupled from any data changesAnd finally our view model:
function Item(data) {
var self = this;
self.id = ko.observable(data.id);
self.name = ko.observable(data.name);
self.inputVal = ko.observable("");
self.tooltipText = ko.computed(function () {
var escapedVal = $("<div>", {text: self.inputVal()}).html();
return "Hi! My value is '" + escapedVal + "'.";
});
}
function ViewModel() {
var self = this;
self.items = ko.observableArray([/* Item objects here ...*/]);
self.tooltipContentProxy = function () {
var targetItem = ko.dataFor(this);
return targetItem.tooltipText();
};
}
Now tooltips show correctly without any further fuss. http://jsfiddle.net/7TqpK/
Upvotes: 0
Reputation: 7131
If you are trying to update the tooltip when a value in your array changes then you will need to change this around a bit so you can observe on the values of the object within your array.
var SelectedItem = function(obj){
var self = this;
self.name = ko.observable(obj.name);
self.id = ko.observable(obj.id);
self.tooltipText = ko.computed(function(){
return "<ul><li>" + self.name() + "</li><li>" + self.id() + "</li></ul>";
});
return self;
};
var MyViewModel = function () {
var self = this;
self.selectedItems = ko.observableArray(
[new SelectedItem({ name: "eww", id: "ww" }),
new SelectedItem({ name: "aa", id: "vv" }),
new SelectedItem({ name: "xx", id: "zz" })]);
return self;
};
Once that is complete you need to update the customBinding to handle updates:
ko.bindingHandlers.assignToolTip = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
if ($(element) != undefined) {
var currentDataItem = ko.dataFor(element);
$(element).tooltip({
items: 'span',
track: true,
content: function () {
return currentDataItem.tooltipText();
}
});
}
},
update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext){
if ($(element) != undefined) {
$(element).tooltip( "destroy" );
var currentDataItem = ko.dataFor(element);
$(element).tooltip({
items: 'span',
track: true,
content: function () {
return currentDataItem.tooltipText();
}
});
}
}
};
The last change needed is that any time you push into the observable array it should be an instance of the SelectedItem
object:
select: function (event, ui) {
var settings = valueAccessor();
var rootVm = settings.rootVm;
rootVm.selectedItems.push(
new SelectedItem({ name: ui.item.label, id: ui.item.label })
);
return false;
},
Working Example: http://jsfiddle.net/infiniteloops/PLYKk/
Upvotes: 0