Search code examples
twitter-bootstrapknockout.jspopoverknockout-2.0

Knockout Twitter Bootstrap Popover Binding


I am trying to create a custom binding for twitter boostrap popovers that references a template but I am having trouble with the binding part of the content inside of the popover once it has been created.

I have seen this question asked before but I feel like they were mostly pretty messy and I am pretty close to a reusable solution that uses templates how I want to.

http://jsfiddle.net/billpull/Edptd/

// Bind Twitter Popover
ko.bindingHandlers.popover = {
    init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var tmplId = ko.utils.unwrapObservable(valueAccessor());
        var tmplHtml = $('#' + tmplId).html();
        var uuid = guid();
        var domId = "ko-bs-popover-" + uuid;
        var tmplDom = $('<div/>', {
            "class" : "ko-popover",
            "id" : domId
        }).html(tmplHtml);

        options = {
            content: tmplDom[0].outerHTML
        };

        var popoverOptions = ko.utils.extend(ko.bindingHandlers.popover.options, options);

        console.log($(element));
        console.log(element);

        $(element).bind('click', function () {
            $(this).popover(popoverOptions).popover('toggle');
            ko.applyBindings(bindingContext, document.getElementById(domId));
        });
    },
    options: {
        placement: "right",
        title: "",
        html: true,
        content: "",
        trigger: "manual"
    }
};

===EDIT

Updated code based on answer below that allows you to do it without the extra withProperties binding

// Bind Twitter Popover
ko.bindingHandlers.popover = {
    init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        // read popover options 
        var popoverBindingValues = ko.utils.unwrapObservable(valueAccessor());

        // set popover template id
        var tmplId = popoverBindingValues.template;

        // set popover trigger
        var trigger = popoverBindingValues.trigger;

        // get template html
        var tmplHtml = $('#' + tmplId).html();

        // create unique identifier to bind to
        var uuid = guid();
        var domId = "ko-bs-popover-" + uuid;

        // create correct binding context
        var childBindingContext = bindingContext.createChildContext(viewModel);

        // create DOM object to use for popover content
        var tmplDom = $('<div/>', {
            "class" : "ko-popover",
            "id" : domId
        }).html(tmplHtml);

        // set content options
        options = {
            content: tmplDom[0].outerHTML
        };

        // Need to copy this, otherwise all the popups end up with the value of the last item
        var popoverOptions = $.extend({}, ko.bindingHandlers.popover.options);
        popoverOptions.content = options.content;

        // bind popover to element click
        $(element).bind(trigger, function () {
            $(this).popover(popoverOptions).popover('toggle');

            // if the popover is visible bind the view model to our dom ID
            if($('#' + domId).is(':visible')){
                ko.applyBindingsToDescendants(childBindingContext, $('#' + domId)[0]);
            }
        });

        return { controlsDescendantBindings: true };
    },
    options: {
        placement: "right",
        title: "",
        html: true,
        content: "",
        trigger: "manual"
    }
};

Solution

  • You need to use my old friend, custom bindings.

    ko.bindingHandlers.withProperties = {
        init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
            // Make a modified binding context, with a extra properties, and apply it to descendant elements
            var newProperties = valueAccessor(),
                innerBindingContext = bindingContext.extend(newProperties);
            ko.applyBindingsToDescendants(innerBindingContext, element);
    
            // Also tell KO *not* to bind the descendants itself, otherwise they will be bound twice
            return { controlsDescendantBindings: true };
        }
    };
    

    You then need to add a data-bind attribute to the html you are generating:

        var tmplDom = $('<div/>', {
            "class": "ko-popover",
            "id": domId,
            "data-bind": "withProperties: { label: '" + viewModel.label() + "', required: '" + viewModel.required() + "' }"
    

    I've put together a jsFiddle showing this. There were a couple of gotchas, I had to copy the popover options for each popover, otherwise they all ended up with the last set of values.

        var popoverOptions = $.extend({}, ko.bindingHandlers.popover.options);
        popoverOptions.content = options.content;
    

    And I also had to apply binding to the popup only if it is visible, otherwise it appears to attempt to bind to the whole page.

    $(element).bind('click', function () {
                $(this).popover(popoverOptions).popover('toggle');
                // If you apply this when the popup isn't visible, I think that it tries to bind to thewhole pageand throws an error
                if($('#' + domId).is(':visible'))
                {
                    ko.applyBindings(viewModel, $('#' + domId)[0]);
                }
            });
    

    This also appears to be 2-way, in that you can change the values in the popup and it updates the non-popup elements, but I won't lie, I didn't expect that to happen!