Knockout Twitter Bootstrap Popover Binding
Asked Answered
U

4

7

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"
    }
};
Urchin answered 11/2, 2013 at 22:21 Comment(6)
What's actually the problem here? You haven't described what the issue is.Paramaribo
"I am having trouble with the binding part of the content inside of the popover once it has been created."Urchin
I'm sure that it must be possible, though not straightforward, but I haven't got time to look at the moment. I'll have a look tonight.Paramaribo
thanks yea I believe I am just not accessing the correct variable when applying the bindings it seems even trickier when you have more than one viewmodel on a page.Urchin
@Urchin I've playing for hours with your library knockout-boostrap trying to add a select in a popover and after it renders I want to make it a bootstrap select control. how do I implement afterRender in your popover binding? silviomoreto.github.io/bootstrap-select/3Mythicize
@Urchin already make it work, added a afterRender method so now I can call your popover this way: data-bind="popover: { template: 'pop-var', afterRender: $data.styleMe } ". good work!Mythicize
P
6

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!

Paramaribo answered 12/2, 2013 at 16:26 Comment(0)
E
3

Here's one more version of the Knockout popover binding which makes use of an html template defined within the document.

Check out this fiddle: https://jsfiddle.net/2cpcgz3o/

(function () {
    var templateEngine = new ko.nativeTemplateEngine();

    ko.bindingHandlers.customPopover = {
        init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
            var placement = allBindings.get("placement") || "top",
                trigger = allBindings.get("trigger") || "click",
                templateName = allBindings.get("customPopover") || null,
                $element = $(element);

            $element.popover({ placement: placement, trigger: trigger, html: true, content: "&nbsp;" });

            $element.on("inserted.bs.popover", function () {
                var container = $element.next().find(".popover-content")[0];
                if (templateName) {
                    ko.renderTemplate(templateName, viewModel, { templateEngine: templateEngine }, container);
                }
                else {
                    container.innerHTML = $element.attr("data-content");
                }
            });

            ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
                $element.popover("destroy");
            });
        }
    };
})();

var model = {
  linkText: "Click me!",
  innerText: "Some fancy text"
};

ko.applyBindings(model);
<link href="https://cdn.jsdelivr.net/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<a data-bind="text: linkText, customPopover: 'popover-template', trigger: 'focus', placement: 'bottom'" tabindex="0" role="button"></a>

<script type="text/html" id="popover-template">
  <span data-bind="text: innerText"></span>
</script>
Essary answered 22/3, 2017 at 17:43 Comment(1)
The ko.renderTemplate fixed my issue after upgrading to bootstrap to 3.4.1. ThanksAiello
C
1

I adapted another answer here: https://mcmap.net/q/513970/-bootstrap-39-s-tooltip-not-working-with-knockout-bindings-w-fiddle

This works a lot better for me, especially for a simple popover.

ko.bindingHandlers.popover = {
    init: function (element, valueAccessor) {
        var local = ko.utils.unwrapObservable(valueAccessor()),
            options = {};

        ko.utils.extend(options, ko.bindingHandlers.popover.options);
        ko.utils.extend(options, local);

        $(element).popover(options);

        ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
            $(element).popover("destroy");
        });
    },
    options: {
        placement: "top"
    }
};

Then the binding is:

<span data-bind="popover: { content: mySimpleTextContent }"></span>

You can override other options obviously.

Continually answered 6/1, 2014 at 16:14 Comment(1)
Nice. Since I posted this question I have made a library of bootstrap-knockout extensions I am looking for any feedback available github.com/billpull/knockout-bootstrapUrchin
L
0

Slightly modified dodbrian's example. The content is bind to an observable.

https://jsfiddle.net/SergeyZhilyakov/0zxamcj6/14/

var model = {
  linkText: "Hover me!",
  innerText: ko.observable("Please, wait...")
};

ko.bindingHandlers.popover = {
  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
    var $element = $(element);
    var placement = allBindings.get("placement") || "top";
    var trigger = allBindings.get("trigger") || "hover";
    var content = allBindings.get("popover");

    $element.popover({
      placement: placement,
      trigger: trigger,
      content: content()
    });

    var popover = $element.data("bs.popover");
    content.subscribe(function(newValue) {
      popover.options.content = newValue;
      popover.setContent();
      popover.$tip.addClass(popover.options.placement);
    });

    ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
      $element.popover("destroy");
    });
  }
};

ko.applyBindings(model);

setTimeout(function() {
  model.innerText("Done!");
}, 3000);
body {
  padding: 20px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link href="https://cdn.jsdelivr.net/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>


<button type="button" class="btn btn-default" data-bind="text: linkText, popover: innerText, placement: 'bottom'">Comment</button>
Leathern answered 12/4, 2017 at 12:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.