sqlnewbie
sqlnewbie

Reputation: 867

Knockout and JQuery UI custom binding no recoginizing widget on observable array update

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

Answers (2)

Tomalak
Tomalak

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

  • can configure all the tooltip options conveniently in the binding
  • don't have any dependencies on the view or the view model
  • don't even need an update handler because this setup in decoupled from any data changes

And 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

Gary.S
Gary.S

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

Related Questions