I am using the knockout js template binding functionality to render a collection of items to an element:
<script type="text/javascript">
ko.applyBindings(new function () {
this.childItems = [{ Text: "Test", ImageUrl: "Images/Image.png" }];
});
</script>
<script type="text/html" id="template">
<div class="childItem" data-bind="attr: { title: Text }">
<img data-bind="attr: { src: ImageUrl }" />
</div>
</script>
<div class="childSelector" data-bind="template: { name: 'template', foreach: childItems }">
</div>
When clicked, the child items are cloned and placed into another element:
$(".childSelector").on("click", ".childItem", function () {
var clone = $(this).clone()[0];
ko.cleanNode(clone);
$(".targetNode").append(clone);
});
The problem is that when the source data changes and the template is re-bound to the new data, the following error is thrown:
Uncaught Error: Unable to parse bindings. Message: ReferenceError:
Text is not defined; Bindings value: attr: { title: Text }
I had found another post that suggested using ko.cleanNode(element)
to remove knockout's bindings, however this has not resolved the issue in my case.
Is there a way to remove knockout's bindings on a cloned element to prevent this error when re-binding? If not I'll just "manually" clone the element by extracting the required data from the clicked element.
Here is a simple example of what I'm doing
You can remove all knockout bindings from an element by traversing the DOM and removing the data-bind
attributes and knockout comments.
Use removeDataBindings(clone);
but first clean the node with ko.cleanNode(clone)
to clear any event handlers.
var commentNodesHaveTextProperty = document.createComment("test").text === "<!--test-->";
var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko(?:\s+(.+\s*\:[\s\S]*))?\s*-->$/ : /^\s*ko(?:\s+(.+\s*\:[\s\S]*))?\s*$/;
var endCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*\/ko\s*-->$/ : /^\s*\/ko\s*$/;
function isStartComment(node) {
return (node.nodeType == 8) && (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(startCommentRegex);
}
function isEndComment(node) {
return (node.nodeType == 8) && (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(endCommentRegex);
}
function traverseNode(node, func) {
func(node);
node = node.firstChild;
while (node) {
traverseNode(node, func);
node = node.nextSibling;
}
}
function removeDataBindings(element) {
var koComments = [];
traverseNode(element, function (node) {
if (isStartComment(node) || isEndComment(node)) {
koComments.push(node);
return;
}
//remove the 'data-bind' attributes
if (node.nodeType === 1) { //ELEMENT_NODE
node.removeAttribute('data-bind');
}
});
//remove Knockout binding comments
for (i = 0; i < koComments.length; i++) {
node = koComments[i];
if (node.parentNode) {
node.parentNode.removeChild(node);
}
}
}
Oliver, using jQuery to clone elements bound to knockout like this is not a good idea. You should be using data-binding for the targetNode
. If you haven't yet done so, its a good idea to go through the Knockout Tutorials to get a good understanding of the basic uses.
If you are trying to keep a list of items, with a clone button, here is a dead simple fiddle using nothing but Knockout to do so. If you are trying to do something else, let me know; your question isn't entirely clear on your goal.
HTML:
<div data-bind="foreach: items">
<span data-bind="text: $data"></span>
<button data-bind="click: $parent.clone">Clone</button></br>
</div>
JS:
var ViewModel = function(data) {
var self = this;
self.items = ko.observableArray(data);
self.clone = function(item) {
//The ko.toJS here is a handy copy tool for viewModels
//It isn't necessary for simple arrays like this one
//But I included it because for an array of objects, you will want to use it
self.items.push(ko.toJS(item));
};
};