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 po
I adapted another answer here: https://stackoverflow.com/a/16876013/1061602
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.
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!
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: " " });
$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>
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>