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"
}
};
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!